API: init op_key, improve seralisation & signature typing

This commit is contained in:
Johannes Kirschbauer 2024-06-15 11:32:09 +02:00
parent a89fd31844
commit cb847cab82
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
2 changed files with 46 additions and 6 deletions

View File

@ -38,8 +38,10 @@ def dataclass_to_dict(obj: Any) -> Any:
return [dataclass_to_dict(item) for item in obj] return [dataclass_to_dict(item) for item in obj]
elif isinstance(obj, dict): elif isinstance(obj, dict):
return {k: dataclass_to_dict(v) for k, v in obj.items()} return {k: dataclass_to_dict(v) for k, v in obj.items()}
else: elif isinstance(obj, Path):
return str(obj) return str(obj)
else:
return obj
# Implement the abstract open_file function # Implement the abstract open_file function
@ -265,6 +267,7 @@ class WebView:
result = handler_fn() result = handler_fn()
else: else:
reconciled_arguments = {} reconciled_arguments = {}
op_key = data.pop("op_key", None)
for k, v in data.items(): for k, v in data.items():
# Some functions expect to be called with dataclass instances # Some functions expect to be called with dataclass instances
# But the js api returns dictionaries. # But the js api returns dictionaries.
@ -276,8 +279,13 @@ class WebView:
else: else:
reconciled_arguments[k] = v reconciled_arguments[k] = v
result = handler_fn(**reconciled_arguments) r = handler_fn(**reconciled_arguments)
serialized = json.dumps(dataclass_to_dict(result)) # Parse the result to a serializable dictionary
# Echo back the "op_key" to the js api
result = dataclass_to_dict(r)
result["op_key"] = op_key
serialized = json.dumps(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)

View File

@ -1,6 +1,7 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import wraps from functools import wraps
from inspect import Parameter, signature
from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
@ -21,17 +22,34 @@ class ApiError:
class SuccessDataClass(Generic[ResponseDataType]): class SuccessDataClass(Generic[ResponseDataType]):
status: Annotated[Literal["success"], "The status of the response."] status: Annotated[Literal["success"], "The status of the response."]
data: ResponseDataType data: ResponseDataType
op_key: str | None
@dataclass @dataclass
class ErrorDataClass: class ErrorDataClass:
status: Literal["error"] status: Literal["error"]
errors: list[ApiError] errors: list[ApiError]
op_key: str | None
ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass
def update_wrapper_signature(wrapper: Callable, wrapped: Callable) -> None:
sig = signature(wrapped)
params = list(sig.parameters.values())
# Add 'op_key' parameter
op_key_param = Parameter(
"op_key", Parameter.KEYWORD_ONLY, default=None, annotation=str | None
)
params.append(op_key_param)
# Create a new signature
new_sig = sig.replace(parameters=params)
wrapper.__signature__ = new_sig # type: ignore
class _MethodRegistry: class _MethodRegistry:
def __init__(self) -> None: def __init__(self) -> None:
self._orig: dict[str, Callable[[Any], Any]] = {} self._orig: dict[str, Callable[[Any], Any]] = {}
@ -41,13 +59,16 @@ class _MethodRegistry:
self._orig[fn.__name__] = fn self._orig[fn.__name__] = fn
@wraps(fn) @wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> ApiResponse[T]: def wrapper(
*args: Any, op_key: str | None = None, **kwargs: Any
) -> ApiResponse[T]:
try: try:
data: T = fn(*args, **kwargs) data: T = fn(*args, **kwargs)
return SuccessDataClass(status="success", data=data) return SuccessDataClass(status="success", data=data, op_key=op_key)
except ClanError as e: except ClanError as e:
return ErrorDataClass( return ErrorDataClass(
status="error", status="error",
op_key=op_key,
errors=[ errors=[
ApiError( ApiError(
message=e.msg, message=e.msg,
@ -63,6 +84,11 @@ class _MethodRegistry:
orig_return_type = get_type_hints(fn).get("return") orig_return_type = get_type_hints(fn).get("return")
wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore
# Add additional argument for the operation key
wrapper.__annotations__["op_key"] = str | None # type: ignore
update_wrapper_signature(wrapper, fn)
self._registry[fn.__name__] = wrapper self._registry[fn.__name__] = wrapper
return fn return fn
@ -91,6 +117,12 @@ class _MethodRegistry:
return_type = serialized_hints.pop("return") return_type = serialized_hints.pop("return")
sig = signature(func)
required_args = []
for n, param in sig.parameters.items():
if param.default == Parameter.empty:
required_args.append(n)
api_schema["properties"][name] = { api_schema["properties"][name] = {
"type": "object", "type": "object",
"required": ["arguments", "return"], "required": ["arguments", "return"],
@ -99,7 +131,7 @@ class _MethodRegistry:
"return": return_type, "return": return_type,
"arguments": { "arguments": {
"type": "object", "type": "object",
"required": [k for k in serialized_hints.keys()], "required": required_args,
"additionalProperties": False, "additionalProperties": False,
"properties": serialized_hints, "properties": serialized_hints,
}, },