api: improve message serialisation

This commit is contained in:
Johannes Kirschbauer 2024-05-23 09:33:57 +02:00
parent fc8a64ef49
commit 691ae9fb15
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
9 changed files with 91 additions and 30 deletions

View File

@ -1,12 +1,14 @@
from collections.abc import Callable
from typing import Any
from typing import Any, TypeVar
T = TypeVar("T")
class _MethodRegistry:
def __init__(self) -> None:
self._registry: dict[str, Callable] = {}
def register(self, fn: Callable) -> Callable:
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._registry[fn.__name__] = fn
return fn

View File

@ -44,6 +44,7 @@ def type_to_dict(t: Any, scope: str = "") -> dict:
elif issubclass(origin, dict):
return {
"type": "object",
"additionalProperties": type_to_dict(t.__args__[1], scope),
}
raise BaseException(f"Error api type not yet supported {t!s}")

View File

@ -1,4 +1,5 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
@ -11,18 +12,24 @@ from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def list_machines(
debug: bool,
flake_url: Path | str,
) -> list[str]:
def list_machines(debug: bool, flake_url: Path | str) -> dict[str, MachineInfo]:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}",
"--apply",
"builtins.attrNames",
"""builtins.mapAttrs (name: attrs: {
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
})""",
"--json",
]
)
@ -33,12 +40,20 @@ def list_machines(
proc = run(cmd)
res = proc.stdout.strip()
return json.loads(res)
machines_dict = json.loads(res)
return {k: MachineInfo(**v) for k, v in machines_dict.items()}
def list_command(args: argparse.Namespace) -> None:
for machine in list_machines(args.debug, Path(args.flake)):
print(machine)
flake_path = Path(args.flake).resolve()
print("Listing all machines:\n")
print("Source: ", flake_path)
print("-" * 40)
for name, machine in list_machines(args.debug, flake_path).items():
description = machine.machine_description or "[no description]"
print(f"{name}\n: {description}\n")
print("-" * 40)
def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -106,7 +106,7 @@ class MainApplication(Adw.Application):
def on_activate(self, source: "MainApplication") -> None:
if not self.window:
self.init_style()
self.window = MainWindow(config=ClanConfig(initial_view="webview"))
self.window = MainWindow(config=ClanConfig(initial_view="list"))
self.window.set_application(self)
self.window.show()

View File

@ -1,3 +1,4 @@
import dataclasses
import json
import logging
import sys
@ -22,6 +23,23 @@ site_index: Path = (
log = logging.getLogger(__name__)
def dataclass_to_dict(obj: Any) -> Any:
"""
Utility function to convert dataclasses to dictionaries
It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries
It does NOT convert member functions.
"""
if dataclasses.is_dataclass(obj):
return {k: dataclass_to_dict(v) for k, v in dataclasses.asdict(obj).items()}
elif isinstance(obj, (list, tuple)):
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:
return obj
class WebView:
def __init__(self, methods: dict[str, Callable]) -> None:
self.method_registry: dict[str, Callable] = methods
@ -82,7 +100,7 @@ class WebView:
with self.mutex_lock:
log.debug("Executing... ", method_name)
result = handler_fn(data)
serialized = json.dumps(result)
serialized = json.dumps(dataclass_to_dict(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

@ -62,8 +62,7 @@ class MainWindow(Adw.ApplicationWindow):
stack_view.add_named(Logs(), "logs")
webview = WebView(methods=API._registry)
stack_view.add_named(webview.get_webview(), "list")
stack_view.add_named(webview.get_webview(), "webview")
stack_view.set_visible_child_name(config.initial_view)

View File

@ -1,8 +1,16 @@
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
import { pyApi } from "./message";
import {
createSignal,
createContext,
useContext,
JSXElement,
createEffect,
} from "solid-js";
import { OperationResponse, pyApi } from "./message";
export const makeCountContext = () => {
const [machines, setMachines] = createSignal<string[]>([]);
const [machines, setMachines] = createSignal<
OperationResponse<"list_machines">
>({});
const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => {
@ -10,6 +18,10 @@ export const makeCountContext = () => {
setMachines(machines);
});
createEffect(() => {
console.log("The count is now", machines());
});
return [
{ loading, machines },
{
@ -25,7 +37,10 @@ export const makeCountContext = () => {
type CountContextType = ReturnType<typeof makeCountContext>;
export const CountContext = createContext<CountContextType>([
{ loading: () => false, machines: () => [] },
{
loading: () => false,
machines: () => ({}),
},
{
getMachines: () => {},
},

View File

@ -1,11 +1,11 @@
import { FromSchema } from "json-schema-to-ts";
import { schema } from "@/api";
type API = FromSchema<typeof schema>;
export type API = FromSchema<typeof schema>;
type OperationNames = keyof API;
type OperationArgs<T extends OperationNames> = API[T]["argument"];
type OperationResponse<T extends OperationNames> = API[T]["return"];
export type OperationNames = keyof API;
export type OperationArgs<T extends OperationNames> = API[T]["argument"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
declare global {
interface Window {
@ -40,7 +40,7 @@ function createFunctions<K extends OperationNames>(
});
},
receive: (fn: (response: OperationResponse<K>) => void) => {
window.clan.list_machines = deserialize(fn);
window.clan[operationName] = deserialize(fn);
},
};
}
@ -59,6 +59,7 @@ 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

@ -1,24 +1,34 @@
import { For, Match, Switch, type Component } from "solid-js";
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { useCountContext } from "./Config";
export const Nested: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
const list = () => Object.values(machines());
createEffect(() => {
console.log("1", list());
});
createEffect(() => {
console.log("2", machines());
});
return (
<div>
<button onClick={() => getMachines()} class="btn btn-primary">
Get machines
</button>
<hr />
<div></div>
<Switch>
<Match when={loading()}>Loading...</Match>
<Match when={!loading() && machines().length === 0}>
<Match when={!loading() && Object.entries(machines()).length === 0}>
No machines found
</Match>
<Match when={!loading() && machines().length}>
<For each={machines()}>
{(machine, i) => (
<Match when={!loading()}>
<For each={list()}>
{(entry, i) => (
<li>
{i() + 1}: {machine}
{i() + 1}: {entry.machineName}{" "}
{entry.machineDescription || "No description"}
</li>
)}
</For>