mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 03:24:20 +00:00
✨ Implement engineer module for commands routing
This commit is contained in:
parent
162049cb67
commit
7a6d65724e
10 changed files with 258 additions and 1 deletions
8
docs/source/autodoc/engineer.rst
Normal file
8
docs/source/autodoc/engineer.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
``engineer`` - Chat command router
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. currentmodule:: royalnet.engineer
|
||||||
|
.. automodule:: royalnet.engineer
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:imported-members:
|
|
@ -10,6 +10,7 @@ It may be incomplete or outdated, as it is automatically updated.
|
||||||
|
|
||||||
alchemist
|
alchemist
|
||||||
campaigns
|
campaigns
|
||||||
|
engineer
|
||||||
lazy
|
lazy
|
||||||
scrolls
|
scrolls
|
||||||
types
|
types
|
||||||
|
|
39
poetry.lock
generated
39
poetry.lock
generated
|
@ -150,6 +150,19 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
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]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.7.2"
|
version = "2.7.2"
|
||||||
|
@ -405,7 +418,7 @@ socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.0"
|
lock-version = "1.0"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "232cea18b2973090dfb3fe4215ef84e87abac749c8d0b50f556ec39610871cc5"
|
content-hash = "4363aefc0ea9322445ee375edce430712159829b5ca40561eea7cd74bbf0a7bd"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
|
@ -503,6 +516,30 @@ py = [
|
||||||
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
||||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
{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 = [
|
pygments = [
|
||||||
{file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
|
{file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
|
||||||
{file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},
|
{file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},
|
||||||
|
|
|
@ -9,6 +9,7 @@ license = "AGPL-3.0-or-later"
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
sqlalchemy = "^1.3.19"
|
sqlalchemy = "^1.3.19"
|
||||||
toml = "^0.10.1"
|
toml = "^0.10.1"
|
||||||
|
pydantic = "^1.7.3"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^6.1.1"
|
pytest = "^6.1.1"
|
||||||
|
|
6
royalnet/engineer/__init__.py
Normal file
6
royalnet/engineer/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
A chatbot command router inspired by :mod:`fastapi`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .params import *
|
||||||
|
from .router import *
|
82
royalnet/engineer/params.py
Normal file
82
royalnet/engineer/params.py
Normal 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",
|
||||||
|
)
|
82
royalnet/engineer/router.py
Normal file
82
royalnet/engineer/router.py
Normal 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",
|
||||||
|
)
|
0
royalnet/engineer/tests/__init__.py
Normal file
0
royalnet/engineer/tests/__init__.py
Normal file
40
royalnet/engineer/tests/test_params.py
Normal file
40
royalnet/engineer/tests/test_params.py
Normal 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)
|
0
royalnet/engineer/tests/test_router.py
Normal file
0
royalnet/engineer/tests/test_router.py
Normal file
Loading…
Reference in a new issue