Merge pull request 'api/generators: remove term 'vars' interact purely with 'generators'' (#4242) from api-cleanup into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4242
This commit is contained in:
hsjobeki
2025-07-07 13:04:00 +00:00
12 changed files with 77 additions and 47 deletions

View File

@@ -90,7 +90,7 @@ const handleCancel = async <K extends OperationNames>(
orig_task: Promise<BackendReturnType<K>>, orig_task: Promise<BackendReturnType<K>>,
) => { ) => {
console.log("Canceling operation: ", ops_key); console.log("Canceling operation: ", ops_key);
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); const { promise, op_key } = _callApi("delete_task", { task_id: ops_key });
promise.catch((error) => { promise.catch((error) => {
toast.custom( toast.custom(
(t) => ( (t) => (

View File

@@ -71,7 +71,7 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
const hwReportQuery = useQuery(() => ({ const hwReportQuery = useQuery(() => ({
queryKey: [props.dir, props.machine_id, "hw_report"], queryKey: [props.dir, props.machine_id, "hw_report"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("describe_machine_hardware", { const result = await callApi("get_machine_hardware_summary", {
machine: { machine: {
flake: { flake: {
identifier: props.dir, identifier: props.dir,
@@ -127,7 +127,7 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
return; return;
} }
const r = await callApi("generate_machine_hardware_info", { const r = await callApi("run_machine_hardware_info", {
opts: { opts: {
machine: { machine: {
name: props.machine_id, name: props.machine_id,

View File

@@ -173,7 +173,7 @@ export const VarsStep = (props: VarsStepProps) => {
toast.error("Error fetching data"); toast.error("Error fetching data");
return; return;
} }
const result = await callApi("generate_vars_for_machine", { const result = await callApi("run_generators", {
machine_name: props.machine_id, machine_name: props.machine_id,
base_dir: props.dir, base_dir: props.dir,
generators: generatorsQuery.data.map((generator) => generator.name), generators: generatorsQuery.data.map((generator) => generator.name),

View File

@@ -90,7 +90,7 @@ const handleCancel = async <K extends OperationNames>(
orig_task: Promise<BackendReturnType<K>>, orig_task: Promise<BackendReturnType<K>>,
) => { ) => {
console.log("Canceling operation: ", ops_key); console.log("Canceling operation: ", ops_key);
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); const { promise, op_key } = _callApi("delete_task", { task_id: ops_key });
promise.catch((error) => { promise.catch((error) => {
toast.custom( toast.custom(
(t) => ( (t) => (

View File

@@ -5,7 +5,7 @@ from pathlib import Path
from clan_lib.machines.hardware import ( from clan_lib.machines.hardware import (
HardwareConfig, HardwareConfig,
HardwareGenerateOptions, HardwareGenerateOptions,
generate_machine_hardware_info, run_machine_hardware_info,
) )
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.machines.suggestions import validate_machine_names from clan_lib.machines.suggestions import validate_machine_names
@@ -38,7 +38,7 @@ def update_hardware_config_command(args: argparse.Namespace) -> None:
host_key_check=args.host_key_check, private_key=args.identity_file host_key_check=args.host_key_check, private_key=args.identity_file
) )
generate_machine_hardware_info(opts, target_host) run_machine_hardware_info(opts, target_host)
def register_update_hardware_config(parser: argparse.ArgumentParser) -> None: def register_update_hardware_config(parser: argparse.ArgumentParser) -> None:

View File

@@ -10,9 +10,9 @@ from clan_cli.tests.helpers import cli
from clan_cli.vars.check import check_vars from clan_cli.vars.check import check_vars
from clan_cli.vars.generate import ( from clan_cli.vars.generate import (
Generator, Generator,
create_machine_vars, run_generators,
create_machine_vars_interactive, create_machine_vars_interactive,
get_machine_generators, get_generators,
) )
from clan_cli.vars.get import get_machine_var from clan_cli.vars.get import get_machine_var
from clan_cli.vars.graph import all_missing_closure, requested_closure from clan_cli.vars.graph import all_missing_closure, requested_closure
@@ -654,7 +654,7 @@ def test_api_set_prompts(
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
create_machine_vars( run_generators(
machine_name="my_machine", machine_name="my_machine",
base_dir=flake.path, base_dir=flake.path,
generators=["my_generator"], generators=["my_generator"],
@@ -668,7 +668,7 @@ def test_api_set_prompts(
store = in_repo.FactStore(machine.name, machine.flake) store = in_repo.FactStore(machine.name, machine.flake)
assert store.exists(Generator("my_generator"), "prompt1") assert store.exists(Generator("my_generator"), "prompt1")
assert store.get(Generator("my_generator"), "prompt1").decode() == "input1" assert store.get(Generator("my_generator"), "prompt1").decode() == "input1"
create_machine_vars( run_generators(
machine_name="my_machine", machine_name="my_machine",
base_dir=flake.path, base_dir=flake.path,
generators=["my_generator"], generators=["my_generator"],
@@ -680,7 +680,7 @@ def test_api_set_prompts(
) )
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2" assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
generators = get_machine_generators( generators = get_generators(
machine_name="my_machine", machine_name="my_machine",
base_dir=flake.path, base_dir=flake.path,
full_closure=True, full_closure=True,

View File

@@ -423,7 +423,7 @@ def get_closure(
@API.register @API.register
def get_machine_generators( def get_generators(
machine_name: str, machine_name: str,
base_dir: Path, base_dir: Path,
full_closure: bool = False, full_closure: bool = False,
@@ -461,7 +461,7 @@ def _generate_vars_for_machine(
@API.register @API.register
def create_machine_vars( def run_generators(
machine_name: str, machine_name: str,
generators: list[str], generators: list[str],
all_prompt_values: dict[str, dict[str, str]], all_prompt_values: dict[str, dict[str, str]],

View File

@@ -254,6 +254,7 @@ API.register(open_file)
"type": "object", "type": "object",
"required": ["arguments", "return"], "required": ["arguments", "return"],
"additionalProperties": False, "additionalProperties": False,
"description": func.__doc__,
"properties": { "properties": {
"return": return_type, "return": return_type,
"arguments": { "arguments": {

View File

@@ -17,7 +17,7 @@ BAKEND_THREADS: dict[str, WebThread] | None = None
@API.register_abstract @API.register_abstract
def cancel_task(task_id: str) -> None: def delete_task(task_id: str) -> None:
"""Cancel a task by its op_key.""" """Cancel a task by its op_key."""
assert BAKEND_THREADS is not None, "Backend threads not initialized" assert BAKEND_THREADS is not None, "Backend threads not initialized"
future = BAKEND_THREADS.get(task_id) future = BAKEND_THREADS.get(task_id)

View File

@@ -67,7 +67,7 @@ class HardwareGenerateOptions:
@API.register @API.register
def generate_machine_hardware_info( def run_machine_hardware_info(
opts: HardwareGenerateOptions, target_host: Remote opts: HardwareGenerateOptions, target_host: Remote
) -> HardwareConfig: ) -> HardwareConfig:
""" """
@@ -157,7 +157,7 @@ class MachineHardwareBrief(TypedDict):
@API.register @API.register
def describe_machine_hardware(machine: Machine) -> MachineHardwareBrief: def get_machine_hardware_summary(machine: Machine) -> MachineHardwareBrief:
""" """
Return a high-level summary of hardware config and platform type. Return a high-level summary of hardware config and platform type.
""" """

View File

@@ -14,7 +14,7 @@ from clan_cli.machines.create import create_machine
from clan_cli.secrets.key import generate_key from clan_cli.secrets.key import generate_key
from clan_cli.secrets.sops import maybe_get_admin_public_keys from clan_cli.secrets.sops import maybe_get_admin_public_keys
from clan_cli.secrets.users import add_user from clan_cli.secrets.users import add_user
from clan_cli.vars.generate import create_machine_vars, get_machine_generators from clan_cli.vars.generate import run_generators, get_generators
from clan_lib.api.disk import hw_main_disk_options, set_machine_disk_schema from clan_lib.api.disk import hw_main_disk_options, set_machine_disk_schema
from clan_lib.api.modules import list_modules from clan_lib.api.modules import list_modules
@@ -222,7 +222,7 @@ def test_clan_create_api(
# Invalidate cache because of new inventory # Invalidate cache because of new inventory
clan_dir_flake.invalidate_cache() clan_dir_flake.invalidate_cache()
generators = get_machine_generators(machine.name, machine.flake.path) generators = get_generators(machine.name, machine.flake.path)
all_prompt_values = {} all_prompt_values = {}
for generator in generators: for generator in generators:
prompt_values = {} prompt_values = {}
@@ -235,7 +235,7 @@ def test_clan_create_api(
raise ClanError(msg) raise ClanError(msg)
all_prompt_values[generator.name] = prompt_values all_prompt_values[generator.name] = prompt_values
create_machine_vars( run_generators(
machine_name=machine.name, machine_name=machine.name,
base_dir=machine.flake.path, base_dir=machine.flake.path,
generators=[gen.name for gen in generators], generators=[gen.name for gen in generators],

View File

@@ -5,23 +5,27 @@ from pathlib import Path
# !!! IMPORTANT !!! # !!! IMPORTANT !!!
# AVOID VERBS NOT IN THIS LIST # AVOID VERBS NOT IN THIS LIST
# We might restrict this even further to build a consistent and easy to use API # IF YOU WANT TO ADD TO THIS LIST CREATE AN ISSUE/DISCUSS FIRST
#
# Verbs are restricted to make API usage intuitive and consistent.
#
# Discouraged verbs:
# do Too vague
# process Sounds generic; lacks clarity.
# generate Ambiguous: does it mutate state or not? Prefer 'run'
# handle Abstract and fuzzy
# show overlaps with get or list
# describe overlap with get or list
# can, is often used for helpers, use check instead for structure responses
COMMON_VERBS = { COMMON_VERBS = {
"get", "get", # fetch single item
"list", "list", # fetch collection
"show", "create", # instantiate resource
"set", "set", # update or configure
"create", "delete", # remove resource
"update", "open", # initiate session, shell, file, etc.
"delete", "check", # validate, probe, or assert
"generate", "run", # start imperative task or action; machine-deploy etc.
"maybe",
"open",
"flash",
"install",
"deploy",
"check",
"cancel",
} }
@@ -39,7 +43,8 @@ def singular(word: str) -> str:
return word return word
def normalize_tag(parts: list[str]) -> list[str]: def normalize_op_name(op_name: str) -> list[str]:
parts = op_name.lower().split("_")
# parts contains [ VERB NOUN NOUN ... ] # parts contains [ VERB NOUN NOUN ... ]
# Where each NOUN is a SUB-RESOURCE # Where each NOUN is a SUB-RESOURCE
verb = parts[0] verb = parts[0]
@@ -53,20 +58,21 @@ def normalize_tag(parts: list[str]) -> list[str]:
return [verb, *nouns] return [verb, *nouns]
def operation_to_tag(op_name: str) -> str: def check_operation_name(op_name: str, normalized: list[str]) -> list[str]:
def check_operation_name(verb: str, _resource_nouns: list[str]) -> None: verb = normalized[0]
_nouns = normalized[1:]
warnings = []
if not is_verb(verb): if not is_verb(verb):
print( warnings.append(
f"""⚠️ WARNING: Verb '{op_name}' of API operation {op_name} is not allowed. f"""Verb '{verb}' of API operation {op_name} is not allowed.
Use one of: {", ".join(COMMON_VERBS)} Use one of: {", ".join(COMMON_VERBS)}
""" """
) )
return warnings
parts = op_name.lower().split("_")
normalized = normalize_tag(parts)
check_operation_name(normalized[0], normalized[1:])
def operation_to_tag(op_name: str) -> str:
normalized = normalize_op_name(op_name)
return " / ".join(normalized[1:]) return " / ".join(normalized[1:])
@@ -134,6 +140,28 @@ def main() -> None:
"components": {"schemas": {}}, "components": {"schemas": {}},
} }
# === Check all functions ===
warnings: list[str] = []
errors: list[str] = []
for func_name, func_schema in functions.items():
normalized = normalize_op_name(func_name)
check_res = check_operation_name(func_name, normalized)
if check_res:
errors.extend(check_res)
if not func_schema.get("description"):
warnings.append(
f"{func_name} doesn't have a description. Python docstring is required for an API function."
)
if warnings:
for message in warnings:
print(f"⚠️ Warn: {message}")
if errors:
for m in errors:
print(f"❌ Error: {m}")
os.abort()
# === Convert each function === # === Convert each function ===
for func_name, func_schema in functions.items(): for func_name, func_schema in functions.items():
args_schema = fix_nullables(deepcopy(func_schema["properties"]["arguments"])) args_schema = fix_nullables(deepcopy(func_schema["properties"]["arguments"]))
@@ -150,6 +178,7 @@ def main() -> None:
"post": { "post": {
"summary": func_name, "summary": func_name,
"operationId": func_name, "operationId": func_name,
"description": func_schema.get("description"),
"tags": [tag], "tags": [tag],
"requestBody": { "requestBody": {
"required": True, "required": True,