api: improve message serialisation #1440
|
@ -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,27 @@ def list_machines(
|
|||
proc = run(cmd)
|
||||
|
||||
res = proc.stdout.strip()
|
||||
return json.loads(res)
|
||||
machines_dict = json.loads(res)
|
||||
|
||||
return {
|
||||
k: MachineInfo(
|
||||
machine_name=v.get("machineName"),
|
||||
machine_description=v.get("machineDescription", None),
|
||||
machine_icon=v.get("machineIcon", None),
|
||||
)
|
||||
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:
|
||||
|
|
|
@ -26,6 +26,14 @@ def test_create_flake(
|
|||
cli.run(["machines", "create", "machine1"])
|
||||
capsys.readouterr() # flush cache
|
||||
|
||||
# create a hardware-configuration.nix that doesn't throw an eval error
|
||||
|
||||
for patch_machine in ["jon", "sara"]:
|
||||
with open(
|
||||
flake_dir / "machines" / f"{patch_machine}/hardware-configuration.nix", "w"
|
||||
) as hw_config_nix:
|
||||
hw_config_nix.write("{}")
|
||||
|
||||
cli.run(["machines", "list"])
|
||||
assert "machine1" in capsys.readouterr().out
|
||||
flake_show = subprocess.run(
|
||||
|
|
|
@ -16,7 +16,10 @@ def test_machine_subcommands(
|
|||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||
|
||||
out = capsys.readouterr()
|
||||
assert "machine1\nvm1\nvm2\n" == out.out
|
||||
|
||||
assert "machine1" in out.out
|
||||
assert "vm1" in out.out
|
||||
assert "vm2" in out.out
|
||||
|
||||
cli.run(
|
||||
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
|
||||
|
@ -25,4 +28,7 @@ def test_machine_subcommands(
|
|||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||
out = capsys.readouterr()
|
||||
assert "vm1\nvm2\n" == out.out
|
||||
|
||||
assert "machine1" not in out.out
|
||||
assert "vm1" in out.out
|
||||
assert "vm2" in out.out
|
||||
|
|
|
@ -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.machine_name}{" "}
|
||||
{entry.machine_description || "No description"}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
|
|
Loading…
Reference in New Issue
Block a user