Merge pull request 'api: improve message serialisation' (#1440) from hsjobeki-feat/api-improvements into main
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class _MethodRegistry:
|
class _MethodRegistry:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._registry: dict[str, Callable] = {}
|
self._registry: dict[str, Callable] = {}
|
||||||
|
|
||||||
def register(self, fn: Callable) -> Callable:
|
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
|
||||||
self._registry[fn.__name__] = fn
|
self._registry[fn.__name__] = fn
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ def type_to_dict(t: Any, scope: str = "") -> dict:
|
|||||||
elif issubclass(origin, dict):
|
elif issubclass(origin, dict):
|
||||||
return {
|
return {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": type_to_dict(t.__args__[1], scope),
|
||||||
}
|
}
|
||||||
|
|
||||||
raise BaseException(f"Error api type not yet supported {t!s}")
|
raise BaseException(f"Error api type not yet supported {t!s}")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -11,18 +12,24 @@ from ..nix import nix_config, nix_eval
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class MachineInfo:
|
||||||
|
machine_name: str
|
||||||
|
machine_description: str | None
|
||||||
|
machine_icon: str | None
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_machines(
|
def list_machines(debug: bool, flake_url: Path | str) -> dict[str, MachineInfo]:
|
||||||
debug: bool,
|
|
||||||
flake_url: Path | str,
|
|
||||||
) -> list[str]:
|
|
||||||
config = nix_config()
|
config = nix_config()
|
||||||
system = config["system"]
|
system = config["system"]
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
[
|
[
|
||||||
f"{flake_url}#clanInternals.machines.{system}",
|
f"{flake_url}#clanInternals.machines.{system}",
|
||||||
"--apply",
|
"--apply",
|
||||||
"builtins.attrNames",
|
"""builtins.mapAttrs (name: attrs: {
|
||||||
|
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
|
||||||
|
})""",
|
||||||
"--json",
|
"--json",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -33,12 +40,27 @@ def list_machines(
|
|||||||
proc = run(cmd)
|
proc = run(cmd)
|
||||||
|
|
||||||
res = proc.stdout.strip()
|
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:
|
def list_command(args: argparse.Namespace) -> None:
|
||||||
for machine in list_machines(args.debug, Path(args.flake)):
|
flake_path = Path(args.flake).resolve()
|
||||||
print(machine)
|
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:
|
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ def test_create_flake(
|
|||||||
cli.run(["machines", "create", "machine1"])
|
cli.run(["machines", "create", "machine1"])
|
||||||
capsys.readouterr() # flush cache
|
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"])
|
cli.run(["machines", "list"])
|
||||||
assert "machine1" in capsys.readouterr().out
|
assert "machine1" in capsys.readouterr().out
|
||||||
flake_show = subprocess.run(
|
flake_show = subprocess.run(
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ def test_machine_subcommands(
|
|||||||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||||
|
|
||||||
out = capsys.readouterr()
|
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(
|
cli.run(
|
||||||
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
|
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
|
||||||
@@ -25,4 +28,7 @@ def test_machine_subcommands(
|
|||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||||
out = capsys.readouterr()
|
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:
|
def on_activate(self, source: "MainApplication") -> None:
|
||||||
if not self.window:
|
if not self.window:
|
||||||
self.init_style()
|
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.set_application(self)
|
||||||
|
|
||||||
self.window.show()
|
self.window.show()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
@@ -22,6 +23,23 @@ site_index: Path = (
|
|||||||
log = logging.getLogger(__name__)
|
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:
|
class WebView:
|
||||||
def __init__(self, methods: dict[str, Callable]) -> None:
|
def __init__(self, methods: dict[str, Callable]) -> None:
|
||||||
self.method_registry: dict[str, Callable] = methods
|
self.method_registry: dict[str, Callable] = methods
|
||||||
@@ -82,7 +100,7 @@ class WebView:
|
|||||||
with self.mutex_lock:
|
with self.mutex_lock:
|
||||||
log.debug("Executing... ", method_name)
|
log.debug("Executing... ", method_name)
|
||||||
result = handler_fn(data)
|
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
|
# 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)
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
stack_view.add_named(Logs(), "logs")
|
stack_view.add_named(Logs(), "logs")
|
||||||
|
|
||||||
webview = WebView(methods=API._registry)
|
webview = WebView(methods=API._registry)
|
||||||
|
stack_view.add_named(webview.get_webview(), "webview")
|
||||||
stack_view.add_named(webview.get_webview(), "list")
|
|
||||||
|
|
||||||
stack_view.set_visible_child_name(config.initial_view)
|
stack_view.set_visible_child_name(config.initial_view)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
|
import {
|
||||||
import { pyApi } from "./message";
|
createSignal,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
JSXElement,
|
||||||
|
createEffect,
|
||||||
|
} from "solid-js";
|
||||||
|
import { OperationResponse, pyApi } from "./message";
|
||||||
|
|
||||||
export const makeCountContext = () => {
|
export const makeCountContext = () => {
|
||||||
const [machines, setMachines] = createSignal<string[]>([]);
|
const [machines, setMachines] = createSignal<
|
||||||
|
OperationResponse<"list_machines">
|
||||||
|
>({});
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
pyApi.list_machines.receive((machines) => {
|
pyApi.list_machines.receive((machines) => {
|
||||||
@@ -10,6 +18,10 @@ export const makeCountContext = () => {
|
|||||||
setMachines(machines);
|
setMachines(machines);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("The count is now", machines());
|
||||||
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ loading, machines },
|
{ loading, machines },
|
||||||
{
|
{
|
||||||
@@ -25,7 +37,10 @@ export const makeCountContext = () => {
|
|||||||
type CountContextType = ReturnType<typeof makeCountContext>;
|
type CountContextType = ReturnType<typeof makeCountContext>;
|
||||||
|
|
||||||
export const CountContext = createContext<CountContextType>([
|
export const CountContext = createContext<CountContextType>([
|
||||||
{ loading: () => false, machines: () => [] },
|
{
|
||||||
|
loading: () => false,
|
||||||
|
machines: () => ({}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
getMachines: () => {},
|
getMachines: () => {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { FromSchema } from "json-schema-to-ts";
|
import { FromSchema } from "json-schema-to-ts";
|
||||||
import { schema } from "@/api";
|
import { schema } from "@/api";
|
||||||
|
|
||||||
type API = FromSchema<typeof schema>;
|
export type API = FromSchema<typeof schema>;
|
||||||
|
|
||||||
type OperationNames = keyof API;
|
export type OperationNames = keyof API;
|
||||||
type OperationArgs<T extends OperationNames> = API[T]["argument"];
|
export type OperationArgs<T extends OperationNames> = API[T]["argument"];
|
||||||
type OperationResponse<T extends OperationNames> = API[T]["return"];
|
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -40,7 +40,7 @@ function createFunctions<K extends OperationNames>(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
receive: (fn: (response: OperationResponse<K>) => void) => {
|
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) =>
|
<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}`);
|
||||||
|
|||||||
@@ -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";
|
import { useCountContext } from "./Config";
|
||||||
|
|
||||||
export const Nested: Component = () => {
|
export const Nested: Component = () => {
|
||||||
const [{ machines, loading }, { getMachines }] = useCountContext();
|
const [{ machines, loading }, { getMachines }] = useCountContext();
|
||||||
|
|
||||||
|
const list = () => Object.values(machines());
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("1", list());
|
||||||
|
});
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("2", machines());
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => getMachines()} class="btn btn-primary">
|
<button onClick={() => getMachines()} class="btn btn-primary">
|
||||||
Get machines
|
Get machines
|
||||||
</button>
|
</button>
|
||||||
<hr />
|
<div></div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={loading()}>Loading...</Match>
|
<Match when={loading()}>Loading...</Match>
|
||||||
<Match when={!loading() && machines().length === 0}>
|
<Match when={!loading() && Object.entries(machines()).length === 0}>
|
||||||
No machines found
|
No machines found
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!loading() && machines().length}>
|
<Match when={!loading()}>
|
||||||
<For each={machines()}>
|
<For each={list()}>
|
||||||
{(machine, i) => (
|
{(entry, i) => (
|
||||||
<li>
|
<li>
|
||||||
{i() + 1}: {machine}
|
{i() + 1}: {entry.machine_name}{" "}
|
||||||
|
{entry.machine_description || "No description"}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
Reference in New Issue
Block a user