persistence: generate properties for data by traversing data
This commit is contained in:
@@ -11,7 +11,6 @@ from clan_lib.nix_models.clan import (
|
||||
InventoryInstancesType,
|
||||
InventoryMachinesType,
|
||||
InventoryMetaType,
|
||||
InventoryTagsType,
|
||||
)
|
||||
from clan_lib.persist.patch_engine import calc_patches
|
||||
from clan_lib.persist.path_utils import (
|
||||
@@ -20,7 +19,7 @@ from clan_lib.persist.path_utils import (
|
||||
path_match,
|
||||
set_value_by_path_tuple,
|
||||
)
|
||||
from clan_lib.persist.write_rules import AttributeMap, compute_attribute_map
|
||||
from clan_lib.persist.write_rules import AttributeMap, compute_attribute_persistence
|
||||
|
||||
|
||||
def unwrap_known_unknown(value: Any) -> Any:
|
||||
@@ -102,7 +101,6 @@ class InventorySnapshot(TypedDict):
|
||||
machines: NotRequired[InventoryMachinesType]
|
||||
instances: NotRequired[InventoryInstancesType]
|
||||
meta: NotRequired[InventoryMetaType]
|
||||
tags: NotRequired[InventoryTagsType]
|
||||
|
||||
|
||||
class InventoryStore:
|
||||
@@ -216,7 +214,7 @@ class InventoryStore:
|
||||
data_eval: InventorySnapshot = self._load_merged_inventory()
|
||||
data_disk: InventorySnapshot = self._get_persisted()
|
||||
|
||||
write_map = compute_attribute_map(
|
||||
write_map = compute_attribute_persistence(
|
||||
current_priority,
|
||||
dict(data_eval),
|
||||
dict(data_disk),
|
||||
|
||||
@@ -14,7 +14,10 @@ from clan_lib.persist.path_utils import (
|
||||
set_value_by_path,
|
||||
set_value_by_path_tuple,
|
||||
)
|
||||
from clan_lib.persist.write_rules import PersistenceAttribute, compute_attribute_map
|
||||
from clan_lib.persist.write_rules import (
|
||||
PersistenceAttribute,
|
||||
compute_attribute_persistence,
|
||||
)
|
||||
|
||||
# --- calculate_static_data ---
|
||||
|
||||
@@ -219,7 +222,7 @@ def test_update_simple() -> None:
|
||||
|
||||
data_disk: dict = {}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -256,7 +259,7 @@ def test_update_add_empty_dict() -> None:
|
||||
|
||||
data_disk: dict = {}
|
||||
|
||||
writeables = compute_attribute_map(prios, data_eval, data_disk)
|
||||
writeables = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
update = deepcopy(data_eval)
|
||||
|
||||
@@ -297,7 +300,7 @@ def test_update_many() -> None:
|
||||
|
||||
data_disk = {"foo": {"bar": "baz", "nested": {"x": "x"}}}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -356,7 +359,7 @@ def test_update_parent_non_writeable() -> None:
|
||||
},
|
||||
}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.READONLY},
|
||||
@@ -380,7 +383,8 @@ def test_update_parent_non_writeable() -> None:
|
||||
# def test_remove_non_writable_attrs() -> None:
|
||||
# prios = {
|
||||
# "foo": {
|
||||
# "__prio": 100, # <- writeable: "foo"
|
||||
# "__this": {"total": True, "prio": 100},
|
||||
# # We cannot delete children because "foo" is total
|
||||
# },
|
||||
# }
|
||||
|
||||
@@ -388,7 +392,7 @@ def test_update_parent_non_writeable() -> None:
|
||||
|
||||
# data_disk: dict = {}
|
||||
|
||||
# writeables = compute_write_map(prios, data_eval, data_disk)
|
||||
# attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
# update: dict = {
|
||||
# "foo": {
|
||||
@@ -398,7 +402,9 @@ def test_update_parent_non_writeable() -> None:
|
||||
# }
|
||||
|
||||
# with pytest.raises(ClanError) as error:
|
||||
# calc_patches(data_disk, update, all_values=data_eval, writeables=writeables)
|
||||
# calc_patches(
|
||||
# data_disk, update, all_values=data_eval, attribute_props=attribute_props
|
||||
# )
|
||||
|
||||
# assert "Cannot delete path 'foo.baz'" in str(error.value)
|
||||
|
||||
@@ -417,7 +423,7 @@ def test_update_list() -> None:
|
||||
|
||||
data_disk = {"foo": ["B"]}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -464,7 +470,7 @@ def test_update_list_duplicates() -> None:
|
||||
|
||||
data_disk = {"foo": ["B"]}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -483,19 +489,26 @@ def test_update_list_duplicates() -> None:
|
||||
|
||||
def test_dont_persist_defaults() -> None:
|
||||
"""Default values should not be persisted to disk if not explicitly requested by the user."""
|
||||
prios = {
|
||||
introspection = {
|
||||
"enabled": {"__prio": 1500},
|
||||
"config": {"__prio": 100},
|
||||
"config": {
|
||||
"__prio": 100,
|
||||
"foo": { # <- default in the 'config' submodule
|
||||
"__prio": 1500,
|
||||
},
|
||||
},
|
||||
}
|
||||
data_eval = {
|
||||
"enabled": True,
|
||||
"config": {"foo": "bar"},
|
||||
}
|
||||
data_disk: dict[str, Any] = {}
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(introspection, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("enabled",): {PersistenceAttribute.WRITE},
|
||||
("config",): {PersistenceAttribute.WRITE},
|
||||
("config", "foo"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
}
|
||||
|
||||
update = deepcopy(data_eval)
|
||||
@@ -526,8 +539,8 @@ def test_set_null() -> None:
|
||||
data_disk,
|
||||
update,
|
||||
all_values=data_eval,
|
||||
attribute_props=compute_attribute_map(
|
||||
{"__prio": 100, "foo": {"__prio": 100}},
|
||||
attribute_props=compute_attribute_persistence(
|
||||
{"__prio": 100, "foo": {"__prio": 100}, "bar": {"__prio": 100}},
|
||||
data_eval,
|
||||
data_disk,
|
||||
),
|
||||
@@ -549,9 +562,24 @@ def test_machine_delete() -> None:
|
||||
}
|
||||
data_disk = data_eval
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
assert attribute_props == {
|
||||
("machines",): {PersistenceAttribute.WRITE},
|
||||
("machines", "foo"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
("machines", "foo", "name"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("machines", "bar"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
("machines", "bar", "name"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("machines", "naz"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
("machines", "naz", "name"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
}
|
||||
|
||||
# Delete machine "bar" from the inventory
|
||||
@@ -580,7 +608,7 @@ def test_update_mismatching_update_type() -> None:
|
||||
|
||||
data_disk: dict = {}
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -611,10 +639,11 @@ def test_delete_key() -> None:
|
||||
|
||||
data_disk = data_eval
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
("foo", "bar"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
}
|
||||
|
||||
# remove all keys from foo
|
||||
@@ -652,10 +681,36 @@ def test_delete_key_intermediate() -> None:
|
||||
|
||||
data_disk = data_eval
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
("foo", "bar"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
("foo", "bar", "name"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("foo", "bar", "info"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("foo", "bar", "other"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("foo", "other"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE},
|
||||
("foo", "other", "name"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("foo", "other", "info"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
("foo", "other", "other"): {
|
||||
PersistenceAttribute.WRITE,
|
||||
PersistenceAttribute.DELETE,
|
||||
},
|
||||
}
|
||||
# remove all keys from foo
|
||||
|
||||
@@ -687,10 +742,16 @@ def test_delete_key_non_writeable() -> None:
|
||||
|
||||
data_disk = data_eval
|
||||
|
||||
attribute_props = compute_attribute_map(prios, data_eval, data_disk)
|
||||
attribute_props = compute_attribute_persistence(prios, data_eval, data_disk)
|
||||
|
||||
# TOOD: Collapse these paths, by early stopping the recursion in
|
||||
# compute_attribute_persistence
|
||||
assert attribute_props == {
|
||||
("foo",): {PersistenceAttribute.READONLY},
|
||||
("foo", "bar"): {PersistenceAttribute.READONLY},
|
||||
("foo", "bar", "name"): {PersistenceAttribute.READONLY},
|
||||
("foo", "bar", "info"): {PersistenceAttribute.READONLY},
|
||||
("foo", "bar", "other"): {PersistenceAttribute.READONLY},
|
||||
}
|
||||
|
||||
# remove all keys from foo
|
||||
|
||||
@@ -139,10 +139,17 @@ def _determine_props_recursive(
|
||||
if results is None:
|
||||
results = {}
|
||||
|
||||
for key, value in priorities.items():
|
||||
if not isinstance(all_values, dict):
|
||||
# Nothing to do for non-dict values
|
||||
# they are handled at parent level
|
||||
return results
|
||||
|
||||
for key in all_values:
|
||||
value = priorities.get(key, {})
|
||||
|
||||
# Skip metadata keys
|
||||
if key in {"__this", "__list", "__prio"}:
|
||||
continue
|
||||
# if key in {"__this", "__list", "__prio"}:
|
||||
# continue
|
||||
|
||||
path = (*current_path, key)
|
||||
|
||||
@@ -170,7 +177,7 @@ def _determine_props_recursive(
|
||||
if force_non_writeable:
|
||||
results.setdefault(path, set()).clear()
|
||||
results.setdefault(path, set()).add(PersistenceAttribute.READONLY)
|
||||
# All children are also non-writeable
|
||||
# All children are also non-writeable, we are done here.
|
||||
if isinstance(value, dict):
|
||||
_determine_props_recursive(
|
||||
value,
|
||||
@@ -199,6 +206,8 @@ def _determine_props_recursive(
|
||||
results.setdefault(path, set()).add(PersistenceAttribute.READONLY)
|
||||
|
||||
# Recurse into children
|
||||
# TODO: Dont need to recurse?
|
||||
# if the current value is READONLY all children are readonly as well
|
||||
if isinstance(value, dict):
|
||||
_determine_props_recursive(
|
||||
value,
|
||||
@@ -214,7 +223,7 @@ def _determine_props_recursive(
|
||||
return results
|
||||
|
||||
|
||||
def compute_attribute_map(
|
||||
def compute_attribute_persistence(
|
||||
priorities: dict[str, Any], all_values: dict[str, Any], persisted: dict[str, Any]
|
||||
) -> AttributeMap:
|
||||
"""Determine writeability for all paths based on priorities and current data.
|
||||
@@ -232,4 +241,14 @@ def compute_attribute_map(
|
||||
Dict with sets of writeable and non-writeable paths using tuple keys
|
||||
|
||||
"""
|
||||
persistence_unsupported = set(all_values.keys()) - set(priorities.keys())
|
||||
if persistence_unsupported:
|
||||
msg = (
|
||||
f"Persistence priorities are not defined for top-level keys: "
|
||||
f"{', '.join(sorted(persistence_unsupported))}. "
|
||||
f"Either remove them from the python inventory model or extend support of"
|
||||
f" persistence properties in 'clanInternals.inventoryClass.introspection'."
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
return _determine_props_recursive(priorities, all_values, persisted)
|
||||
|
||||
@@ -5,7 +5,10 @@ import pytest
|
||||
|
||||
from clan_lib.flake.flake import Flake
|
||||
from clan_lib.persist.inventory_store import InventoryStore
|
||||
from clan_lib.persist.write_rules import PersistenceAttribute, compute_attribute_map
|
||||
from clan_lib.persist.write_rules import (
|
||||
PersistenceAttribute,
|
||||
compute_attribute_persistence,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_lib.nix_models.clan import Clan
|
||||
@@ -21,17 +24,13 @@ def test_write_integration(clan_flake: Callable[..., Flake]) -> None:
|
||||
data_eval = cast("dict", inventory_store.read())
|
||||
prios = flake.select("clanInternals.inventoryClass.introspection")
|
||||
|
||||
res = compute_attribute_map(prios, data_eval, {})
|
||||
res = compute_attribute_persistence(prios, data_eval, {})
|
||||
|
||||
# We should be able to write to these top-level keys
|
||||
assert PersistenceAttribute.WRITE in res[("machines",)]
|
||||
assert PersistenceAttribute.WRITE in res[("instances",)]
|
||||
assert PersistenceAttribute.WRITE in res[("meta",)]
|
||||
|
||||
# # Managed by nix
|
||||
assert PersistenceAttribute.WRITE not in res[("assertions",)]
|
||||
assert PersistenceAttribute.READONLY in res[("assertions",)]
|
||||
|
||||
|
||||
# New style __this.prio
|
||||
|
||||
@@ -47,9 +46,9 @@ def test_write_simple() -> None:
|
||||
"foo.bar": {"__this": {"prio": 1000}},
|
||||
}
|
||||
|
||||
default: dict = {"foo": {}}
|
||||
data: dict = {}
|
||||
res = compute_attribute_map(prios, default, data)
|
||||
all_values: dict = {"foo": {"bar": "baz"}, "foo.bar": {}}
|
||||
persisted: dict = {}
|
||||
res = compute_attribute_persistence(prios, all_values, persisted)
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -74,8 +73,8 @@ def test_write_inherited() -> None:
|
||||
},
|
||||
}
|
||||
|
||||
data: dict = {}
|
||||
res = compute_attribute_map(prios, {"foo": {"bar": {}}}, data)
|
||||
persisted: dict = {}
|
||||
res = compute_attribute_persistence(prios, {"foo": {"bar": {"baz": {}}}}, persisted)
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -99,7 +98,7 @@ def test_non_write_inherited() -> None:
|
||||
}
|
||||
|
||||
data: dict = {}
|
||||
res = compute_attribute_map(prios, {}, data)
|
||||
res = compute_attribute_persistence(prios, {"foo": {"bar": {"baz": {}}}}, data)
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.READONLY},
|
||||
@@ -122,7 +121,7 @@ def test_write_list() -> None:
|
||||
"b",
|
||||
], # <- writeable: because lists are merged. Filtering out nix-values comes later
|
||||
}
|
||||
res = compute_attribute_map(prios, default, data)
|
||||
res = compute_attribute_persistence(prios, default, data)
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -144,9 +143,10 @@ def test_write_because_written() -> None:
|
||||
},
|
||||
}
|
||||
|
||||
data_eval: dict = {"foo": {"bar": {"baz": 1, "foobar": 1}}}
|
||||
# Given the following data. {}
|
||||
# Check that the non-writeable paths are correct.
|
||||
res = compute_attribute_map(prios, {"foo": {"bar": {}}}, {})
|
||||
res = compute_attribute_persistence(prios, data_eval, {})
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
@@ -165,7 +165,7 @@ def test_write_because_written() -> None:
|
||||
},
|
||||
},
|
||||
}
|
||||
res = compute_attribute_map(prios, {}, data)
|
||||
res = compute_attribute_persistence(prios, data_eval, data)
|
||||
|
||||
assert res[("foo", "bar", "baz")] == {
|
||||
PersistenceAttribute.WRITE,
|
||||
@@ -210,7 +210,7 @@ def test_static_object() -> None:
|
||||
data_eval: dict = {"foo": {"a": {"c": {"bar": 1}}}}
|
||||
persisted: dict = {"foo": {"a": {"c": {"bar": 1}}}}
|
||||
|
||||
res = compute_attribute_map(introspection, data_eval, persisted)
|
||||
res = compute_attribute_persistence(introspection, data_eval, persisted)
|
||||
assert res == {
|
||||
# We can extend "foo", "foo.a", "foo.a.c"
|
||||
# That means the user could define "foo.b"
|
||||
@@ -246,7 +246,7 @@ def test_attributes_totality() -> None:
|
||||
data_eval: dict = {"foo": {"a": {}}}
|
||||
persisted: dict = {"foo": {"a": {}}}
|
||||
|
||||
res = compute_attribute_map(introspection, data_eval, persisted)
|
||||
res = compute_attribute_persistence(introspection, data_eval, persisted)
|
||||
|
||||
assert res == {
|
||||
("foo",): {PersistenceAttribute.WRITE},
|
||||
|
||||
@@ -41,7 +41,7 @@ def list_tags(flake: Flake) -> TagList:
|
||||
for tag in role_tags:
|
||||
tags.add(tag)
|
||||
|
||||
global_tags = inventory.get("tags", {})
|
||||
global_tags = inventory_store.get_readonly_raw().get("tags", {})
|
||||
|
||||
for tag in global_tags:
|
||||
if tag not in tags:
|
||||
|
||||
Reference in New Issue
Block a user