diff --git a/pkgs/clan-cli/clan_cli/inventory/classes.py b/pkgs/clan-cli/clan_cli/inventory/classes.py index 7aa567294..7712932a3 100644 --- a/pkgs/clan-cli/clan_cli/inventory/classes.py +++ b/pkgs/clan-cli/clan_cli/inventory/classes.py @@ -10,64 +10,64 @@ from typing import Any @dataclass class MachineDeploy: - targetHost: str | None = field(default=None ) + targetHost: None | str = field(default = None) @dataclass class Machine: deploy: MachineDeploy name: str - description: str | None = field(default=None ) - icon: str | None = field(default=None ) - system: str | None = field(default=None ) - tags: list[str] = field(default_factory=list ) + description: None | str = field(default = None) + icon: None | str = field(default = None) + system: None | str = field(default = None) + tags: list[str] = field(default_factory = list) @dataclass class Meta: name: str - description: str | None = field(default=None ) - icon: str | None = field(default=None ) + description: None | str = field(default = None) + icon: None | str = field(default = None) @dataclass class BorgbackupConfigDestination: - repo: str name: str + repo: str @dataclass class BorgbackupConfig: - destinations: dict[str, BorgbackupConfigDestination] = field(default_factory=dict ) + destinations: dict[str, BorgbackupConfigDestination] = field(default_factory = dict) @dataclass class ServiceBorgbackupMachine: - config: BorgbackupConfig = field(default_factory=BorgbackupConfig ) - imports: list[str] = field(default_factory=list ) + config: BorgbackupConfig = field(default_factory = BorgbackupConfig) + imports: list[str] = field(default_factory = list) @dataclass class ServiceMeta: name: str - description: str | None = field(default=None ) - icon: str | None = field(default=None ) + description: None | str = field(default = None) + icon: None | str = field(default = None) @dataclass class ServiceBorgbackupRoleClient: - config: BorgbackupConfig = field(default_factory=BorgbackupConfig ) - imports: list[str] = field(default_factory=list ) - machines: list[str] = field(default_factory=list ) - tags: list[str] = field(default_factory=list ) + config: BorgbackupConfig = field(default_factory = BorgbackupConfig) + imports: list[str] = field(default_factory = list) + machines: list[str] = field(default_factory = list) + tags: list[str] = field(default_factory = list) @dataclass class ServiceBorgbackupRoleServer: - config: BorgbackupConfig = field(default_factory=BorgbackupConfig ) - imports: list[str] = field(default_factory=list ) - machines: list[str] = field(default_factory=list ) - tags: list[str] = field(default_factory=list ) + config: BorgbackupConfig = field(default_factory = BorgbackupConfig) + imports: list[str] = field(default_factory = list) + machines: list[str] = field(default_factory = list) + tags: list[str] = field(default_factory = list) @dataclass @@ -80,27 +80,27 @@ class ServiceBorgbackupRole: class ServiceBorgbackup: meta: ServiceMeta roles: ServiceBorgbackupRole - config: BorgbackupConfig = field(default_factory=BorgbackupConfig ) - machines: dict[str, ServiceBorgbackupMachine] = field(default_factory=dict ) + config: BorgbackupConfig = field(default_factory = BorgbackupConfig) + machines: dict[str, ServiceBorgbackupMachine] = field(default_factory = dict) @dataclass class PackagesConfig: - packages: list[str] = field(default_factory=list ) + packages: list[str] = field(default_factory = list) @dataclass class ServicePackageMachine: - config: PackagesConfig = field(default_factory=PackagesConfig ) - imports: list[str] = field(default_factory=list ) + config: PackagesConfig = field(default_factory = PackagesConfig) + imports: list[str] = field(default_factory = list) @dataclass class ServicePackageRoleDefault: - config: PackagesConfig = field(default_factory=PackagesConfig ) - imports: list[str] = field(default_factory=list ) - machines: list[str] = field(default_factory=list ) - tags: list[str] = field(default_factory=list ) + config: PackagesConfig = field(default_factory = PackagesConfig) + imports: list[str] = field(default_factory = list) + machines: list[str] = field(default_factory = list) + tags: list[str] = field(default_factory = list) @dataclass @@ -112,27 +112,27 @@ class ServicePackageRole: class ServicePackage: meta: ServiceMeta roles: ServicePackageRole - config: PackagesConfig = field(default_factory=PackagesConfig ) - machines: dict[str, ServicePackageMachine] = field(default_factory=dict ) + config: PackagesConfig = field(default_factory = PackagesConfig) + machines: dict[str, ServicePackageMachine] = field(default_factory = dict) @dataclass class SingleDiskConfig: - device: str | None = field(default=None ) + device: None | str = field(default = None) @dataclass class ServiceSingleDiskMachine: - config: SingleDiskConfig = field(default_factory=SingleDiskConfig ) - imports: list[str] = field(default_factory=list ) + config: SingleDiskConfig = field(default_factory = SingleDiskConfig) + imports: list[str] = field(default_factory = list) @dataclass class ServiceSingleDiskRoleDefault: - config: SingleDiskConfig = field(default_factory=SingleDiskConfig ) - imports: list[str] = field(default_factory=list ) - machines: list[str] = field(default_factory=list ) - tags: list[str] = field(default_factory=list ) + config: SingleDiskConfig = field(default_factory = SingleDiskConfig) + imports: list[str] = field(default_factory = list) + machines: list[str] = field(default_factory = list) + tags: list[str] = field(default_factory = list) @dataclass @@ -144,19 +144,19 @@ class ServiceSingleDiskRole: class ServiceSingleDisk: meta: ServiceMeta roles: ServiceSingleDiskRole - config: SingleDiskConfig = field(default_factory=SingleDiskConfig ) - machines: dict[str, ServiceSingleDiskMachine] = field(default_factory=dict ) + config: SingleDiskConfig = field(default_factory = SingleDiskConfig) + machines: dict[str, ServiceSingleDiskMachine] = field(default_factory = dict) @dataclass class Service: - borgbackup: dict[str, ServiceBorgbackup] = field(default_factory=dict ) - packages: dict[str, ServicePackage] = field(default_factory=dict ) - single_disk: dict[str, ServiceSingleDisk] = field(default_factory=dict , metadata={"original_name": "single-disk"}) + borgbackup: dict[str, ServiceBorgbackup] = field(default_factory = dict) + packages: dict[str, ServicePackage] = field(default_factory = dict) + single_disk: dict[str, ServiceSingleDisk] = field(default_factory = dict, metadata = {"original_name": "single-disk"}) @dataclass class Inventory: meta: Meta services: Service - machines: dict[str, Machine] = field(default_factory=dict ) + machines: dict[str, Machine] = field(default_factory = dict) diff --git a/pkgs/classgen/main.py b/pkgs/classgen/main.py index ea3c2b17b..a4537c276 100644 --- a/pkgs/classgen/main.py +++ b/pkgs/classgen/main.py @@ -1,6 +1,8 @@ # ruff: noqa: RUF001 import argparse import json +from collections.abc import Callable +from functools import partial from typing import Any @@ -37,6 +39,142 @@ known_classes = set() root_class = "Inventory" +def field_def_from_default_type( + field_name: str, + field_types: set[str], + class_name: str, + finalize_field: Callable[..., str], +) -> str | None: + if "dict" in str(field_types): + return finalize_field( + field_types=field_types, + default_factory="dict", + ) + + if "list" in str(field_types): + return finalize_field( + field_types=field_types, + default_factory="list", + ) + if "None" in str(field_types): + return finalize_field( + field_types=field_types, + default="None", + ) + + if class_name.endswith("Config"): + # SingleDiskConfig + # PackagesConfig + # ... + # Config classes MUST always be optional + raise ValueError( + f""" + ################################################# + Clan module '{class_name}' specifies a top-level option '{field_name}' without a default value. + + To fix this: + - Add a default value to the option + + lib.mkOption {{ + type = lib.types.nullOr lib.types.str; + default = null; # <- Add a default value here + }}; + + # Other options + + - make the field nullable + + lib.mkOption {{ + # ╔══════════════╗ <- Nullable type + type = lib.types.nullOr lib.types.str; + }}; + + - Use lib.types.attrsOf if suitable + - Use lib.types.listOf if suitable + + + Or report this problem to the clan team. So the class generator can be improved. + ################################################# + """ + ) + + return None + + +def field_def_from_default_value( + default_value: Any, + field_name: str, + field_types: set[str], + nested_class_name: str, + finalize_field: Callable[..., str], +) -> str | None: + # default_value = prop_info.get("default") + if default_value is None: + return finalize_field( + field_types=field_types | {"None"}, + default="None", + ) + elif isinstance(default_value, list): + return finalize_field( + field_types=field_types, + default_factory="list", + ) + elif isinstance(default_value, dict): + serialised_types = " | ".join(field_types) + if serialised_types == nested_class_name: + return finalize_field( + field_types=field_types, + default_factory=nested_class_name, + ) + + elif f"dict[str, {nested_class_name}]" in serialised_types: + return finalize_field( + field_types=field_types, + default_factory="dict", + ) + else: + return finalize_field( + field_types=field_types, + default_factory="dict", + type_apendix=" | dict[str,Any]", + ) + elif default_value == "‹name›": + return None + elif isinstance(default_value, str): + return finalize_field( + field_types=field_types, + default=f"'{default_value}'", + ) + else: + # Other default values unhandled yet. + raise ValueError( + f"Unhandled default value for field '{field_name}' - default value: {default_value}" + ) + + +def get_field_def( + field_name: str, + field_meta: str | None, + field_types: set[str], + default: str | None = None, + default_factory: str | None = None, + type_apendix: str = "", +) -> str: + sorted_field_types = sorted(field_types) + serialised_types = " | ".join(sorted_field_types) + type_apendix + if not default and not default_factory and not field_meta: + return f"{field_name}: {serialised_types}" + field_init = "field(" + if default: + field_init += f"default = {default}" + if default_factory: + field_init += f"default_factory = {default_factory}" + if field_meta: + field_init += f", metadata = {field_meta}" + + return f"{field_name}: {serialised_types} = {field_init})" + + # Recursive function to generate dataclasses from JSON schema def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) -> str: properties = schema.get("properties", {}) @@ -105,97 +243,54 @@ def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) -> assert field_types, f"Python type not found for {prop} {prop_info}" - serialised_types = " | ".join(field_types) field_meta = None if field_name != prop: field_meta = f"""{{"original_name": "{prop}"}}""" - field_def = f"{field_name}: {serialised_types}" - if field_meta: - field_def = f"{field_def} = field(metadata={field_meta})" + finalize_field = partial(get_field_def, field_name, field_meta) if "default" in prop_info or field_name not in prop_info.get("required", []): if "default" in prop_info: default_value = prop_info.get("default") - if default_value is None: - field_types |= {"None"} - serialised_types = " | ".join(field_types) + field_def = field_def_from_default_value( + default_value=default_value, + field_name=field_name, + field_types=field_types, + nested_class_name=nested_class_name, + finalize_field=finalize_field, + ) + if field_def: + fields_with_default.append(field_def) - field_def = f"""{field_name}: {serialised_types} = field(default=None {f", metadata={field_meta}" if field_meta else ""})""" - elif isinstance(default_value, list): - field_def = f"""{field_def} = field(default_factory=list {f", metadata={field_meta}" if field_meta else ""})""" - elif isinstance(default_value, dict): - serialised_types = " | ".join(field_types) - if serialised_types == nested_class_name: - field_def = f"""{field_name}: {serialised_types} = field(default_factory={nested_class_name} {f", metadata={field_meta}" if field_meta else ""})""" - elif f"dict[str, {nested_class_name}]" in serialised_types: - field_def = f"""{field_name}: {serialised_types} = field(default_factory=dict {f", metadata={field_meta}" if field_meta else ""})""" - else: - field_def = f"""{field_name}: {serialised_types} | dict[str,Any] = field(default_factory=dict {f", metadata={field_meta}" if field_meta else ""})""" - elif default_value == "‹name›": - # Special case for nix submodules - pass - elif isinstance(default_value, str): - field_def = f"""{field_name}: {serialised_types} = field(default = '{default_value}' {f", metadata={field_meta}" if field_meta else ""})""" - else: - # Other default values unhandled yet. - raise ValueError( - f"Unhandled default value for field '{field_name}' - default value: {default_value}" + if not field_def: + # Finalize without the default value + field_def = finalize_field( + field_types=field_types, ) - - fields_with_default.append(field_def) + required_fields.append(field_def) if "default" not in prop_info: # Field is not required and but also specifies no default value # Trying to infer default value from type - if "dict" in str(serialised_types): - field_def = f"""{field_name}: {serialised_types} = field(default_factory=dict {f", metadata={field_meta}" if field_meta else ""})""" + field_def = field_def_from_default_type( + field_name=field_name, + field_types=field_types, + class_name=class_name, + finalize_field=finalize_field, + ) + + if field_def: fields_with_default.append(field_def) - elif "list" in str(serialised_types): - field_def = f"""{field_name}: {serialised_types} = field(default_factory=list {f", metadata={field_meta}" if field_meta else ""})""" - fields_with_default.append(field_def) - elif "None" in str(serialised_types): - field_def = f"""{field_name}: {serialised_types} = field(default=None {f", metadata={field_meta}" if field_meta else ""})""" - fields_with_default.append(field_def) - - elif class_name.endswith("Config"): - # SingleDiskConfig - # PackagesConfig - # ... - # Config classes MUST always be optional - raise ValueError( - f""" - ################################################# - Clan module '{class_name}' specifies a top-level option '{field_name}' without a default value. - - To fix this: - - Add a default value to the option - - lib.mkOption {{ - type = lib.types.nullOr lib.types.str; - default = null; # <- Add a default value here - }}; - - # Other options - - - make the field nullable - - lib.mkOption {{ - # ╔══════════════╗ <- Nullable type - type = lib.types.nullOr lib.types.str; - }}; - - - Use lib.types.attrsOf if suitable - - Use lib.types.listOf if suitable - - - Or report this problem to the clan team. So the class generator can be improved. - ################################################# - """ + if not field_def: + field_def = finalize_field( + field_types=field_types, ) - else: required_fields.append(field_def) + else: + field_def = finalize_field( + field_types=field_types, + ) required_fields.append(field_def) fields_str = "\n ".join(required_fields + fields_with_default) diff --git a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx index 3ff0f7037..31b035f32 100644 --- a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx +++ b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx @@ -9,10 +9,11 @@ export const BlockDevicesView: Component = () => { refetch: loadDevices, isFetching, } = createQuery(() => ({ - queryKey: ["TanStack Query"], + queryKey: ["block_devices"], queryFn: async () => { const result = await callApi("show_block_devices", {}); if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; }, staleTime: 1000 * 60 * 5, diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 1a386a509..31a2275bc 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -34,7 +34,7 @@ export const Flash = () => { refetch: loadDevices, isFetching, } = createQuery(() => ({ - queryKey: ["TanStack Query"], + queryKey: ["block_devices"], queryFn: async () => { const result = await callApi("show_block_devices", {}); if (result.status === "error") throw new Error("Failed to fetch data");