store: move merge_objects into persistence helpers

This commit is contained in:
Johannes Kirschbauer
2025-07-22 14:23:24 +02:00
parent 77f75b916d
commit 0ea42ae541
2 changed files with 59 additions and 39 deletions

View File

@@ -2,7 +2,6 @@ import argparse
import logging import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import TypeVar, cast
from clan_lib.api import API from clan_lib.api import API
from clan_lib.dirs import get_clan_flake_toplevel_or_env from clan_lib.dirs import get_clan_flake_toplevel_or_env
@@ -12,7 +11,7 @@ from clan_lib.git import commit_file
from clan_lib.nix_models.clan import InventoryMachine from clan_lib.nix_models.clan import InventoryMachine
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path from clan_lib.persist.util import merge_objects, set_value_by_path
from clan_lib.templates.handler import machine_template from clan_lib.templates.handler import machine_template
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
@@ -28,41 +27,6 @@ class CreateOptions:
target_host: str | None = None target_host: str | None = None
T = TypeVar("T")
def merge_objects(obj1: T, obj2: T) -> T:
"""
Updates values in obj2 by values of Obj1
The output contains values for all keys of Obj1 and Obj2 together
Lists are deduplicated and appended almost like in the nix module system.
"""
result = {}
msg = f"cannot update non-dictionary values: {obj2} by {obj1}"
if not isinstance(obj1, dict):
raise ClanError(msg)
if not isinstance(obj2, dict):
raise ClanError(msg)
all_keys = set(obj1.keys()).union(obj2.keys())
for key in all_keys:
val1 = obj1.get(key)
val2 = obj2.get(key)
if isinstance(val1, dict) and isinstance(val2, dict):
result[key] = merge_objects(val1, val2)
elif isinstance(val1, list) and isinstance(val2, list):
result[key] = list(dict.fromkeys(val2 + val1)) # type: ignore
elif key in obj1:
result[key] = val1 # type: ignore
elif key in obj2:
result[key] = val2 # type: ignore
return cast(T, result)
@API.register @API.register
def create_machine( def create_machine(
opts: CreateOptions, opts: CreateOptions,
@@ -122,7 +86,7 @@ def create_machine(
inventory = inventory_store.read() inventory = inventory_store.read()
curr_machine = inventory.get("machines", {}).get(machine_name, {}) curr_machine = inventory.get("machines", {}).get(machine_name, {})
new_machine = merge_objects(opts.machine, curr_machine) new_machine = merge_objects(curr_machine, opts.machine)
set_value_by_path( set_value_by_path(
inventory, inventory,

View File

@@ -3,11 +3,67 @@ Utilities for working with nested dictionaries, particularly for
flattening, unmerging lists, finding duplicates, and calculating patches. flattening, unmerging lists, finding duplicates, and calculating patches.
""" """
import json
from collections import Counter from collections import Counter
from typing import Any from typing import Any, TypeVar, cast
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
T = TypeVar("T")
empty: list[str] = []
def merge_objects(
curr: T, update: T, merge_lists: bool = True, path: list[str] = empty
) -> T:
"""
Updates values in curr by values of update
The output contains values for all keys of curr and update together.
Lists are deduplicated and appended almost like in the nix module system.
Example:
merge_objects({"a": 1}, {"a": null }) -> {"a": null}
merge_objects({"a": null}, {"a": 1 }) -> {"a": 1}
"""
result = {}
msg = f"cannot update non-dictionary values: {curr} by {update}"
if not isinstance(update, dict):
raise ClanError(msg)
if not isinstance(curr, dict):
raise ClanError(msg)
all_keys = set(update.keys()).union(curr.keys())
for key in all_keys:
curr_val = curr.get(key)
update_val = update.get(key)
if isinstance(update_val, dict) and isinstance(curr_val, dict):
result[key] = merge_objects(
curr_val, update_val, merge_lists=merge_lists, path=[*path, key]
)
elif isinstance(update_val, list) and isinstance(curr_val, list):
if merge_lists:
result[key] = list(dict.fromkeys(curr_val + update_val)) # type: ignore
else:
result[key] = update_val # type: ignore
elif (
update_val is not None
and curr_val is not None
and type(update_val) is not type(curr_val)
):
msg = f"Type mismatch for key '{key}'. Cannot update {type(curr_val)} with {type(update_val)}"
raise ClanError(msg, location=json.dumps([*path, key]))
elif key in update:
result[key] = update_val # type: ignore
elif key in curr:
result[key] = curr_val # type: ignore
return cast(T, result)
def path_match(path: list[str], whitelist_paths: list[list[str]]) -> bool: def path_match(path: list[str], whitelist_paths: list[list[str]]) -> bool:
""" """