API: handle functions with multiple arguments
All checks were successful
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-iso-installer Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.package-wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-vm-manager Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-vm-manager Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
checks / checks-impure (pull_request) Successful in 2m34s
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-eval Build done.

This commit is contained in:
Johannes Kirschbauer 2024-05-26 18:04:49 +02:00
parent ed171f0264
commit ab656d5655
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
9 changed files with 85 additions and 24 deletions

View File

@ -1,10 +1,12 @@
import json
from clan_cli.api import API from clan_cli.api import API
def main() -> None: def main() -> None:
schema = API.to_json_schema() schema = API.to_json_schema()
print( print(
f"""export const schema = {schema} as const; f"""export const schema = {json.dumps(schema, indent=2)} as const;
""" """
) )

View File

@ -24,15 +24,14 @@ class ApiResponse(Generic[ResponseDataType]):
class _MethodRegistry: class _MethodRegistry:
def __init__(self) -> None: def __init__(self) -> None:
self._registry: dict[str, Callable] = {} self._registry: dict[str, Callable[[Any], Any]] = {}
def register(self, fn: Callable[..., T]) -> Callable[..., T]: def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._registry[fn.__name__] = fn self._registry[fn.__name__] = fn
return fn return fn
def to_json_schema(self) -> str: def to_json_schema(self) -> dict[str, Any]:
# Import only when needed # Import only when needed
import json
from typing import get_type_hints from typing import get_type_hints
from clan_cli.api.util import type_to_dict from clan_cli.api.util import type_to_dict
@ -41,25 +40,51 @@ class _MethodRegistry:
"$comment": "An object containing API methods. ", "$comment": "An object containing API methods. ",
"type": "object", "type": "object",
"additionalProperties": False, "additionalProperties": False,
"required": ["list_machines"], "required": [func_name for func_name in self._registry.keys()],
"properties": {}, "properties": {},
} }
for name, func in self._registry.items(): for name, func in self._registry.items():
hints = get_type_hints(func) hints = get_type_hints(func)
serialized_hints = { serialized_hints = {
"argument" if key != "return" else "return": type_to_dict( key: type_to_dict(
value, scope=name + " argument" if key != "return" else "return" value, scope=name + " argument" if key != "return" else "return"
) )
for key, value in hints.items() for key, value in hints.items()
} }
return_type = serialized_hints.pop("return")
api_schema["properties"][name] = { api_schema["properties"][name] = {
"type": "object", "type": "object",
"required": [k for k in serialized_hints.keys()], "required": ["arguments", "return"],
"additionalProperties": False, "additionalProperties": False,
"properties": {**serialized_hints}, "properties": {
"return": return_type,
"arguments": {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": serialized_hints,
},
},
} }
return json.dumps(api_schema, indent=2) return api_schema
def get_method_argtype(self, method_name: str, arg_name: str) -> Any:
from inspect import signature
func = self._registry.get(method_name, None)
if func:
sig = signature(func)
param = sig.parameters.get(arg_name)
if param:
param_class = param.annotation
return param_class
return None
API = _MethodRegistry() API = _MethodRegistry()

View File

@ -42,10 +42,14 @@ def type_to_dict(t: Any, scope: str = "") -> dict:
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)} return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
elif issubclass(origin, dict): elif issubclass(origin, dict):
return { value_type = t.__args__[1]
"type": "object", if value_type is Any:
"additionalProperties": type_to_dict(t.__args__[1], scope), return {"type": "object", "additionalProperties": True}
} else:
return {
"type": "object",
"additionalProperties": type_to_dict(value_type, scope),
}
raise BaseException(f"Error api type not yet supported {t!s}") raise BaseException(f"Error api type not yet supported {t!s}")

View File

