Merge pull request 'Classgen: refactor functions' (#1785) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-07-19 16:52:37 +00:00
4 changed files with 216 additions and 120 deletions

View File

@@ -10,30 +10,30 @@ from typing import Any
@dataclass @dataclass
class MachineDeploy: class MachineDeploy:
targetHost: str | None = field(default=None ) targetHost: None | str = field(default = None)
@dataclass @dataclass
class Machine: class Machine:
deploy: MachineDeploy deploy: MachineDeploy
name: str name: str
description: str | None = field(default=None ) description: None | str = field(default = None)
icon: str | None = field(default=None ) icon: None | str = field(default = None)
system: str | None = field(default=None ) system: None | str = field(default = None)
tags: list[str] = field(default_factory = list) tags: list[str] = field(default_factory = list)
@dataclass @dataclass
class Meta: class Meta:
name: str name: str
description: str | None = field(default=None ) description: None | str = field(default = None)
icon: str | None = field(default=None ) icon: None | str = field(default = None)
@dataclass @dataclass
class BorgbackupConfigDestination: class BorgbackupConfigDestination:
repo: str
name: str name: str
repo: str
@dataclass @dataclass
@@ -50,8 +50,8 @@ class ServiceBorgbackupMachine:
@dataclass @dataclass
class ServiceMeta: class ServiceMeta:
name: str name: str
description: str | None = field(default=None ) description: None | str = field(default = None)
icon: str | None = field(default=None ) icon: None | str = field(default = None)
@dataclass @dataclass
@@ -118,7 +118,7 @@ class ServicePackage:
@dataclass @dataclass
class SingleDiskConfig: class SingleDiskConfig:
device: str | None = field(default=None ) device: None | str = field(default = None)
@dataclass @dataclass

View File

@@ -1,6 +1,8 @@
# ruff: noqa: RUF001 # ruff: noqa: RUF001
import argparse import argparse
import json import json
from collections.abc import Callable
from functools import partial
from typing import Any from typing import Any
@@ -37,6 +39,142 @@ known_classes = set()
root_class = "Inventory" 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 # Recursive function to generate dataclasses from JSON schema
def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) -> str: def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) -> str:
properties = schema.get("properties", {}) 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}" assert field_types, f"Python type not found for {prop} {prop_info}"
serialised_types = " | ".join(field_types)
field_meta = None field_meta = None
if field_name != prop: if field_name != prop:
field_meta = f"""{{"original_name": "{prop}"}}""" field_meta = f"""{{"original_name": "{prop}"}}"""
field_def = f"{field_name}: {serialised_types}" finalize_field = partial(get_field_def, field_name, field_meta)
if field_meta:
field_def = f"{field_def} = field(metadata={field_meta})"
if "default" in prop_info or field_name not in prop_info.get("required", []): if "default" in prop_info or field_name not in prop_info.get("required", []):
if "default" in prop_info: if "default" in prop_info:
default_value = prop_info.get("default") default_value = prop_info.get("default")
if default_value is None: field_def = field_def_from_default_value(
field_types |= {"None"} default_value=default_value,
serialised_types = " | ".join(field_types) field_name=field_name,
field_types=field_types,
field_def = f"""{field_name}: {serialised_types} = field(default=None {f", metadata={field_meta}" if field_meta else ""})""" nested_class_name=nested_class_name,
elif isinstance(default_value, list): finalize_field=finalize_field,
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 field_def:
fields_with_default.append(field_def) fields_with_default.append(field_def)
if not field_def:
# Finalize without the default value
field_def = finalize_field(
field_types=field_types,
)
required_fields.append(field_def)
if "default" not in prop_info: if "default" not in prop_info:
# Field is not required and but also specifies no default value # Field is not required and but also specifies no default value
# Trying to infer default value from type # Trying to infer default value from type
if "dict" in str(serialised_types): field_def = field_def_from_default_type(
field_def = f"""{field_name}: {serialised_types} = field(default_factory=dict {f", metadata={field_meta}" if field_meta else ""})""" field_name=field_name,
fields_with_default.append(field_def) field_types=field_types,
elif "list" in str(serialised_types): class_name=class_name,
field_def = f"""{field_name}: {serialised_types} = field(default_factory=list {f", metadata={field_meta}" if field_meta else ""})""" finalize_field=finalize_field,
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 ""})""" if field_def:
fields_with_default.append(field_def) fields_with_default.append(field_def)
if not field_def:
elif class_name.endswith("Config"): field_def = finalize_field(
# SingleDiskConfig field_types=field_types,
# 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.
#################################################
"""
) )
else:
required_fields.append(field_def) required_fields.append(field_def)
else: else:
field_def = finalize_field(
field_types=field_types,
)
required_fields.append(field_def) required_fields.append(field_def)
fields_str = "\n ".join(required_fields + fields_with_default) fields_str = "\n ".join(required_fields + fields_with_default)

View File

@@ -9,10 +9,11 @@ export const BlockDevicesView: Component = () => {
refetch: loadDevices, refetch: loadDevices,
isFetching, isFetching,
} = createQuery(() => ({ } = createQuery(() => ({
queryKey: ["TanStack Query"], queryKey: ["block_devices"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("show_block_devices", {}); const result = await callApi("show_block_devices", {});
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,

View File

@@ -34,7 +34,7 @@ export const Flash = () => {
refetch: loadDevices, refetch: loadDevices,
isFetching, isFetching,
} = createQuery(() => ({ } = createQuery(() => ({
queryKey: ["TanStack Query"], queryKey: ["block_devices"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("show_block_devices", {}); const result = await callApi("show_block_devices", {});
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");