mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
add api + cli Interface
This commit is contained in:
parent
e7a0e65903
commit
39e7f0d8a5
2 changed files with 246 additions and 0 deletions
162
src/interface/api.py
Normal file
162
src/interface/api.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import types
|
||||
import fastapi
|
||||
import pydantic
|
||||
import uvicorn
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
def snake_to_camel_case(snake: str) -> str:
|
||||
return "".join(word.title() for word in snake.split("_"))
|
||||
|
||||
|
||||
def get_path_param(route: str) -> Optional[str]:
|
||||
last_path = route.split("/")[-1]
|
||||
if last_path and "{" in last_path:
|
||||
return last_path.strip("{}")
|
||||
return None
|
||||
|
||||
|
||||
def alter_params_for_body(
|
||||
parameters, as_body, as_body_except
|
||||
) -> list[Union[list[inspect.Parameter], inspect.Parameter]]:
|
||||
if as_body_except:
|
||||
body_params = []
|
||||
rest = []
|
||||
for param in parameters:
|
||||
if param.name not in as_body_except:
|
||||
body_params.append(param)
|
||||
else:
|
||||
rest.append(param)
|
||||
return [body_params, *rest]
|
||||
|
||||
if as_body:
|
||||
return [parameters]
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
def params_to_body(
|
||||
params: list[inspect.Parameter], func_name: str
|
||||
) -> inspect.Parameter:
|
||||
model = pydantic.create_model(
|
||||
snake_to_camel_case(func_name),
|
||||
**{
|
||||
param.name: (
|
||||
param.annotation,
|
||||
param.default if param.default != param.empty else ...,
|
||||
)
|
||||
for param in params
|
||||
},
|
||||
)
|
||||
|
||||
return inspect.Parameter(
|
||||
func_name.split("_")[0],
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=...,
|
||||
annotation=model,
|
||||
)
|
||||
|
||||
|
||||
class Interface:
|
||||
type = "api"
|
||||
|
||||
instance: Union[fastapi.FastAPI, fastapi.APIRouter]
|
||||
name: str
|
||||
|
||||
def __init__(self, root: bool = False, name: Optional[str] = None):
|
||||
self.instance = fastapi.FastAPI() if root else fastapi.APIRouter()
|
||||
self.name = "root" if root else name or ""
|
||||
|
||||
def add(self, interface: Interface):
|
||||
assert isinstance(interface.instance, fastapi.APIRouter)
|
||||
self.instance.include_router(
|
||||
interface.instance, prefix=f"/{interface.name}", tags=[interface.name]
|
||||
)
|
||||
|
||||
def run(self, host="127.0.0.1", port=6787, debug=False, reload=False):
|
||||
uvicorn.run(
|
||||
self.instance,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info" if not debug else "debug",
|
||||
reload=reload,
|
||||
root_path="/yunohost/api",
|
||||
)
|
||||
|
||||
def cli(self, *args, **kwargs):
|
||||
def decorator(func):
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def api(
|
||||
self,
|
||||
route: str,
|
||||
method: str = "get",
|
||||
as_body: bool = False,
|
||||
as_body_except: Optional[list[str]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
as_body = as_body if not as_body_except else True
|
||||
|
||||
def decorator(func):
|
||||
signature = inspect.signature(func)
|
||||
override_params = []
|
||||
params = alter_params_for_body(
|
||||
signature.parameters.values(), as_body, as_body_except
|
||||
)
|
||||
path_param = get_path_param(route)
|
||||
|
||||
for param in params:
|
||||
if isinstance(param, list):
|
||||
override_params.append(params_to_body(param, func.__name__))
|
||||
else:
|
||||
default_kwargs = kwargs.get(param.name, {})
|
||||
default_cls = (
|
||||
fastapi.Path if param.name == path_param else fastapi.Query
|
||||
)
|
||||
|
||||
if param.default is None:
|
||||
default_value = default_cls(None, **default_kwargs)
|
||||
elif param.default is param.empty:
|
||||
default_value = default_cls(..., **default_kwargs)
|
||||
else:
|
||||
default_value = default_cls(param.default, **default_kwargs)
|
||||
|
||||
override_params.append(param.replace(default=default_value))
|
||||
|
||||
route_func = getattr(self.instance, method)(route)
|
||||
override_signature = signature.replace(parameters=tuple(override_params))
|
||||
|
||||
if as_body:
|
||||
|
||||
def body_to_args_back(*args, **kwargs):
|
||||
new_kwargs = {}
|
||||
for kwarg, value in kwargs.items():
|
||||
if issubclass(type(value), pydantic.BaseModel):
|
||||
new_kwargs = value.dict() | new_kwargs
|
||||
else:
|
||||
new_kwargs[kwarg] = value
|
||||
return func(*args, **new_kwargs)
|
||||
|
||||
body_to_args_back.__name__ = func.__name__
|
||||
body_to_args_back.__signature__ = override_signature
|
||||
route_func(body_to_args_back)
|
||||
else:
|
||||
func_copy = types.FunctionType(
|
||||
func.__code__,
|
||||
func.__globals__,
|
||||
func.__name__,
|
||||
func.__defaults__,
|
||||
func.__closure__,
|
||||
)
|
||||
func_copy.__signature__ = override_signature
|
||||
route_func(func_copy)
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
84
src/interface/cli.py
Normal file
84
src/interface/cli.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import typer
|
||||
import yaml
|
||||
|
||||
from typing import Any, Optional
|
||||
from rich import print as rprint
|
||||
from rich.syntax import Syntax
|
||||
|
||||
|
||||
def parse_cli_command(command: str) -> tuple[str, list[str]]:
|
||||
command, *args = command.split(" ")
|
||||
return (command, [arg.strip("{}") for arg in args])
|
||||
|
||||
|
||||
def print_as_yaml(data: Any):
|
||||
data = yaml.dump(data, default_flow_style=False)
|
||||
rprint(Syntax(data, "yaml", background_color="default"))
|
||||
|
||||
|
||||
class Interface:
|
||||
type = "cli"
|
||||
|
||||
instance: typer.Typer
|
||||
name: str
|
||||
|
||||
def __init__(self, root: bool = False, name: Optional[str] = None):
|
||||
self.instance = typer.Typer()
|
||||
self.name = "root" if root else name or ""
|
||||
|
||||
def add(self, interface: Interface):
|
||||
self.instance.add_typer(interface.instance, name=interface.name)
|
||||
|
||||
def run(self):
|
||||
self.instance()
|
||||
|
||||
def cli(self, command_def: str, **kwargs):
|
||||
def decorator(func):
|
||||
signature = inspect.signature(func)
|
||||
override_params = []
|
||||
command, args = parse_cli_command(command_def)
|
||||
|
||||
for param in signature.parameters.values():
|
||||
|
||||
# Auto setup typer Argument or Option kwargs
|
||||
default_kwargs = kwargs.get(param.name, {})
|
||||
# if param.name not in args and not default_kwargs.get("hidden", False):
|
||||
# default_kwargs["prompt"] = True
|
||||
if param.name == "password":
|
||||
default_kwargs["confirmation_prompt"] = True
|
||||
default_kwargs["hide_input"] = True
|
||||
|
||||
# Define new default value for typer
|
||||
default_cls = typer.Argument if param.name in args else typer.Option
|
||||
if param.default is None:
|
||||
default_value = default_cls(None, **default_kwargs)
|
||||
elif param.default is param.empty:
|
||||
default_value = default_cls(..., **default_kwargs)
|
||||
else:
|
||||
default_value = default_cls(param.default, **default_kwargs)
|
||||
|
||||
override_params.append(param.replace(default=default_value))
|
||||
|
||||
def hook_results(*args, **kwargs):
|
||||
results = func(*args, **kwargs)
|
||||
print_as_yaml(results)
|
||||
return results
|
||||
|
||||
hook_results.__name__ = func.__name__
|
||||
hook_results.__signature__ = signature.replace(
|
||||
parameters=tuple(override_params)
|
||||
)
|
||||
self.instance.command(command)(hook_results)
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def api(self, *args, **kwargs):
|
||||
def decorator(func):
|
||||
return func
|
||||
|
||||
return decorator
|
Loading…
Add table
Reference in a new issue