forked from clan/clan-core
clan-api: wrap all api responses with error/success envelop type
This commit is contained in:
parent
db88e63148
commit
6576290160
@ -1,10 +1,12 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, Literal, TypeVar
|
||||
from functools import wraps
|
||||
from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
ResponseDataType = TypeVar("ResponseDataType")
|
||||
|
||||
|
||||
@ -16,22 +18,55 @@ class ApiError:
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiResponse(Generic[ResponseDataType]):
|
||||
status: Literal["success", "error"]
|
||||
errors: list[ApiError] | None
|
||||
data: ResponseDataType | None
|
||||
class SuccessDataClass(Generic[ResponseDataType]):
|
||||
status: Annotated[Literal["success"], "The status of the response."]
|
||||
data: ResponseDataType
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorDataClass:
|
||||
status: Literal["error"]
|
||||
errors: list[ApiError]
|
||||
|
||||
|
||||
ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass
|
||||
|
||||
|
||||
class _MethodRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._orig: dict[str, Callable[[Any], Any]] = {}
|
||||
self._registry: dict[str, Callable[[Any], Any]] = {}
|
||||
|
||||
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
|
||||
self._registry[fn.__name__] = fn
|
||||
self._orig[fn.__name__] = fn
|
||||
|
||||
@wraps(fn)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> ApiResponse[T]:
|
||||
try:
|
||||
data: T = fn(*args, **kwargs)
|
||||
return SuccessDataClass(status="success", data=data)
|
||||
except ClanError as e:
|
||||
return ErrorDataClass(
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=e.msg,
|
||||
description=e.description,
|
||||
location=[fn.__name__, e.location],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# @wraps preserves all metadata of fn
|
||||
# we need to update the annotation, because our wrapper changes the return type
|
||||
# This overrides the new return type annotation with the generic typeVar filled in
|
||||
orig_return_type = get_type_hints(fn).get("return")
|
||||
wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore
|
||||
|
||||
self._registry[fn.__name__] = wrapper
|
||||
return fn
|
||||
|
||||
def to_json_schema(self) -> dict[str, Any]:
|
||||
# Import only when needed
|
||||
from typing import get_type_hints
|
||||
|
||||
from clan_cli.api.util import type_to_dict
|
||||
|
@ -8,9 +8,8 @@ import {
|
||||
import { OperationResponse, pyApi } from "./message";
|
||||
|
||||
export const makeCountContext = () => {
|
||||
const [machines, setMachines] = createSignal<
|
||||
OperationResponse<"list_machines">
|
||||
>([]);
|
||||
const [machines, setMachines] =
|
||||
createSignal<OperationResponse<"list_machines">>();
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
pyApi.list_machines.receive((machines) => {
|
||||
@ -41,7 +40,7 @@ export const CountContext = createContext<CountContextType>([
|
||||
loading: () => false,
|
||||
|
||||
// eslint-disable-next-line
|
||||
machines: () => ([]),
|
||||
machines: () => undefined,
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
|
@ -72,6 +72,12 @@ const deserialize =
|
||||
// Create the API object
|
||||
|
||||
const pyApi: PyApi = {} as PyApi;
|
||||
|
||||
pyApi.create_clan.receive((r) => {
|
||||
if (r.status === "success") {
|
||||
r.status;
|
||||
}
|
||||
});
|
||||
operationNames.forEach((opName) => {
|
||||
const name = opName as OperationNames;
|
||||
// @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly
|
||||
|
@ -1,13 +1,30 @@
|
||||
import { For, Match, Switch, createEffect, type Component } from "solid-js";
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Switch,
|
||||
createEffect,
|
||||
createSignal,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { useCountContext } from "../../Config";
|
||||
import { route } from "@/src/App";
|
||||
|
||||
export const MachineListView: Component = () => {
|
||||
const [{ machines, loading }, { getMachines }] = useCountContext();
|
||||
|
||||
const [data, setData] = createSignal<string[]>([]);
|
||||
createEffect(() => {
|
||||
if (route() === "machines") getMachines();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const response = machines();
|
||||
if (response?.status === "success") {
|
||||
console.log(response.data);
|
||||
setData(response.data);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="max-w-screen-lg">
|
||||
<div class="tooltip" data-tip="Refresh ">
|
||||
@ -32,12 +49,12 @@ export const MachineListView: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!loading() && machines().length === 0}>
|
||||
<Match when={!loading() && data().length === 0}>
|
||||
No machines found
|
||||
</Match>
|
||||
<Match when={!loading()}>
|
||||
<ul>
|
||||
<For each={machines()}>
|
||||
<For each={data()}>
|
||||
{(entry) => (
|
||||
<li>
|
||||
<div class="card card-side m-2 bg-base-100 shadow-lg">
|
||||
|
Loading…
Reference in New Issue
Block a user