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:
@@ -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) => (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]],
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user