From 104343a334eb615bba0dc775a2c762a949e0d85b Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 27 May 2025 17:08:24 +0200 Subject: [PATCH] feat(InventoryStore): return a restricted view of the inventory --- pkgs/clan-cli/clan_cli/clan/update.py | 5 +- pkgs/clan-cli/clan_lib/inventory/__init__.py | 5 +- .../clan-cli/clan_lib/nix_models/inventory.py | 111 ++++++++++++++++++ .../clan_lib/persist/inventory_store.py | 44 +++++-- 4 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 pkgs/clan-cli/clan_lib/nix_models/inventory.py diff --git a/pkgs/clan-cli/clan_cli/clan/update.py b/pkgs/clan-cli/clan_cli/clan/update.py index 7e1ac3355..8cf32f1bc 100644 --- a/pkgs/clan-cli/clan_cli/clan/update.py +++ b/pkgs/clan-cli/clan_cli/clan/update.py @@ -2,9 +2,8 @@ from dataclasses import dataclass from clan_lib.api import API from clan_lib.flake import Flake -from clan_lib.nix_models.clan import Inventory from clan_lib.nix_models.clan import InventoryMeta as Meta -from clan_lib.persist.inventory_store import InventoryStore +from clan_lib.persist.inventory_store import InventorySnapshot, InventoryStore from clan_lib.persist.util import set_value_by_path @@ -15,7 +14,7 @@ class UpdateOptions: @API.register -def update_clan_meta(options: UpdateOptions) -> Inventory: +def update_clan_meta(options: UpdateOptions) -> InventorySnapshot: inventory_store = InventoryStore(options.flake) inventory = inventory_store.read() set_value_by_path(inventory, "meta", options.meta) diff --git a/pkgs/clan-cli/clan_lib/inventory/__init__.py b/pkgs/clan-cli/clan_lib/inventory/__init__.py index 8e5114de2..7409ce8fe 100644 --- a/pkgs/clan-cli/clan_lib/inventory/__init__.py +++ b/pkgs/clan-cli/clan_lib/inventory/__init__.py @@ -13,12 +13,11 @@ Interacting with 'clan_lib.inventory' is NOT recommended and will be removed from clan_lib.api import API from clan_lib.flake import Flake -from clan_lib.nix_models.clan import Inventory -from clan_lib.persist.inventory_store import InventoryStore +from clan_lib.persist.inventory_store import InventorySnapshot, InventoryStore @API.register -def get_inventory(flake: Flake) -> Inventory: +def get_inventory(flake: Flake) -> InventorySnapshot: inventory_store = InventoryStore(flake) inventory = inventory_store.read() return inventory diff --git a/pkgs/clan-cli/clan_lib/nix_models/inventory.py b/pkgs/clan-cli/clan_lib/nix_models/inventory.py new file mode 100644 index 000000000..1d57a281b --- /dev/null +++ b/pkgs/clan-cli/clan_lib/nix_models/inventory.py @@ -0,0 +1,111 @@ +# DO NOT EDIT THIS FILE MANUALLY. IT IS GENERATED. +# This file was generated by running `pkgs/clan-cli/clan_lib.inventory/update.sh` +# +# ruff: noqa: N815 +# ruff: noqa: N806 +# ruff: noqa: F401 +# fmt: off +from typing import Any, Literal, NotRequired, TypedDict + + +# Mimic "unknown". +# 'Any' is unsafe because it allows any operations +# This forces the user to use type-narrowing or casting in the code +class Unknown: + pass + + +InstanceModuleNameType = str +InstanceModuleInputType = str + +class InstanceModule(TypedDict): + name: str + input: NotRequired[InstanceModuleInputType] + + + +InstanceRoleMachineSettingsType = Unknown + +class InstanceRoleMachine(TypedDict): + settings: NotRequired[InstanceRoleMachineSettingsType] + + + + + +class InstanceRoleTag(TypedDict): + pass + + + +InstanceRoleMachinesType = dict[str, InstanceRoleMachine] +InstanceRoleSettingsType = Unknown +InstanceRoleTagsType = dict[str, InstanceRoleTag] + +class InstanceRole(TypedDict): + machines: NotRequired[InstanceRoleMachinesType] + settings: NotRequired[InstanceRoleSettingsType] + tags: NotRequired[InstanceRoleTagsType] + + + +InstanceModuleType = InstanceModule +InstanceRolesType = dict[str, InstanceRole] + +class Instance(TypedDict): + module: NotRequired[InstanceModuleType] + roles: NotRequired[InstanceRolesType] + + + +MachineDeployTargethostType = str + +class MachineDeploy(TypedDict): + targetHost: NotRequired[MachineDeployTargethostType] + + + +MachineDeployType = MachineDeploy +MachineDescriptionType = str +MachineIconType = str +MachineMachineclassType = Literal["nixos", "darwin"] +MachineNameType = str +MachineTagsType = list[str] + +class Machine(TypedDict): + deploy: NotRequired[MachineDeployType] + description: NotRequired[MachineDescriptionType] + icon: NotRequired[MachineIconType] + machineClass: NotRequired[MachineMachineclassType] + name: NotRequired[MachineNameType] + tags: NotRequired[MachineTagsType] + + + +MetaNameType = str +MetaDescriptionType = str +MetaIconType = str + +class Meta(TypedDict): + name: str + description: NotRequired[MetaDescriptionType] + icon: NotRequired[MetaIconType] + +Service = dict[str, Any] + + + +InventoryInstancesType = dict[str, Instance] +InventoryMachinesType = dict[str, Machine] +InventoryMetaType = Meta +InventoryModulesType = dict[str, Any] +InventoryServicesType = dict[str, Service] +InventoryTagsType = dict[str, Any] + +class Inventory(TypedDict): + instances: NotRequired[InventoryInstancesType] + machines: NotRequired[InventoryMachinesType] + meta: NotRequired[InventoryMetaType] + modules: NotRequired[InventoryModulesType] + services: NotRequired[InventoryServicesType] + tags: NotRequired[InventoryTagsType] diff --git a/pkgs/clan-cli/clan_lib/persist/inventory_store.py b/pkgs/clan-cli/clan_lib/persist/inventory_store.py index 92944dfde..601b0b3e6 100644 --- a/pkgs/clan-cli/clan_lib/persist/inventory_store.py +++ b/pkgs/clan-cli/clan_lib/persist/inventory_store.py @@ -1,11 +1,17 @@ import json from dataclasses import dataclass from pathlib import Path -from typing import Any, Protocol +from typing import Any, NotRequired, Protocol, TypedDict from clan_lib.errors import ClanError from clan_lib.git import commit_file -from clan_lib.nix_models.clan import Inventory +from clan_lib.nix_models.clan import ( + Inventory, + InventoryInstancesType, + InventoryMachinesType, + InventoryMetaType, + InventoryServicesType, +) from .util import ( calc_patches, @@ -75,8 +81,8 @@ def sanitize(data: Any, whitelist_paths: list[str], current_path: list[str]) -> @dataclass class WriteInfo: writeables: dict[str, set[str]] - data_eval: Inventory - data_disk: Inventory + data_eval: "InventorySnapshot" + data_disk: "InventorySnapshot" class FlakeInterface(Protocol): @@ -92,6 +98,19 @@ class FlakeInterface(Protocol): def path(self) -> Path: ... +class InventorySnapshot(TypedDict): + """ + Restricted view of an Inventory. + + It contains only the keys that are convertible to python types and can be serialized to JSON. + """ + + machines: NotRequired[InventoryMachinesType] + instances: NotRequired[InventoryInstancesType] + meta: NotRequired[InventoryMetaType] + services: NotRequired[InventoryServicesType] + + class InventoryStore: def __init__( self, @@ -117,10 +136,11 @@ class InventoryStore: self._allowed_path_transforms = _allowed_path_transforms if _keys is None: - _keys = ["machines", "instances", "meta", "services"] + _keys = list(InventorySnapshot.__annotations__.keys()) + self._keys = _keys - def _load_merged_inventory(self) -> Inventory: + def _load_merged_inventory(self) -> InventorySnapshot: """ Loads the evaluated inventory. After all merge operations with eventual nix code in buildClan. @@ -140,7 +160,7 @@ class InventoryStore: return sanitized - def _get_persisted(self) -> Inventory: + def _get_persisted(self) -> InventorySnapshot: """ Load the inventory FILE from the flake directory If no file is found, returns an empty dictionary @@ -189,8 +209,8 @@ class InventoryStore: """ current_priority = self._get_inventory_current_priority() - data_eval: Inventory = self._load_merged_inventory() - data_disk: Inventory = self._get_persisted() + data_eval: InventorySnapshot = self._load_merged_inventory() + data_disk: InventorySnapshot = self._get_persisted() writeables = determine_writeability( current_priority, dict(data_eval), dict(data_disk) @@ -198,7 +218,7 @@ class InventoryStore: return WriteInfo(writeables, data_eval, data_disk) - def read(self) -> Inventory: + def read(self) -> InventorySnapshot: """ Accessor to the merged inventory @@ -226,7 +246,9 @@ class InventoryStore: commit_message=f"Delete inventory keys {delete_set}", ) - def write(self, update: Inventory, message: str, commit: bool = True) -> None: + def write( + self, update: InventorySnapshot, message: str, commit: bool = True + ) -> None: """ Write the inventory to the flake directory and commit it to git with the given message