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>>,
) => {
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) => {
toast.custom(
(t) => (

View File

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

View File

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

View File

@@ -90,7 +90,7 @@ const handleCancel = async <K extends OperationNames>(
orig_task: Promise<BackendReturnType<K>>,
) => {
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) => {
toast.custom(
(t) => (

View File

@@ -5,7 +5,7 @@ from pathlib import Path
from clan_lib.machines.hardware import (
HardwareConfig,
HardwareGenerateOptions,
generate_machine_hardware_info,
run_machine_hardware_info,
)
from clan_lib.machines.machines import Machine
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
)
generate_machine_hardware_info(opts, target_host)
run_machine_hardware_info(opts, target_host)
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.generate import (
Generator,
create_machine_vars,
run_generators,
create_machine_vars_interactive,
get_machine_generators,
get_generators,
)
from clan_cli.vars.get import get_machine_var
from clan_cli.vars.graph import all_missing_closure, requested_closure
@@ -654,7 +654,7 @@ def test_api_set_prompts(
monkeypatch.chdir(flake.path)
create_machine_vars(
run_generators(
machine_name="my_machine",
base_dir=flake.path,
generators=["my_generator"],
@@ -668,7 +668,7 @@ def test_api_set_prompts(
store = in_repo.FactStore(machine.name, machine.flake)
assert store.exists(Generator("my_generator"), "prompt1")
assert store.get(Generator("my_generator"), "prompt1").decode() == "input1"
create_machine_vars(
run_generators(
machine_name="my_machine",
base_dir=flake.path,
generators=["my_generator"],
@@ -680,7 +680,7 @@ def test_api_set_prompts(
)
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
generators = get_machine_generators(
generators = get_generators(
machine_name="my_machine",
base_dir=flake.path,
full_closure=True,

View File

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

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ class HardwareGenerateOptions:
@API.register
def generate_machine_hardware_info(
def run_machine_hardware_info(
opts: HardwareGenerateOptions, target_host: Remote
) -> HardwareConfig:
"""
@@ -157,7 +157,7 @@ class MachineHardwareBrief(TypedDict):
@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.
"""

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.sops import maybe_get_admin_public_keys
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.modules import list_modules
@@ -222,7 +222,7 @@ def test_clan_create_api(
# Invalidate cache because of new inventory
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 = {}
for generator in generators:
prompt_values = {}
@@ -235,7 +235,7 @@ def test_clan_create_api(
raise ClanError(msg)
all_prompt_values[generator.name] = prompt_values
create_machine_vars(
run_generators(
machine_name=machine.name,
base_dir=machine.flake.path,
generators=[gen.name for gen in generators],

View File

@@ -5,23 +5,27 @@ from pathlib import Path
# !!! IMPORTANT !!!
# 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 = {
"get",
"list",
"show",
"set",
"create",
"update",
"delete",
"generate",
"maybe",
"open",
"flash",
"install",
"deploy",
"check",
"cancel",
"get", # fetch single item
"list", # fetch collection
"create", # instantiate resource
"set", # update or configure
"delete", # remove resource
"open", # initiate session, shell, file, etc.
"check", # validate, probe, or assert
"run", # start imperative task or action; machine-deploy etc.
}
@@ -39,7 +43,8 @@ def singular(word: str) -> str:
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 ... ]
# Where each NOUN is a SUB-RESOURCE
verb = parts[0]
@@ -53,20 +58,21 @@ def normalize_tag(parts: list[str]) -> list[str]:
return [verb, *nouns]
def operation_to_tag(op_name: str) -> str:
def check_operation_name(verb: str, _resource_nouns: list[str]) -> None:
def check_operation_name(op_name: str, normalized: list[str]) -> list[str]:
verb = normalized[0]
_nouns = normalized[1:]
warnings = []
if not is_verb(verb):
print(
f"""⚠️ WARNING: Verb '{op_name}' of API operation {op_name} is not allowed.
warnings.append(
f"""Verb '{verb}' of API operation {op_name} is not allowed.
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:])
@@ -134,6 +140,28 @@ def main() -> None:
"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 ===
for func_name, func_schema in functions.items():
args_schema = fix_nullables(deepcopy(func_schema["properties"]["arguments"]))
@@ -150,6 +178,7 @@ def main() -> None:
"post": {
"summary": func_name,
"operationId": func_name,
"description": func_schema.get("description"),
"tags": [tag],
"requestBody": {
"required": True,