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

View File

@ -1,6 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass
from functools import wraps
from inspect import Parameter, signature
from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints
from clan_cli.errors import ClanError
@ -21,17 +22,34 @@ class ApiError:
class SuccessDataClass(Generic[ResponseDataType]):
status: Annotated[Literal["success"], "The status of the response."]
data: ResponseDataType
op_key: str | None
@dataclass
class ErrorDataClass:
status: Literal["error"]
errors: list[ApiError]
op_key: str | None
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:
def __init__(self) -> None:
self._orig: dict[str, Callable[[Any], Any]] = {}
@ -41,13 +59,16 @@ class _MethodRegistry:
self._orig[fn.__name__] = 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:
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:
return ErrorDataClass(
status="error",
op_key=op_key,
errors=[
ApiError(
message=e.msg,
@ -63,6 +84,11 @@ class _MethodRegistry:
orig_return_type = get_type_hints(fn).get("return")
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
return fn
@ -91,6 +117,12 @@ class _MethodRegistry:
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] = {
"type": "object",
"required": ["arguments", "return"],
@ -99,7 +131,7 @@ class _MethodRegistry:
"return": return_type,
"arguments": {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"required": required_args,
"additionalProperties": False,
"properties": serialized_hints,
},

View File

@ -11,15 +11,17 @@ faker.option({
const getFakeResponse = (method: OperationNames, data: any) => {
const fakeData = faker.generate(schema.properties[method].properties.return);
const { op_key } = data;
if (method === "open_file") {
return {
status: "success",
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 };

View File

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

View File

@ -6,6 +6,7 @@ import {
createEffect,
} from "solid-js";
import { OperationResponse, pyApi } from "./api";
import { currClanURI } from "./App";
export const makeMachineContext = () => {
const [machines, setMachines] =
@ -27,7 +28,10 @@ export const makeMachineContext = () => {
getMachines: () => {
// When the gtk function sends its data the loading state will be set to false
setLoading(true);
pyApi.list_machines.dispatch({ debug: true, flake_url: "." });
pyApi.list_machines.dispatch({
debug: true,
flake_url: currClanURI(),
});
},
},
] as const;

View File

@ -11,6 +11,10 @@ export type SuccessData<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "success" }
>;
export type ErrorData<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "error" }
>;
export type ClanOperations = {
[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.
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>(
operationName: K
): {
@ -41,24 +66,27 @@ function createFunctions<K extends OperationNames>(
} {
return {
dispatch: (args: OperationArgs<K>) => {
console.log(
`Operation: ${String(operationName)}, Arguments: ${JSON.stringify(args)}`
);
// console.log(
// `Operation: ${String(operationName)}, Arguments: ${JSON.stringify(args)}`
// );
// Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({
method: operationName,
data: args,
});
},
receive: (fn: (response: OperationResponse<K>) => void) => {
window.clan[operationName] = deserialize(fn);
receive: (
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 = {
[K in OperationNames]: {
dispatch: (args: OperationArgs<K>) => void;
@ -70,7 +98,6 @@ const deserialize =
<T>(fn: (response: T) => void) =>
(str: string) => {
try {
console.debug("Received data: ", str);
fn(JSON.parse(str) as T);
} catch (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) {
console.log("Development mode");
// Load the debugger in development mode
@ -27,7 +26,7 @@ if (import.meta.env.DEV) {
console.debug("Python API call", { method, data });
setTimeout(() => {
const mock = getFakeResponse(method, data);
console.log("mock", { mock });
console.log("Returning mock-data: ", { mock });
window.clan[method](JSON.stringify(mock));
}, 200);

View File

@ -10,6 +10,7 @@ import { useMachineContext } from "../../Config";
import { route } from "@/src/App";
import { OperationResponse, pyApi } from "@/src/api";
import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem";
type FilesModel = Extract<
OperationResponse<"get_directory">,
@ -99,37 +100,7 @@ export const MachineListView: Component = () => {
<Match when={!loading()}>
<ul>
<For each={data()}>
{(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>
)}
{(entry) => <MachineListItem name={entry} />}
</For>
</ul>
</Match>