store: move merge_objects into persistence helpers
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user