feat(InventoryStore): return a restricted view of the inventory

This commit is contained in:
Johannes Kirschbauer
2025-05-27 17:08:24 +02:00
parent bac3e6c2b2
commit 104343a334
4 changed files with 148 additions and 17 deletions

View File

@@ -2,9 +2,8 @@ from dataclasses import dataclass
from clan_lib.api import API from clan_lib.api import API
from clan_lib.flake import Flake 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.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 from clan_lib.persist.util import set_value_by_path
@@ -15,7 +14,7 @@ class UpdateOptions:
@API.register @API.register
def update_clan_meta(options: UpdateOptions) -> Inventory: def update_clan_meta(options: UpdateOptions) -> InventorySnapshot:
inventory_store = InventoryStore(options.flake) inventory_store = InventoryStore(options.flake)
inventory = inventory_store.read() inventory = inventory_store.read()
set_value_by_path(inventory, "meta", options.meta) set_value_by_path(inventory, "meta", options.meta)

View File

@@ -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.api import API
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.nix_models.clan import Inventory from clan_lib.persist.inventory_store import InventorySnapshot, InventoryStore
from clan_lib.persist.inventory_store import InventoryStore
@API.register @API.register
def get_inventory(flake: Flake) -> Inventory: def get_inventory(flake: Flake) -> InventorySnapshot:
inventory_store = InventoryStore(flake) inventory_store = InventoryStore(flake)
inventory = inventory_store.read() inventory = inventory_store.read()
return inventory return inventory

View File

@@ -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]

View File

@@ -1,11 +1,17 @@
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path 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.errors import ClanError
from clan_lib.git import commit_file 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 ( from .util import (
calc_patches, calc_patches,
@@ -75,8 +81,8 @@ def sanitize(data: Any, whitelist_paths: list[str], current_path: list[str]) ->
@dataclass @dataclass
class WriteInfo: class WriteInfo:
writeables: dict[str, set[str]] writeables: dict[str, set[str]]
data_eval: Inventory data_eval: "InventorySnapshot"
data_disk: Inventory data_disk: "InventorySnapshot"
class FlakeInterface(Protocol): class FlakeInterface(Protocol):
@@ -92,6 +98,19 @@ class FlakeInterface(Protocol):
def path(self) -> Path: ... 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: class InventoryStore:
def __init__( def __init__(
self, self,
@@ -117,10 +136,11 @@ class InventoryStore:
self._allowed_path_transforms = _allowed_path_transforms self._allowed_path_transforms = _allowed_path_transforms
if _keys is None: if _keys is None:
_keys = ["machines", "instances", "meta", "services"] _keys = list(InventorySnapshot.__annotations__.keys())
self._keys = _keys self._keys = _keys
def _load_merged_inventory(self) -> Inventory: def _load_merged_inventory(self) -> InventorySnapshot:
""" """
Loads the evaluated inventory. Loads the evaluated inventory.
After all merge operations with eventual nix code in buildClan. After all merge operations with eventual nix code in buildClan.
@@ -140,7 +160,7 @@ class InventoryStore:
return sanitized return sanitized
def _get_persisted(self) -> Inventory: def _get_persisted(self) -> InventorySnapshot:
""" """
Load the inventory FILE from the flake directory Load the inventory FILE from the flake directory
If no file is found, returns an empty dictionary If no file is found, returns an empty dictionary
@@ -189,8 +209,8 @@ class InventoryStore:
""" """
current_priority = self._get_inventory_current_priority() current_priority = self._get_inventory_current_priority()
data_eval: Inventory = self._load_merged_inventory() data_eval: InventorySnapshot = self._load_merged_inventory()
data_disk: Inventory = self._get_persisted() data_disk: InventorySnapshot = self._get_persisted()
writeables = determine_writeability( writeables = determine_writeability(
current_priority, dict(data_eval), dict(data_disk) current_priority, dict(data_eval), dict(data_disk)
@@ -198,7 +218,7 @@ class InventoryStore:
return WriteInfo(writeables, data_eval, data_disk) return WriteInfo(writeables, data_eval, data_disk)
def read(self) -> Inventory: def read(self) -> InventorySnapshot:
""" """
Accessor to the merged inventory Accessor to the merged inventory
@@ -226,7 +246,9 @@ class InventoryStore:
commit_message=f"Delete inventory keys {delete_set}", 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 Write the inventory to the flake directory
and commit it to git with the given message and commit it to git with the given message