Source code for gyoza.decorator.core
"""Simplified gyoza decorator for input/output model-driven function execution."""
from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar
from pydantic import BaseModel, ValidationError
F = TypeVar("F", bound=Callable[..., Any])
ModelType = type[BaseModel]
def _parse_output(result: Any, output_model: ModelType) -> BaseModel:
"""
Convert a function's return value into the output model instance.
Parameters
----------
result : Any
Raw return value from the decorated function.
output_model : type[BaseModel]
Pydantic model to validate and wrap the result.
Returns
-------
BaseModel
Validated output model instance.
Raises
------
ValueError
If the result cannot be mapped to the output model.
"""
if isinstance(result, output_model):
return result
if isinstance(result, BaseModel):
return output_model.model_validate(result.model_dump())
if isinstance(result, dict):
return output_model(**result)
if isinstance(result, tuple):
fields = list(output_model.model_fields.keys())
if len(fields) != len(result):
msg = (
f"Tuple result length {len(result)} does not match "
f"output model field count {len(fields)}"
)
raise ValueError(msg)
return output_model(**dict(zip(fields, result, strict=True)))
fields = list(output_model.model_fields.keys())
if len(fields) == 1:
return output_model(**{fields[0]: result})
msg = (
f"Cannot convert result of type {type(result).__name__} "
f"to {output_model.__name__}"
)
raise ValueError(msg)
[docs]
class GyozaOp:
"""
Bind a function to Pydantic input/output models.
The decorated function keeps its original call signature and gains a
``run`` method that accepts a plain ``dict`` and returns a plain ``dict``,
using the models for validation at both ends.
Parameters
----------
input_model : type[BaseModel]
Pydantic model whose fields describe the function's inputs.
output_model : type[BaseModel]
Pydantic model whose fields describe the function's outputs.
"""
def __init__(self, input_model: ModelType, output_model: ModelType) -> None:
self.input_model = input_model
self.output_model = output_model
def __call__(self, func: F) -> F:
"""Wrap *func*, attaching ``input_model``, ``output_model`` and ``run``."""
return self._wrap(func, self.input_model, self.output_model)
@staticmethod
def _wrap(
func: F,
input_model: ModelType,
output_model: ModelType,
) -> F:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)
def run(inputs: dict[str, Any]) -> dict[str, Any]:
"""
Execute the function from a plain dict and return a plain dict.
Parameters
----------
inputs : dict[str, Any]
Mapping whose keys match the fields of ``input_model``.
Returns
-------
dict[str, Any]
Mapping whose keys match the fields of ``output_model``.
Raises
------
ValueError
If input validation or output conversion fails.
"""
try:
validated = input_model(**inputs)
except ValidationError as exc:
msg = f"Input validation error: {exc}"
raise ValueError(msg) from exc
result = func(
**{k: getattr(validated, k) for k in type(validated).model_fields}
)
try:
output = _parse_output(result, output_model)
except (ValueError, ValidationError) as exc:
msg = f"Output validation error: {exc}"
raise ValueError(msg) from exc
return output.model_dump()
wrapper.run = run # type: ignore[attr-defined]
wrapper.input_model = input_model # type: ignore[attr-defined]
wrapper.output_model = output_model # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
[docs]
def gyoza_op(
*,
input_model: ModelType,
output_model: ModelType,
) -> Callable[[F], F]:
"""
Decorate a function with input/output model definitions.
The decorated function retains its original call signature and gains:
- ``run(inputs: dict) -> dict`` — validates inputs via ``input_model``,
calls the function, and returns the result serialised via ``output_model``.
- ``input_model`` / ``output_model`` — the attached model classes.
Parameters
----------
input_model : type[BaseModel]
Pydantic model whose fields describe the function's inputs.
output_model : type[BaseModel]
Pydantic model whose fields describe the function's outputs.
Returns
-------
Callable[[F], F]
Decorator that wraps the target function.
"""
return GyozaOp(input_model=input_model, output_model=output_model)