Merge pull request 'API: init op_key, improve seralisation & signature typing' (#1622) from hsjobeki/clan-core:hsjobeki-main into main
All checks were successful
deploy / deploy-docs (push) Successful in 20s
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.flash Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup 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.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-pytest 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."clan-dep-python3.11-mypy" 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-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-app 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.borgbackup Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs 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.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-install-test-ubuntu-22-04 Build done.
buildbot/nix-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept 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.package-webview-ui 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-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.postgresql Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.template-minimal Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
checks / checks-impure (push) Successful in 2m4s

This commit is contained in:
clan-bot 2024-06-15 09:38:29 +00:00
commit b28950f310
9 changed files with 183 additions and 51 deletions

View File

@ -38,8 +38,10 @@ def dataclass_to_dict(obj: Any) -> Any:
return [dataclass_to_dict(item) for item in obj] return [dataclass_to_dict(item) for item in obj]
elif isinstance(obj, dict): elif isinstance(obj, dict):
return {k: dataclass_to_dict(v) for k, v in obj.items()} return {k: dataclass_to_dict(v) for k, v in obj.items()}
else: elif isinstance(obj, Path):
return str(obj) return str(obj)
else:
return obj
# Implement the abstract open_file function # Implement the abstract open_file function
@ -265,6 +267,7 @@ class WebView:
result = handler_fn() result = handler_fn()
else: else:
reconciled_arguments = {} reconciled_arguments = {}
op_key = data.pop("op_key", None)
for k, v in data.items(): for k, v in data.items():
# Some functions expect to be called with dataclass instances # Some functions expect to be called with dataclass instances
# But the js api returns dictionaries. # But the js api returns dictionaries.
@ -276,8 +279,13 @@ class WebView:
else: else:
reconciled_arguments[k] = v reconciled_arguments[k] = v
result = handler_fn(**reconciled_arguments) r = handler_fn(**reconciled_arguments)
serialized = json.dumps(dataclass_to_dict(result)) # Parse the result to a serializable dictionary
# Echo back the "op_key" to the js api
result = dataclass_to_dict(r)
result["op_key"] = op_key
serialized = json.dumps(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
GLib.idle_add(self.return_data_to_js, method_name, serialized) GLib.idle_add(self.return_data_to_js, method_name, serialized)

View File

@ -1,6 +1,7 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import wraps from functools import wraps
from inspect import Parameter, signature
from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
@ -21,17 +22,34 @@ class ApiError:
class SuccessDataClass(Generic[ResponseDataType]): class SuccessDataClass(Generic[ResponseDataType]):
status: Annotated[Literal["success"], "The status of the response."] status: Annotated[Literal["success"], "The status of the response."]
data: ResponseDataType data: ResponseDataType
op_key: str | None
@dataclass @dataclass
class ErrorDataClass: class ErrorDataClass:
status: Literal["error"] status: Literal["error"]
errors: list[ApiError] errors: list[ApiError]
op_key: str | None
ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass
def update_wrapper_signature(wrapper: Callable, wrapped: Callable) -> None:
sig = signature(wrapped)
params = list(sig.parameters.values())
# Add 'op_key' parameter
op_key_param = Parameter(
"op_key", Parameter.KEYWORD_ONLY, default=None, annotation=str | None
)
params.append(op_key_param)
# Create a new signature
new_sig = sig.replace(parameters=params)
wrapper.__signature__ = new_sig # type: ignore
class _MethodRegistry: class _MethodRegistry:
def __init__(self) -> None: def __init__(self) -> None:
self._orig: dict[str, Callable[[Any], Any]] = {} self._orig: dict[str, Callable[[Any], Any]] = {}
@ -41,13 +59,16 @@ class _MethodRegistry:
self._orig[fn.__name__] = fn self._orig[fn.__name__] = fn
@wraps(fn) @wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> ApiResponse[T]: def wrapper(
*args: Any, op_key: str | None = None, **kwargs: Any
) -> ApiResponse[T]:
try: try:
data: T = fn(*args, **kwargs) data: T = fn(*args, **kwargs)
return SuccessDataClass(status="success", data=data) return SuccessDataClass(status="success", data=data, op_key=op_key)
except ClanError as e: except ClanError as e:
return ErrorDataClass( return ErrorDataClass(
status="error", status="error",
op_key=op_key,
errors=[ errors=[
ApiError( ApiError(
message=e.msg, message=e.msg,
@ -63,6 +84,11 @@ class _MethodRegistry:
orig_return_type = get_type_hints(fn).get("return") orig_return_type = get_type_hints(fn).get("return")
wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore
# Add additional argument for the operation key
wrapper.__annotations__["op_key"] = str | None # type: ignore
update_wrapper_signature(wrapper, fn)
self._registry[fn.__name__] = wrapper self._registry[fn.__name__] = wrapper
return fn return fn
@ -91,6 +117,12 @@ class _MethodRegistry:
return_type = serialized_hints.pop("return") return_type = serialized_hints.pop("return")
sig = signature(func)
required_args = []
for n, param in sig.parameters.items():
if param.default == Parameter.empty:
required_args.append(n)
api_schema["properties"][name] = { api_schema["properties"][name] = {
"type": "object", "type": "object",
"required": ["arguments", "return"], "required": ["arguments", "return"],
@ -99,7 +131,7 @@ class _MethodRegistry:
"return": return_type, "return": return_type,
"arguments": { "arguments": {
"type": "object", "type": "object",
"required": [k for k in serialized_hints.keys()], "required": required_args,
"additionalProperties": False, "additionalProperties": False,
"properties": serialized_hints, "properties": serialized_hints,
}, },

View File

@ -11,15 +11,17 @@ faker.option({
const getFakeResponse = (method: OperationNames, data: any) => { const getFakeResponse = (method: OperationNames, data: any) => {
const fakeData = faker.generate(schema.properties[method].properties.return); const fakeData = faker.generate(schema.properties[method].properties.return);
const { op_key } = data;
if (method === "open_file") { if (method === "open_file") {
return { return {
status: "success", status: "success",
data: "/path/to/clan", data: "/path/to/clan",
op_key,
}; };
} }
return fakeData; // @ts-expect-error: fakeData is guaranteed to always be some object
return { ...fakeData, op_key };
}; };
export { getFakeResponse }; export { getFakeResponse };

View File

@ -7,6 +7,12 @@ import { Toaster } from "solid-toast";
// Global state // Global state
const [route, setRoute] = createSignal<Route>("machines"); const [route, setRoute] = createSignal<Route>("machines");
const [currClanURI, setCurrClanURI] = createSignal<string>(
"/home/johannes/1_clans/myclan"
);
export { currClanURI, setCurrClanURI };
export { route, setRoute }; export { route, setRoute };
const App: Component = () => { const App: Component = () => {

View File

@ -6,6 +6,7 @@ import {
createEffect, createEffect,
} from "solid-js"; } from "solid-js";
import { OperationResponse, pyApi } from "./api"; import { OperationResponse, pyApi } from "./api";
import { currClanURI } from "./App";
export const makeMachineContext = () => { export const makeMachineContext = () => {
const [machines, setMachines] = const [machines, setMachines] =
@ -27,7 +28,10 @@ export const makeMachineContext = () => {
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({ debug: true, flake_url: "." }); pyApi.list_machines.dispatch({
debug: true,
flake_url: currClanURI(),
});
}, },
}, },
] as const; ] as const;

View File

@ -11,6 +11,10 @@ export type SuccessData<T extends OperationNames> = Extract<
OperationResponse<T>, OperationResponse<T>,
{ status: "success" } { status: "success" }
>; >;
export type ErrorData<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "error" }
>;
export type ClanOperations = { export type ClanOperations = {
[K in OperationNames]: (str: string) => void; [K in OperationNames]: (str: string) => void;
@ -33,6 +37,27 @@ declare global {
// Make sure window.webkit is defined although the type is not correctly filled yet. // Make sure window.webkit is defined although the type is not correctly filled yet.
window.clan = {} as ClanOperations; window.clan = {} as ClanOperations;
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
type ObserverRegistry = {
[K in OperationNames]: ((response: OperationResponse<K>) => void)[];
};
const obs: ObserverRegistry = operationNames.reduce(
(acc, opName) => ({
...acc,
[opName]: [],
}),
{} as ObserverRegistry
);
interface ReceiveOptions {
/**
* Calls only the registered function that has the same key as used with dispatch
*
*/
fnKey: string;
}
function createFunctions<K extends OperationNames>( function createFunctions<K extends OperationNames>(
operationName: K operationName: K
): { ): {
@ -41,24 +66,27 @@ function createFunctions<K extends OperationNames>(
} { } {
return { return {
dispatch: (args: OperationArgs<K>) => { dispatch: (args: OperationArgs<K>) => {
console.log( // console.log(
`Operation: ${String(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({
method: operationName, method: operationName,
data: args, data: args,
}); });
}, },
receive: (fn: (response: OperationResponse<K>) => void) => { receive: (
window.clan[operationName] = deserialize(fn); fn: (response: OperationResponse<K>) => void
// options?: ReceiveOptions
) => {
obs[operationName].push(fn);
window.clan[operationName] = (s: string) => {
obs[operationName].forEach((f) => deserialize(f)(s));
};
}, },
}; };
} }
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
type PyApi = { type PyApi = {
[K in OperationNames]: { [K in OperationNames]: {
dispatch: (args: OperationArgs<K>) => void; dispatch: (args: OperationArgs<K>) => void;
@ -70,7 +98,6 @@ const deserialize =
<T>(fn: (response: T) => void) => <T>(fn: (response: T) => void) =>
(str: string) => { (str: string) => {
try { try {
console.debug("Received data: ", str);
fn(JSON.parse(str) as T); fn(JSON.parse(str) as T);
} catch (e) { } catch (e) {
alert(`Error parsing JSON: ${e}`); alert(`Error parsing JSON: ${e}`);

View File

@ -0,0 +1,83 @@
import { For, Show, createSignal } from "solid-js";
import { ErrorData, SuccessData, pyApi } from "../api";
import { currClanURI } from "../App";
interface MachineListItemProps {
name: string;
}
type MachineDetails = Record<string, SuccessData<"show_machine">["data"]>;
type MachineErrors = Record<string, ErrorData<"show_machine">["errors"]>;
const [details, setDetails] = createSignal<MachineDetails>({});
const [errors, setErrors] = createSignal<MachineErrors>({});
pyApi.show_machine.receive((r) => {
if (r.status === "error") {
const { op_key } = r;
if (op_key) {
setErrors((e) => ({ ...e, [op_key]: r.errors }));
}
console.error(r.errors);
}
if (r.status === "success") {
setDetails((d) => ({ ...d, [r.data.machine_name]: r.data }));
}
});
export const MachineListItem = (props: MachineListItemProps) => {
const { name } = props;
pyApi.show_machine.dispatch({
op_key: name,
machine_name: name,
debug: false,
flake_url: currClanURI(),
});
return (
<li>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<span class="material-icons content-center text-5xl">
devices_other
</span>
</figure>
<div class="card-body flex-row justify-between">
<div class="flex flex-col">
<h2 class="card-title">{name}</h2>
<Show when={errors()[name]}>
{(errors) => (
<For each={errors()}>
{(error) => (
<p class="text-red-500">
{error.message}: {error.description}
</p>
)}
</For>
)}
</Show>
<Show when={details()[name]}>
{(details) => (
<p
classList={{
"text-gray-400": !details().machine_description,
"text-gray-600": !!details().machine_description,
}}
>
{details().machine_description || "No description"}
</p>
)}
</Show>
</div>
<div>
<button class="btn btn-ghost">
<span class="material-icons">more_vert</span>
</button>
</div>
</div>
</div>
</li>
);
};

View File

@ -14,7 +14,6 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
); );
} }
console.log(import.meta.env);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log("Development mode"); console.log("Development mode");
// Load the debugger in development mode // Load the debugger in development mode
@ -27,7 +26,7 @@ if (import.meta.env.DEV) {
console.debug("Python API call", { method, data }); console.debug("Python API call", { method, data });
setTimeout(() => { setTimeout(() => {
const mock = getFakeResponse(method, data); const mock = getFakeResponse(method, data);
console.log("mock", { mock }); console.log("Returning mock-data: ", { mock });
window.clan[method](JSON.stringify(mock)); window.clan[method](JSON.stringify(mock));
}, 200); }, 200);

View File

@ -10,6 +10,7 @@ import { useMachineContext } from "../../Config";
import { route } from "@/src/App"; import { route } from "@/src/App";
import { OperationResponse, pyApi } from "@/src/api"; import { OperationResponse, pyApi } from "@/src/api";
import toast from "solid-toast"; import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem";
type FilesModel = Extract< type FilesModel = Extract<
OperationResponse<"get_directory">, OperationResponse<"get_directory">,
@ -99,37 +100,7 @@ export const MachineListView: Component = () => {
<Match when={!loading()}> <Match when={!loading()}>
<ul> <ul>
<For each={data()}> <For each={data()}>
{(entry) => ( {(entry) => <MachineListItem name={entry} />}
<li>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<span class="material-icons content-center text-5xl">
devices_other
</span>
</figure>
<div class="card-body flex-row justify-between">
<div class="flex flex-col">
<h2 class="card-title">{entry}</h2>
{/*
<p
classList={{
"text-gray-400": !entry.machine_description,
"text-gray-600": !!entry.machine_description,
}}
>
{entry.machine_description || "No description"}
</p>
*/}
</div>
<div>
<button class="btn btn-ghost">
<span class="material-icons">more_vert</span>
</button>
</div>
</div>
</div>
</li>
)}
</For> </For>
</ul> </ul>
</Match> </Match>