@ -39,7 +39,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
system = config["system"] system = config["system"]
# Check if the machine exists # Check if the machine exists
machines = list_machines(False, flake_url) machines = list_machines(flake_url, False)
if machine_name not in machines: if machine_name not in machines:
raise ClanError( raise ClanError(
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}" f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"

View File

@ -12,7 +12,7 @@ log = logging.getLogger(__name__)
@dataclass @dataclass
class MachineCreateRequest: class MachineCreateRequest:
name: str name: str
config: dict config: dict[str, int]
@API.register @API.register

View File

@ -20,7 +20,7 @@ class MachineInfo:
@API.register @API.register
def list_machines(debug: bool, flake_url: Path | str) -> dict[str, MachineInfo]: def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(
@ -57,7 +57,7 @@ def list_command(args: argparse.Namespace) -> None:
print("Listing all machines:\n") print("Listing all machines:\n")
print("Source: ", flake_path) print("Source: ", flake_path)
print("-" * 40) print("-" * 40)
for name, machine in list_machines(args.debug, flake_path).items(): for name, machine in list_machines(flake_path, args.debug).items():
description = machine.machine_description or "[no description]" description = machine.machine_description or "[no description]"
print(f"{name}\n: {description}\n") print(f"{name}\n: {description}\n")
print("-" * 40) print("-" * 40)

View File

@ -9,6 +9,7 @@ from threading import Lock
from typing import Any from typing import Any
import gi import gi
from clan_cli.api import API
gi.require_version("WebKit", "6.0") gi.require_version("WebKit", "6.0")
@ -95,11 +96,34 @@ class WebView:
self.queue_size += 1 self.queue_size += 1
def threaded_handler( def threaded_handler(
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str self,
handler_fn: Callable[
...,
Any,
],
data: dict[str, Any] | None,
method_name: str,
) -> None: ) -> None:
with self.mutex_lock: with self.mutex_lock:
log.debug("Executing... ", method_name) log.debug("Executing... ", method_name)
result = handler_fn(data) log.debug(f"{data}")
if data is None:
result = handler_fn()
else:
reconciled_arguments = {}
for k, v in data.items():
# Some functions expect to be called with dataclass instances
# But the js api returns dictionaries.
# Introspect the function and create the expected dataclass from dict dynamically
# Depending on the introspected argument_type
arg_type = API.get_method_argtype(method_name, k)
if dataclasses.is_dataclass(arg_type):
reconciled_arguments[k] = arg_type(**v)
else:
reconciled_arguments[k] = v
result = handler_fn(**reconciled_arguments)
serialized = json.dumps(dataclass_to_dict(result)) serialized = json.dumps(dataclass_to_dict(result))
# Use idle_add to queue the response call to js on the main GTK thread # Use idle_add to queue the response call to js on the main GTK thread

View File

@ -28,7 +28,7 @@ export const makeCountContext = () => {
getMachines: () => { getMachines: () => {
// When the gtk function sends its data the loading state will be set to false // When the gtk function sends its data the loading state will be set to false
setLoading(true); setLoading(true);
pyApi.list_machines.dispatch("."); pyApi.list_machines.dispatch({ debug: true, flake_url: "." });
}, },
}, },
] as const; ] as const;

View File

@ -4,7 +4,7 @@ import { schema } from "@/api";
export type API = FromSchema<typeof schema>; export type API = FromSchema<typeof schema>;
export type OperationNames = keyof API; export type OperationNames = keyof API;
export type OperationArgs<T extends OperationNames> = API[T]["argument"]; export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"]; export type OperationResponse<T extends OperationNames> = API[T]["return"];
declare global { declare global {
@ -15,7 +15,10 @@ declare global {
webkit: { webkit: {
messageHandlers: { messageHandlers: {
gtk: { gtk: {
postMessage: (message: { method: OperationNames; data: any }) => void; postMessage: (message: {
method: OperationNames;
data: OperationArgs<OperationNames>;
}) => void;
}; };
}; };
}; };
@ -31,7 +34,7 @@ function createFunctions<K extends OperationNames>(
return { return {
dispatch: (args: OperationArgs<K>) => { dispatch: (args: OperationArgs<K>) => {
console.log( console.log(
`Operation: ${operationName}, Arguments: ${JSON.stringify(args)}` `Operation: ${String(operationName)}, Arguments: ${JSON.stringify(args)}`
); );
// Send the data to the gtk app // Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({ window.webkit.messageHandlers.gtk.postMessage({
@ -69,7 +72,10 @@ const deserialize =
// Create the API object // Create the API object
const pyApi: PyApi = {} as PyApi; const pyApi: PyApi = {} as PyApi;
operationNames.forEach((name) => { operationNames.forEach((opName) => {
const name = opName as OperationNames;
// @ts-ignore: TODO make typescript happy
pyApi[name] = createFunctions(name); pyApi[name] = createFunctions(name);
}); });
export { pyApi }; export { pyApi };