1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-22 19:14:20 +00:00

Implement engineer module for commands routing

This commit is contained in:
Steffo 2020-12-06 23:41:36 +01:00
parent 162049cb67
commit 7a6d65724e
10 changed files with 258 additions and 1 deletions

View file

@ -0,0 +1,8 @@
``engineer`` - Chat command router
==================================
.. currentmodule:: royalnet.engineer
.. automodule:: royalnet.engineer
:members:
:undoc-members:
:imported-members:

View file

@ -10,6 +10,7 @@ It may be incomplete or outdated, as it is automatically updated.
alchemist
campaigns
engineer
lazy
scrolls
types

39
poetry.lock generated
View file

@ -150,6 +150,19 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydantic"
version = "1.7.3"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
name = "pygments"
version = "2.7.2"
@ -405,7 +418,7 @@ socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[metadata]
lock-version = "1.0"
python-versions = "^3.8"
content-hash = "232cea18b2973090dfb3fe4215ef84e87abac749c8d0b50f556ec39610871cc5"
content-hash = "4363aefc0ea9322445ee375edce430712159829b5ca40561eea7cd74bbf0a7bd"
[metadata.files]
alabaster = [
@ -503,6 +516,30 @@ py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pydantic = [
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
{file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
{file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
{file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
{file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
{file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
{file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
{file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
]
pygments = [
{file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
{file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},

View file

@ -9,6 +9,7 @@ license = "AGPL-3.0-or-later"
python = "^3.8"
sqlalchemy = "^1.3.19"
toml = "^0.10.1"
pydantic = "^1.7.3"
[tool.poetry.dev-dependencies]
pytest = "^6.1.1"

View file

@ -0,0 +1,6 @@
"""
A chatbot command router inspired by :mod:`fastapi`.
"""
from .params import *
from .router import *

View file

@ -0,0 +1,82 @@
from royalnet.types import *
import pydantic
import inspect
class ModelConfig(pydantic.BaseConfig):
"""
A :mod:`pydantic` model config which allows for arbitrary types.
"""
arbitrary_types_allowed = True
def parameter_to_field(param: inspect.Parameter, **kwargs) -> Tuple[type, pydantic.fields.FieldInfo]:
"""
Convert a :class:`inspect.Parameter` to a type-field :class:`tuple`, which can be easily passed to
:func:`pydantic.create_model`.
If the parameter is already a :class:`pydantic.FieldInfo` (created by :func:`pydantic.Field`), it will be
returned as the value, without creating a new model.
:param param: The :class:`inspect.Parameter` to convert.
:param kwargs: Additional kwargs to pass to the field.
:return: A :class:`tuple`, where the first element is a :class:`type` and the second is a :class:`pydantic.Field`.
"""
if isinstance(param.default, pydantic.fields.FieldInfo):
return (
param.annotation,
param.default
)
else:
return (
param.annotation,
pydantic.Field(
default=param.default if param.default else None,
title=param.name,
**kwargs,
),
)
def signature_to_model(f: Callable, __config__: pydantic.BaseConfig = ModelConfig, **extra_params):
"""
Convert the signature of a function to a pydantic model.
Arguments starting with ``_`` are ignored.
:param f: The function to use the signature of.
:param __config__: The config the pydantic model should use.
:param extra_params: Extra parameters to be added to the model.
:return: The created pydantic model.
"""
name: str = f.__name__
signature: inspect.Signature = inspect.signature(f)
params = {key: parameter_to_field(value) for key, value in signature.parameters if not key.startswith("_")}
model: Type[pydantic.BaseModel] = pydantic.create_model(name,
__config__=ModelConfig,
**params,
**extra_params)
return model
def function_with_model(__config__: pydantic.BaseConfig = ModelConfig, **extra_params):
"""
A decorator that adds the property ``.model`` to the wrapped function.
:param __config__: The config the pydantic model should use.
:param extra_params: Extra parameters to be added to the model.
"""
def decorator(f: Callable):
f.model = signature_to_model(f, __config__=__config__, **extra_params)
return f
return decorator
__all__ = (
"ModelConfig",
"parameter_to_field",
"signature_to_model",
"function_with_model",
)

View file

@ -0,0 +1,82 @@
import functools
import pydantic
from royalnet.types import *
from . import params
class EngineerRouter:
"""
A class that handles the registration and call of commands and the validation of their parameters.
"""
def __init__(self):
self.commands = {}
def add_command(self, f: Callable, name: Optional[str]):
"""
Add a command to the router.
:param name: The name of the command (``start``, ``settings``, etc). If not specified, it will use the name
of the wrapped function.
:param f: The function that should be executed when the command is called. It must have a ``.model`` property.
.. seealso:: :meth:`.command`, :func:`.params.function_with_model`
"""
name = name if name else f.__name__
f._model = params.signature_to_model(f)
self.commands[name] = f
def command(self, name: Optional[str] = None):
"""
A decorator factory to add a command to the router.
.. code-block:: python
@command()
def ping():
print("Pong!")
.. code-block:: python
@command(name="ping")
def xyzzy():
print("Pong!")
:param name: The name of the command (``start``, ``settings``, etc). If not specified, it will use the name
of the wrapped function.
:return: The decorated function.
.. seealso:: :meth:`.add_command`
"""
def decorator(f: Callable):
@functools.wraps(f)
@params.function_with_model()
def decorated(*args, **kwargs):
return f(*args, **kwargs)
self.add_command(f=decorated, name=name)
return decorated
return decorator
def call(self, __name: str, **kwargs):
model_params = {}
extra_params = {}
for key, value in kwargs.items():
if key.startswith("_"):
extra_params[key] = value
else:
model_params[key] = value
f = self.commands[__name]
# noinspection PyPep8Naming
Model: Type[pydantic.BaseModel] = f.model
model: pydantic.BaseModel = Model(**model_params)
return f(**model.dict(), **extra_params)
__all__ = (
"EngineerRouter",
)

View file

View file

@ -0,0 +1,40 @@
import pytest
import inspect
import pydantic
import pydantic.fields
import royalnet.engineer as re
@pytest.fixture
def a_random_function():
def f(big_f: str, _hidden: int) -> str:
return big_f
return f
def test_parameter_to_field(a_random_function):
signature = inspect.signature(a_random_function)
parameter = signature.parameters["big_f"]
fieldinfo = re.parameter_to_field(parameter)
assert isinstance(fieldinfo, pydantic.fields.FieldInfo)
assert fieldinfo.default == parameter.default == str
assert fieldinfo.title == parameter.name == "big_f"
def test_signature_to_model(a_random_function):
Model = re.signature_to_model(a_random_function)
assert callable(Model)
model = Model(big_f="banana")
assert isinstance(model, pydantic.BaseModel)
assert model.big_f == "banana"
assert model.dict() == {"big_f": "banana"}
with pytest.raises(pydantic.ValidationError):
Model()
with pytest.raises(pydantic.ValidationError):
Model(big_f="exists", _hidden="no")
with pytest.raises(pydantic.ValidationError):
Model(big_f=1)

View file