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.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)

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.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

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