api: improve message serialisation
This commit is contained in:
parent
fc8a64ef49
commit
691ae9fb15
|
@ -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
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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: () => {},
|
||||
},
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user