mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-26 21:14:19 +00:00
📔 Document engineer module
This commit is contained in:
parent
d9c5545033
commit
74a376e65d
16 changed files with 335 additions and 205 deletions
|
@ -7,7 +7,21 @@
|
||||||
``bullet``
|
``bullet``
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. automodule:: royalnet.engineer.gunpowder
|
.. automodule:: royalnet.engineer.bullet
|
||||||
|
|
||||||
|
|
||||||
|
``contents``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. automodule:: royalnet.engineer.bullet.contents
|
||||||
|
:imported-members:
|
||||||
|
|
||||||
|
|
||||||
|
``projectiles``
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. automodule:: royalnet.engineer.bullet.projectiles
|
||||||
|
:imported-members:
|
||||||
|
|
||||||
|
|
||||||
``command``
|
``command``
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# import sys
|
# import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ copyright = '2020, Stefano Pigozzi'
|
||||||
author = 'Stefano Pigozzi'
|
author = 'Stefano Pigozzi'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '6.0.0a12'
|
release = pkg_resources.get_distribution("royalnet").version
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
@ -69,6 +70,7 @@ html_static_path = ['_static']
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"python": ("https://docs.python.org/3.8", None),
|
"python": ("https://docs.python.org/3.8", None),
|
||||||
"sqlalchemy": ("https://docs.sqlalchemy.org/en/13/", None),
|
"sqlalchemy": ("https://docs.sqlalchemy.org/en/13/", None),
|
||||||
|
"async_property": ("https://async-property.readthedocs.io/en/latest/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Casings are parts of the data model that :mod:`royalnet.engineer` uses to build a common interface between
|
This module contains the base :class:`.Casing` class.
|
||||||
different applications (implemented by individual *PDAs*).
|
|
||||||
|
|
||||||
They exclusively use coroutine functions to access data, as it may be required to fetch it from a remote location before
|
|
||||||
it is available.
|
|
||||||
|
|
||||||
**All** coroutine functions can have three different results:
|
|
||||||
|
|
||||||
- :exc:`.exc.CasingException` is raised, meaning that something went wrong during the data retrieval.
|
|
||||||
- :exc:`.exc.NotSupportedError` is raised, meaning that the frontend does not support the feature the requested data
|
|
||||||
is about (asking for :meth:`.Message.reply_to` in an IRC frontend, for example).
|
|
||||||
- :data:`None` is returned, meaning that there is no data in that field (if a message is not a reply to anything,
|
|
||||||
:meth:`Message.reply_to` will be :data:`None`.
|
|
||||||
- The data is returned.
|
|
||||||
|
|
||||||
To instantiate a new :class:`Bullet` from a bullet, you should use the methods of :attr:`.Bullet.mag`.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -24,7 +9,23 @@ import abc
|
||||||
|
|
||||||
class Casing(metaclass=abc.ABCMeta):
|
class Casing(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
The abstract base class for :mod:`~royalnet.engineer.casing` models.
|
:class:`.Casing`\\ s are parts of the data model that :mod:`royalnet.engineer` uses to build a common interface
|
||||||
|
between different applications (*PDA implementations*).
|
||||||
|
|
||||||
|
They use :func:`~async_property.async_property` to represent data, as it may be required to fetch it from a remote
|
||||||
|
location before it is available.
|
||||||
|
|
||||||
|
**All** their methods can have three different results:
|
||||||
|
|
||||||
|
- :exc:`.exc.CasingException` is raised, meaning that something went wrong during the data retrieval.
|
||||||
|
- :exc:`.exc.NotSupportedError` is raised, meaning that the frontend does not support the feature the requested
|
||||||
|
data is about (asking for :meth:`~royalnet.engineer.bullet.contents.message.Message.reply_to` in an SMS
|
||||||
|
implementation, for example).
|
||||||
|
|
||||||
|
- :data:`None` is returned, meaning that there is no data in that field (if a message is not a reply to anything,
|
||||||
|
:meth:`Message.reply_to` will be :data:`None`.
|
||||||
|
|
||||||
|
- The data is returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -35,6 +36,6 @@ class Casing(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""
|
"""
|
||||||
:return: A value that uniquely identifies the object in this Python interpreter process.
|
:return: A :class:`int` value that uniquely identifies the object in this Python interpreter process.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
The exceptions which can happen in bullets.
|
This modules contains the exceptions which can happen in bullets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .. import exc
|
from .. import exc
|
||||||
|
@ -19,11 +19,11 @@ class FrontendError(BulletException):
|
||||||
|
|
||||||
class NotSupportedError(FrontendError, NotImplementedError):
|
class NotSupportedError(FrontendError, NotImplementedError):
|
||||||
"""
|
"""
|
||||||
The requested property isn't available on the current frontend.
|
The requested property isn't available on the current implementation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ForbiddenError(FrontendError):
|
class ForbiddenError(FrontendError):
|
||||||
"""
|
"""
|
||||||
The bot user does not have sufficient permissions to perform a frontend operation.
|
The bot does not have sufficient permissions to perform an operation.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
"""
|
||||||
|
This module contains the base :class:`.Projectile` class.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
"""
|
||||||
|
This module contains the projectiles related to messages: :class:`.MessageReceived`, :class:`MessageEdited` and
|
||||||
|
:class:`MessageDeleted`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from ._imports import *
|
from ._imports import *
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
"""
|
||||||
|
This module contains the :class:`.Reaction` projectile.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from ._imports import *
|
from ._imports import *
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
"""
|
||||||
|
This module contains the projectiles related to user actions, such as :class:`.UserJoined`, :class:`.UserLeft` and
|
||||||
|
:class:`.UserUpdate`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from ._imports import *
|
from ._imports import *
|
||||||
|
|
||||||
|
|
|
@ -1,90 +1,129 @@
|
||||||
# Module docstring
|
"""
|
||||||
|
This module contains the :class:`.FullCommand` and :class:`.PartialCommand` classes, and all
|
||||||
|
:class:`.CommandException`\\ s.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Special imports
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import royalnet.royaltyping as t
|
import royalnet.royaltyping as t
|
||||||
|
|
||||||
# External imports
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Internal imports
|
|
||||||
from . import conversation as c
|
from . import conversation as c
|
||||||
from . import sentry as s
|
from . import sentry as s
|
||||||
from . import bullet as b
|
from . import bullet as b
|
||||||
from . import teleporter as tp
|
from . import teleporter as tp
|
||||||
|
from . import exc
|
||||||
|
|
||||||
# Special global objects
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Code
|
class CommandException(exc.EngineerException):
|
||||||
class Command(c.Conversation):
|
|
||||||
"""
|
"""
|
||||||
A Command is a :class:`~.conversation.Conversation` which is started by a :class:`~.bullet.Message` having a text
|
The base :class:`Exception` of the :mod:`royalnet.engineer.command`\\ module.
|
||||||
matching the :attr:`.pattern`; the named capture groups of the pattern are then passed as keyword arguments to
|
|
||||||
:attr:`.f`.
|
|
||||||
|
|
||||||
.. info:: Usually you don't directly instantiate commands, you define :class:`.PartialCommand`\\ s in packs which
|
|
||||||
are later completed by a runner.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, f: c.ConversationProtocol, *, names: t.List[str] = None, pattern: re.Pattern, lock: bool = True):
|
|
||||||
|
class CommandNameException(CommandException):
|
||||||
"""
|
"""
|
||||||
Create a new :class:`.Command` .
|
An :class:`Exception` raised because the :attr:`.FullCommand.names` were invalid.
|
||||||
"""
|
"""
|
||||||
log.debug(f"Teleporting function: {f!r}")
|
|
||||||
|
|
||||||
|
class MissingNameError(ValueError, CommandNameException):
|
||||||
|
"""
|
||||||
|
:class:`.FullCommand`\\ s must have at least one name, but no names were passed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class NotAlphanumericNameError(ValueError, CommandNameException):
|
||||||
|
"""
|
||||||
|
All :attr:`.FullCommand.names` should be alphanumeric (a-z 0-9).
|
||||||
|
|
||||||
|
.. seealso:: :meth:`str.isalnum`
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FullCommand(c.Conversation):
|
||||||
|
"""
|
||||||
|
A :class:`.FullCommand` is a :class:`~royalnet.engineer.conversation.Conversation` which is started by a
|
||||||
|
:class:`~royalnet.engineer.projectiles.Projectile` having a text matching the :attr:`.pattern`;
|
||||||
|
the named capture groups of the pattern are then passed as teleported keyword arguments to :attr:`.f`.
|
||||||
|
|
||||||
|
.. note:: If you're creating a command pack, you shouldn't instantiate directly :class:`.FullCommand` objects, but
|
||||||
|
you should use :class:`.PartialCommand`\\ s instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, f: c.ConversationProtocol, *, names: t.List[str], pattern: re.Pattern, lock: bool = True):
|
||||||
|
"""
|
||||||
|
Instantiate a new :class:`.FullCommand`\\ .
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.plain_f = f
|
||||||
|
"""
|
||||||
|
A reference to the unteleported function :attr:`.f`\\ .
|
||||||
|
"""
|
||||||
|
|
||||||
teleported = tp.Teleporter(f, validate_input=True, validate_output=False)
|
teleported = tp.Teleporter(f, validate_input=True, validate_output=False)
|
||||||
|
|
||||||
log.debug(f"Initializing Conversation with: {teleported!r}")
|
|
||||||
super().__init__(teleported)
|
super().__init__(teleported)
|
||||||
|
|
||||||
self.names: t.List[str] = names if names else []
|
if len(names) < 1:
|
||||||
|
raise MissingNameError(f"Passed 'names' list is empty", names)
|
||||||
|
|
||||||
|
self.names: t.List[str] = names
|
||||||
"""
|
"""
|
||||||
The names of the command, as a :class:`list` of :class:`str`.
|
The names of the command, as a :class:`list` of :class:`str`.
|
||||||
|
|
||||||
The first element of the list is the "main" name, and will be displayed in help messages.
|
The first element of the list is the primary :attr:`.name`, and will be displayed in help messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.pattern: re.Pattern = pattern
|
self.pattern: re.Pattern = pattern
|
||||||
"""
|
"""
|
||||||
The pattern that should be matched by the command.
|
The pattern that should be matched by the text of the first
|
||||||
|
:class:`~royalnet.engineer.bullet.projectiles.message.MessageReceived` by the
|
||||||
|
:class:`~royalnet.engineer.conversation.Conversation`\\ .
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.lock: bool = lock
|
self.lock: bool = lock
|
||||||
"""
|
"""
|
||||||
If calling this command should :meth:`~royalnet.engineer.dispenser.Dispenser.lock` the dispenser.
|
If calling this command should :meth:`~royalnet.engineer.dispenser.Dispenser.lock` the calling dispenser.
|
||||||
|
|
||||||
|
Locked dispensers cannot run any other :class:`~royalnet.engineer.conversation.Conversation`\\ s but the one
|
||||||
|
which locked it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
"""
|
"""
|
||||||
:return: The main name of the Command.
|
:return: The primary name of the :class:`.FullCommand`.
|
||||||
"""
|
"""
|
||||||
return self.names[0]
|
return self.names[0]
|
||||||
|
|
||||||
def aliases(self):
|
def aliases(self):
|
||||||
"""
|
"""
|
||||||
:return: The aliases (non-main names) of the Command.
|
:return: The secondary names of the :class:`.FullCommand`.
|
||||||
"""
|
"""
|
||||||
return self.names[1:]
|
return self.names[1:]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if (nc := len(self.names)) > 1:
|
plus = f" + {nc-1} other names" if (nc := len(self.names)) > 1 else ""
|
||||||
plus = f" + {nc-1} other names"
|
|
||||||
else:
|
|
||||||
plus = ""
|
|
||||||
return f"<{self.__class__.__qualname__}: {self.name()!r}{plus}>"
|
return f"<{self.__class__.__qualname__}: {self.name()!r}{plus}>"
|
||||||
|
|
||||||
async def run(self, *, _sentry: s.Sentry, **base_kwargs) -> t.Optional[c.ConversationProtocol]:
|
async def run(self, *, _sentry: s.Sentry, **base_kwargs) -> t.Optional[c.ConversationProtocol]:
|
||||||
|
"""
|
||||||
|
Run the command as if it was a conversation.
|
||||||
|
|
||||||
|
:param _sentry: The :class:`~royalnet.engineer.sentry.Sentry` to use for the conversation.
|
||||||
|
:param base_kwargs: Keyword arguments to pass to the wrapped function :attr:`.f` .
|
||||||
|
:return: The result of the wrapped function :attr:`.f` , or :data:`None` if the first projectile received does
|
||||||
|
not satisfy the requirements of the command.
|
||||||
|
"""
|
||||||
|
|
||||||
log.debug(f"Awaiting a bullet...")
|
log.debug(f"Awaiting a bullet...")
|
||||||
projectile: b.Projectile = await _sentry
|
projectile: b.Projectile = await _sentry
|
||||||
|
|
||||||
log.debug(f"Received: {projectile!r}")
|
log.debug(f"Received: {projectile!r}")
|
||||||
|
|
||||||
log.debug(f"Ensuring a message was received: {projectile!r}")
|
log.debug(f"Ensuring the projectile is a MessageReceived: {projectile!r}")
|
||||||
if not isinstance(projectile, b.MessageReceived):
|
if not isinstance(projectile, b.MessageReceived):
|
||||||
log.debug(f"Returning: {projectile!r} is not a message")
|
log.debug(f"Returning: {projectile!r} is not a message")
|
||||||
return
|
return
|
||||||
|
@ -112,7 +151,7 @@ class Command(c.Conversation):
|
||||||
|
|
||||||
with _sentry.dispenser().lock(self):
|
with _sentry.dispenser().lock(self):
|
||||||
log.debug(f"Passing args to function: {message_kwargs!r}")
|
log.debug(f"Passing args to function: {message_kwargs!r}")
|
||||||
return await super().run(
|
return await super().__call__(
|
||||||
_sentry=_sentry,
|
_sentry=_sentry,
|
||||||
_proj=projectile,
|
_proj=projectile,
|
||||||
_msg=msg,
|
_msg=msg,
|
||||||
|
@ -123,7 +162,7 @@ class Command(c.Conversation):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug(f"Passing args to function: {message_kwargs!r}")
|
log.debug(f"Passing args to function: {message_kwargs!r}")
|
||||||
return await super().run(
|
return await super().__call__(
|
||||||
_sentry=_sentry,
|
_sentry=_sentry,
|
||||||
_proj=projectile,
|
_proj=projectile,
|
||||||
_msg=msg,
|
_msg=msg,
|
||||||
|
@ -143,21 +182,28 @@ class Command(c.Conversation):
|
||||||
|
|
||||||
class PartialCommand:
|
class PartialCommand:
|
||||||
"""
|
"""
|
||||||
A PartialCommand is a :class:`.Command` having an unknown :attr:`~.Command.name` and :attr:`~.Command.pattern` at
|
:class:`.PartialCommand` is a class meant for **building command packs**.
|
||||||
the moment of creation.
|
|
||||||
|
|
||||||
They can specified later using :meth:`.complete`.
|
It provides mostly the same interface as a :class:`.FullCommand`, but some fields are unknown until the command is
|
||||||
|
registered in a PDA.
|
||||||
|
|
||||||
|
The missing fields currently are:
|
||||||
|
- :attr:`~.FullCommand.name`
|
||||||
|
- :attr:`~.FullCommand.pattern`
|
||||||
|
|
||||||
|
At the moment of registration in a PDA, :meth:`.complete` is called, converting it in a :class:`.FullCommand`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, f: c.ConversationProtocol, syntax: str, lock: bool = True):
|
def __init__(self, f: c.ConversationProtocol, syntax: str, lock: bool = True):
|
||||||
"""
|
"""
|
||||||
Create a new :class:`.PartialCommand` .
|
Instantiate a new :class:`.PartialCommand`\\ .
|
||||||
|
|
||||||
.. seealso:: :meth:`.new`
|
.. seealso:: The corresponding decorator form, :meth:`.new`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.f: c.ConversationProtocol = f
|
self.f: c.ConversationProtocol = f
|
||||||
"""
|
"""
|
||||||
The function to pass to :attr:`.c.Conversation.f`.
|
The function to pass to be called when the command is executed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.syntax: str = syntax
|
self.syntax: str = syntax
|
||||||
|
@ -173,11 +219,20 @@ class PartialCommand:
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, *args, **kwargs):
|
def new(cls, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
A decorator that instantiates a new :class:`Conversation` object using the decorated function.
|
Decorator factory for creating new :class:`.PartialCommand` with the decorator syntax::
|
||||||
|
|
||||||
:return: The created :class:`Conversation` object.
|
>>> import royalnet.engineer as engi
|
||||||
It can still be called in the same way as the previous function!
|
|
||||||
|
>>> @PartialCommand.new(syntax="")
|
||||||
|
... async def ping(*, _sentry: engi.Sentry, _msg: engi.Message, **__):
|
||||||
|
... await _msg.reply(text="🏓 Pong!")
|
||||||
|
|
||||||
|
>>> ping
|
||||||
|
<PartialCommand <function ping at ...>>
|
||||||
|
|
||||||
|
:return: The decorator to wrap around the :attr:`.f` function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: c.ConversationProtocol):
|
def decorator(f: c.ConversationProtocol):
|
||||||
partial_command = cls(f=f, *args, **kwargs)
|
partial_command = cls(f=f, *args, **kwargs)
|
||||||
log.debug(f"Created: {partial_command!r}")
|
log.debug(f"Created: {partial_command!r}")
|
||||||
|
@ -185,36 +240,42 @@ class PartialCommand:
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def complete(self, *, names: t.List[str] = None, pattern: str) -> Command:
|
def complete(self, *, names: t.List[str], pattern: str) -> FullCommand:
|
||||||
"""
|
"""
|
||||||
Complete the :class:`.PartialCommand` with names and a pattern, creating a :class:`Command` object.
|
Complete the :class:`.PartialCommand` with the missing fields, creating a :class:`.FullCommand` object.
|
||||||
|
|
||||||
:param names: The names of the command. See :attr:`.Command.names` .
|
:param names: The :attr:`~.FullCommand.names` that the :class:`.FullCommand` should have.
|
||||||
:param pattern: The pattern to add to the PartialCommand. It is first :meth:`~str.format`\\ ted with the keyword
|
:param pattern: The pattern to add to the :class:`.PartialCommand`\\ .
|
||||||
arguments ``name`` and ``syntax`` and later :func:`~re.compile`\\ d with the
|
It is first :meth:`~str.format`\\ ted with the keyword arguments ``name`` and ``syntax``
|
||||||
:data:`re.IGNORECASE` flag.
|
and later :func:`~re.compile`\\ d with the :data:`re.IGNORECASE` flag.
|
||||||
:return: The complete :class:`Command`.
|
|
||||||
|
:return: The completed :class:`.FullCommand`.
|
||||||
|
|
||||||
|
:raises .MissingNameError: If no :attr:`.FullCommand.names` are specified.
|
||||||
"""
|
"""
|
||||||
if names is None:
|
|
||||||
names = []
|
|
||||||
if len(names) < 1:
|
if len(names) < 1:
|
||||||
raise ValueError("Commands must have at least one name.")
|
raise MissingNameError(f"Passed 'names' list is empty", names)
|
||||||
for name in names:
|
for name in names:
|
||||||
if not name.isalnum():
|
if not name.isalnum():
|
||||||
raise ValueError(f"Name is not alphanumeric: {name!r}")
|
raise NotAlphanumericNameError(f"Name is not alphanumeric", name)
|
||||||
|
|
||||||
|
log.debug(f"")
|
||||||
name_regex = f"(?:{'|'.join(names)})"
|
name_regex = f"(?:{'|'.join(names)})"
|
||||||
log.debug(f"Completed pattern: {name_regex!r}")
|
log.debug(f"Completed pattern: {name_regex!r}")
|
||||||
|
|
||||||
pattern: re.Pattern = re.compile(pattern.format(name=name_regex, syntax=self.syntax), re.IGNORECASE)
|
pattern: re.Pattern = re.compile(pattern.format(name=name_regex, syntax=self.syntax), re.IGNORECASE)
|
||||||
return Command(f=self.f, names=names, pattern=pattern, lock=self.lock)
|
return FullCommand(f=self.f, names=names, pattern=pattern, lock=self.lock)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__} {self.f!r}>"
|
return f"<{self.__class__.__qualname__} {self.f!r}>"
|
||||||
|
|
||||||
|
|
||||||
# Objects exported by this module
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Command",
|
"CommandException",
|
||||||
|
"CommandNameException",
|
||||||
|
"FullCommand",
|
||||||
|
"MissingNameError",
|
||||||
|
"NotAlphanumericNameError",
|
||||||
"PartialCommand",
|
"PartialCommand",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
# Module docstring
|
|
||||||
"""
|
"""
|
||||||
Conversations are wrapper classes that can be applied to functions which await
|
This module contains :class:`.ConversationProtocol`, the typing stub for conversation functions, and
|
||||||
:class:`~royalnet.engineer.bullet.Bullet`\\ s from a :class:`~royalnet.engineer.sentry.Sentry`.
|
:class:`.Conversation`, a decorator for functions which should help in debugging conversations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Special imports
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import royalnet.royaltyping as t
|
import royalnet.royaltyping as t
|
||||||
|
|
||||||
# External imports
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Internal imports
|
|
||||||
from . import sentry as s
|
from . import sentry as s
|
||||||
|
|
||||||
|
|
||||||
# Special global objects
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Code
|
|
||||||
class ConversationProtocol(t.Protocol):
|
class ConversationProtocol(t.Protocol):
|
||||||
"""
|
"""
|
||||||
Typing annotation for Conversation functions.
|
Typing stub for :class:`.Conversation`\\ -compatible functions.
|
||||||
"""
|
"""
|
||||||
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||||
...
|
...
|
||||||
|
@ -30,15 +23,20 @@ class ConversationProtocol(t.Protocol):
|
||||||
|
|
||||||
class Conversation:
|
class Conversation:
|
||||||
"""
|
"""
|
||||||
The base class for Conversations. It does nothing on its own except providing better debug information.
|
:class:`.Conversation`\\ s are functions which await
|
||||||
|
:class:`~royalnet.engineer.bullet.projectiles._base.Projectile`\\ s incoming from a
|
||||||
|
:class:`~royalnet.engineer.sentry.Sentry` .
|
||||||
|
|
||||||
|
This class is callable ( :meth:`.__call__` ), and can be used in place of plain functions to have better debug
|
||||||
|
information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, f: ConversationProtocol, *_, **__):
|
def __init__(self, f: ConversationProtocol, *_, **__):
|
||||||
self.f: ConversationProtocol = f
|
self.f: ConversationProtocol = f
|
||||||
"""
|
"""
|
||||||
The function that was wrapped by this conversation.
|
The function that is wrapped by this class.
|
||||||
|
|
||||||
It can be called by calling this object as it was a function::
|
It can be called by calling this object as if it was a function::
|
||||||
|
|
||||||
my_conv(_sentry=_sentry, _msg=msg)
|
my_conv(_sentry=_sentry, _msg=msg)
|
||||||
"""
|
"""
|
||||||
|
@ -46,7 +44,14 @@ class Conversation:
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, *args, **kwargs):
|
def new(cls, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
A decorator that instantiates a new :class:`Conversation` object using the decorated function.
|
Decorator factory for creating new :class:`.PartialCommand` with the decorator syntax::
|
||||||
|
|
||||||
|
>>> @Conversation.new()
|
||||||
|
... def my_conv(*, _sentry: s.Sentry, **__):
|
||||||
|
... pass
|
||||||
|
|
||||||
|
>>> my_conv
|
||||||
|
<Conversation #1234>
|
||||||
|
|
||||||
:return: The created :class:`Conversation` object.
|
:return: The created :class:`Conversation` object.
|
||||||
It can still be called in the same way as the previous function!
|
It can still be called in the same way as the previous function!
|
||||||
|
@ -59,16 +64,7 @@ class Conversation:
|
||||||
|
|
||||||
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||||
log.debug(f"Calling: {self!r}")
|
log.debug(f"Calling: {self!r}")
|
||||||
return self.run(_sentry=_sentry, **kwargs)
|
return self.f(_sentry=_sentry, **kwargs)
|
||||||
|
|
||||||
async def run(self, *, _sentry: s.Sentry, **kwargs) -> t.Optional[ConversationProtocol]:
|
|
||||||
"""
|
|
||||||
The coroutine function that generates the coroutines returned by :meth:`.__call__` .
|
|
||||||
|
|
||||||
It is a :class:`Conversation` itself.
|
|
||||||
"""
|
|
||||||
log.debug(f"Running: {self!r}")
|
|
||||||
return await self.f(_sentry=_sentry, **kwargs)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__} #{id(self)}>"
|
return f"<{self.__class__.__qualname__} #{id(self)}>"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
"""
|
||||||
|
This module contains the :class:`.Discard` special exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Discard(BaseException):
|
class Discard(BaseException):
|
||||||
"""
|
"""
|
||||||
A special exception which should be raised by :class:`~royalnet.engineer.wrench.Wrench`\\ es if a certain object
|
A special exception which should be raised by :class:`~royalnet.engineer.wrench.Wrench`\\ es if a certain object
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Dispensers instantiate sentries and dispatch events in bulk to the whole group.
|
This module contains the :class:`.Dispenser` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -10,13 +10,36 @@ import contextlib
|
||||||
|
|
||||||
from .sentry import SentrySource
|
from .sentry import SentrySource
|
||||||
from .conversation import Conversation
|
from .conversation import Conversation
|
||||||
from .exc import LockedDispenserError
|
from .exc import EngineerException
|
||||||
from .bullet.projectiles import Projectile
|
from .bullet.projectiles import Projectile
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DispenserException(EngineerException):
|
||||||
|
"""
|
||||||
|
The base class for errors in :mod:`royalnet.engineer.dispenser`\\ .
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class LockedDispenserError(DispenserException):
|
||||||
|
"""
|
||||||
|
The :class:`.Dispenser` couldn't start a new :class:`~royalnet.engineer.conversation.Conversation` as it is
|
||||||
|
currently :attr:`.Dispenser.lock`\\ ed.
|
||||||
|
"""
|
||||||
|
def __init__(self, locked_by, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self.locked_by = locked_by
|
||||||
|
|
||||||
|
|
||||||
class Dispenser:
|
class Dispenser:
|
||||||
|
"""
|
||||||
|
A :class:`.Dispenser` is an object which instantiates multiple :class:`~royalnet.engineer.sentry.Sentry` and
|
||||||
|
multiplexes and distributes incoming :class:`~royalnet.engineer.bullet.projectiles._base.Projectile`\\ s to them.
|
||||||
|
|
||||||
|
They usually represent a single "conversation channel" with the bot: either a chat channel, or an user.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sentries: t.List[SentrySource] = []
|
self.sentries: t.List[SentrySource] = []
|
||||||
"""
|
"""
|
||||||
|
@ -32,47 +55,48 @@ class Dispenser:
|
||||||
|
|
||||||
async def put(self, item: Projectile) -> None:
|
async def put(self, item: Projectile) -> None:
|
||||||
"""
|
"""
|
||||||
Insert a new projectile in the queues of all the running sentries.
|
Insert a new :class:`~royalnet.engineer.bullet.projectiles._base.Projectile` in the queues of all the
|
||||||
|
running :attr:`.sentries`.
|
||||||
|
|
||||||
:param item: The projectile to insert.
|
:param item: The :class:`~royalnet.engineer.bullet.projectiles._base.Projectile` to insert.
|
||||||
"""
|
"""
|
||||||
log.debug(f"Putting {item}...")
|
log.debug(f"Putting {item!r}...")
|
||||||
for sentry in self.sentries:
|
for sentry in self.sentries:
|
||||||
await sentry.put(item)
|
await sentry.put(item)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def sentry(self, *args, **kwargs):
|
def sentry(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
A context manager which creates a :class:`.SentrySource` and keeps it in :attr:`.sentries` while it is being
|
A :func:`~contextlib.contextmanager` which creates a :class:`.SentrySource` and keeps it in :attr:`.sentries`
|
||||||
used.
|
while it is being used.
|
||||||
"""
|
"""
|
||||||
log.debug("Creating a new SentrySource...")
|
log.debug("Creating a new SentrySource...")
|
||||||
sentry = SentrySource(dispenser=self, *args, **kwargs)
|
sentry = SentrySource(dispenser=self, *args, **kwargs)
|
||||||
|
|
||||||
log.debug(f"Adding: {sentry}")
|
log.debug(f"Adding: {sentry!r}")
|
||||||
self.sentries.append(sentry)
|
self.sentries.append(sentry)
|
||||||
|
|
||||||
log.debug(f"Yielding: {sentry}")
|
log.debug(f"Yielding: {sentry!r}")
|
||||||
yield sentry
|
yield sentry
|
||||||
|
|
||||||
log.debug(f"Removing from the sentries list: {sentry}")
|
log.debug(f"Removing from the sentries list: {sentry!r}")
|
||||||
self.sentries.remove(sentry)
|
self.sentries.remove(sentry)
|
||||||
|
|
||||||
async def run(self, conv: Conversation, **kwargs) -> None:
|
async def run(self, conv: Conversation, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Run the passed conversation.
|
Run a :class:`~royalnet.engineer.conversation.Conversation`\\ .
|
||||||
|
|
||||||
:param conv: The conversation to run.
|
:param conv: The :class:`~royalnet.engineer.conversation.Conversation` to run.
|
||||||
:raises .LockedDispenserError: If the dispenser is currently :attr:`.locked_by` a :class:`.Conversation`.
|
:raises .LockedDispenserError: If the dispenser is currently :attr:`.locked_by` a :class:`.Conversation`.
|
||||||
"""
|
"""
|
||||||
log.debug(f"Trying to run: {conv!r}")
|
log.debug(f"Trying to run: {conv!r}")
|
||||||
|
|
||||||
if self._locked_by:
|
if self._locked_by:
|
||||||
log.debug(f"Dispenser is locked by {self._locked_by!r}, refusing to run {conv!r}")
|
log.debug(f"Dispenser is locked by {self._locked_by!r}, refusing to run {conv!r}")
|
||||||
raise LockedDispenserError(self._locked_by, f"The Dispenser is currently locked by {self._locked_by!r} and "
|
raise LockedDispenserError(
|
||||||
f"cannot start new conversations.")
|
f"The Dispenser is currently locked and cannot start any new Conversation.", self._locked_by)
|
||||||
|
|
||||||
log.debug(f"Running: {conv}")
|
log.debug(f"Running: {conv!r}")
|
||||||
with self.sentry() as sentry:
|
with self.sentry() as sentry:
|
||||||
state = conv(_sentry=sentry, **kwargs)
|
state = conv(_sentry=sentry, **kwargs)
|
||||||
|
|
||||||
|
@ -83,10 +107,10 @@ class Dispenser:
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def lock(self, conv: Conversation):
|
def lock(self, conv: Conversation):
|
||||||
"""
|
"""
|
||||||
Lock the dispenser while this :func:`~contextlib.contextmanager` is in scope.
|
Lock the :class:`.Dispenser` while this :func:`~contextlib.contextmanager` is in scope.
|
||||||
|
|
||||||
A locked dispenser will refuse to :meth:`.run` any new conversations, raising :exc:`.exc.LockedDispenserError`
|
A locked :class:`.Dispenser` will refuse to :meth:`.run` any new conversations,
|
||||||
instead.
|
raising :exc:`.LockedDispenserError` instead.
|
||||||
|
|
||||||
:param conv: The conversation that requested the lock.
|
:param conv: The conversation that requested the lock.
|
||||||
|
|
||||||
|
@ -104,4 +128,6 @@ class Dispenser:
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Dispenser",
|
"Dispenser",
|
||||||
|
"DispenserException",
|
||||||
|
"LockedDispenserError",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import pydantic
|
"""
|
||||||
|
This module contains the base :class:`.EngineerException` .
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class EngineerException(Exception):
|
class EngineerException(Exception):
|
||||||
|
@ -7,58 +9,6 @@ class EngineerException(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class WrenchException(EngineerException):
|
|
||||||
"""
|
|
||||||
The base class for errors in :mod:`royalnet.engineer.wrench`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DeliberateException(WrenchException):
|
|
||||||
"""
|
|
||||||
This exception was deliberately raised by :class:`royalnet.engineer.wrench.ErrorAll`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TeleporterError(EngineerException, pydantic.ValidationError):
|
|
||||||
"""
|
|
||||||
The base class for errors in :mod:`royalnet.engineer.teleporter`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class InTeleporterError(TeleporterError):
|
|
||||||
"""
|
|
||||||
The input parameters validation failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class OutTeleporterError(TeleporterError):
|
|
||||||
"""
|
|
||||||
The return value validation failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DispenserException(EngineerException):
|
|
||||||
"""
|
|
||||||
The base class for errors in :mod:`royalnet.engineer.dispenser`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class LockedDispenserError(DispenserException):
|
|
||||||
"""
|
|
||||||
The dispenser couldn't start a new conversation as it is currently locked.
|
|
||||||
"""
|
|
||||||
def __init__(self, locked_by, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
self.locked_by = locked_by
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"EngineerException",
|
"EngineerException",
|
||||||
"WrenchException",
|
|
||||||
"DeliberateException",
|
|
||||||
"TeleporterError",
|
|
||||||
"InTeleporterError",
|
|
||||||
"OutTeleporterError",
|
|
||||||
"DispenserException",
|
|
||||||
"LockedDispenserError",
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Sentries are asynchronous receivers for events (usually :class:`bullet.Projectile`) incoming from Dispensers.
|
This module contains the :class:`.Sentry` class and its descendents :class:`SentryFilter` and :class:`SentrySource`\\ .
|
||||||
|
|
||||||
|
|
||||||
They support event filtering through Wrenches and coroutine functions.
|
They support event filtering through Wrenches and coroutine functions.
|
||||||
"""
|
"""
|
||||||
|
@ -22,7 +23,13 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Sentry(metaclass=abc.ABCMeta):
|
class Sentry(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
The abstract object representing a node of the pipeline.
|
A :class:`.Sentry` is an asynchronous receiver for :class:`~royalnet.engineer.bullet.projectiles._base.Projectile`
|
||||||
|
incoming from :class:`~royalnet.engineer.dispenser.Dispenser`\\ s.
|
||||||
|
|
||||||
|
Sentries can be chained together to form a filtering pipeline, starting with a :class:`.SentrySource` followed by
|
||||||
|
zero or more :class:`.SentryFilter`\\ s.
|
||||||
|
|
||||||
|
This abstract base class represents a single node of the pipeline.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -32,11 +39,14 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_nowait(self):
|
def get_nowait(self):
|
||||||
"""
|
"""
|
||||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, without blocking or handling discards.
|
Try to get a single :class:`~royalnet.engineer.bullet.projectiles._base.Projectile` from the pipeline,
|
||||||
|
**without blocking** or **handling :class:`~royalnet.engineer.discard.Discard`\\ s** .
|
||||||
|
|
||||||
|
:return: The **returned** :class:`~royalnet.engineer.bullet.projectiles._base.Projectile`.
|
||||||
|
|
||||||
:return: The **returned** :class:`~.bullet.Projectile`.
|
|
||||||
:raises asyncio.QueueEmpty: If the queue is empty.
|
:raises asyncio.QueueEmpty: If the queue is empty.
|
||||||
:raises .discard.Discard: If the object was **discarded** by the pipeline.
|
:raises .discard.Discard: If the object was **:class:`~royalnet.engineer.discard.Discard`\\ ed** by
|
||||||
|
the pipeline.
|
||||||
:raises Exception: If an exception was **raised** in the pipeline.
|
:raises Exception: If an exception was **raised** in the pipeline.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -44,10 +54,12 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def get(self):
|
async def get(self):
|
||||||
"""
|
"""
|
||||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, blocking until something is available, but
|
Try to get a single :class:`~royalnet.engineer.bullet.projectiles._base.Projectile` from the pipeline,
|
||||||
without handling discards.
|
**blocking** until something is available, but
|
||||||
|
**without handling :class:`~royalnet.engineer.discard.Discard`\\ s**.
|
||||||
|
|
||||||
|
:return: The **returned** :class:`~royalnet.engineer.bullet.projectiles._base.Projectile`.
|
||||||
|
|
||||||
:return: The **returned** :class:`~.bullet.Projectile`.
|
|
||||||
:raises .discard.Discard: If the object was **discarded** by the pipeline.
|
:raises .discard.Discard: If the object was **discarded** by the pipeline.
|
||||||
:raises Exception: If an exception was **raised** in the pipeline.
|
:raises Exception: If an exception was **raised** in the pipeline.
|
||||||
"""
|
"""
|
||||||
|
@ -55,10 +67,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
async def wait(self):
|
async def wait(self):
|
||||||
"""
|
"""
|
||||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, blocking until something is available and is
|
Try to get a single :class:`~.bullet.Projectile` from the pipeline, **blocking** until something is available
|
||||||
not discarded.
|
and is **not discarded**.
|
||||||
|
|
||||||
:return: The **returned** :class:`~.bullet.Projectile`.
|
:return: The **returned** :class:`~.bullet.Projectile`.
|
||||||
|
|
||||||
:raises Exception: If an exception was **raised** in the pipeline.
|
:raises Exception: If an exception was **raised** in the pipeline.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
|
@ -72,7 +85,7 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
def __await__(self):
|
def __await__(self):
|
||||||
"""
|
"""
|
||||||
Awaiting an object implementing :class:`.SentryInterface` corresponds to awaiting :meth:`.wait`.
|
Awaiting an object implementing :class:`.Sentry` corresponds to awaiting :meth:`.wait`.
|
||||||
"""
|
"""
|
||||||
return self.get().__await__()
|
return self.get().__await__()
|
||||||
|
|
||||||
|
@ -89,9 +102,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
Chain a new filter to the pipeline.
|
Chain a new filter to the pipeline.
|
||||||
|
|
||||||
:param wrench: The filter to add to the chain. It can either be a :class:`.wrench.Wrench`, or a coroutine
|
:param wrench: The filter to add to the chain. It can either be a :class:`~royalnet.engineer.wrench.Wrench`,
|
||||||
function accepting a single object as parameter and returning the same or a different one.
|
or a coroutine function accepting a single object as parameter and returning another one.
|
||||||
:return: A new :class:`.SentryFilter` which includes the filter.
|
:return: The resulting :class:`.SentryFilter`\\ .
|
||||||
|
:raises TypeError: If the right side operator is neither a :class:`~royalnet.engineer.wrench.Wrench` or a
|
||||||
|
coroutine function.
|
||||||
|
|
||||||
.. seealso:: :meth:`.__or__`
|
.. seealso:: :meth:`.__or__`
|
||||||
"""
|
"""
|
||||||
|
@ -106,8 +121,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
await (sentry | wrench.Type(Message) | wrench.Sync(lambda o: o.text))
|
await (sentry | wrench.Type(engi.MessageReceived) | wrench.Lambda(lambda o: o.text))
|
||||||
|
|
||||||
|
:return: The resulting :class:`.SentryFilter`\\ .
|
||||||
|
:raises TypeError: If the right side operator is neither a :class:`~royalnet.engineer.wrench.Wrench` or a
|
||||||
|
coroutine function.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.filter(other)
|
return self.filter(other)
|
||||||
|
@ -117,9 +135,9 @@ class Sentry(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def dispenser(self) -> Dispenser:
|
def dispenser(self) -> Dispenser:
|
||||||
"""
|
"""
|
||||||
Get the :class:`.Dispenser` that created this Sentry.
|
Get the :class:`~royalnet.engineer.dispenser.Dispenser` that created this :class:`.Sentry`.
|
||||||
|
|
||||||
:return: The :class:`.Dispenser` object.
|
:return: The :class:`~royalnet.engineer.dispenser.Dispenser` object.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
The teleporter uses :mod:`pydantic` to validate function parameters and return values.
|
This module contains the :class:`.Teleporter` class and its exceptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -15,7 +15,29 @@ Value = t.TypeVar("Value")
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TeleporterError(exc.EngineerException, pydantic.ValidationError):
|
||||||
|
"""
|
||||||
|
The base class for errors in :mod:`royalnet.engineer.teleporter`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InTeleporterError(TeleporterError):
|
||||||
|
"""
|
||||||
|
The input parameters validation failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OutTeleporterError(TeleporterError):
|
||||||
|
"""
|
||||||
|
The return value validation failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Teleporter:
|
class Teleporter:
|
||||||
|
"""
|
||||||
|
A :class:`.Teleporter` is a function wrapper which uses :mod:`pydantic` to perform type checking
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
f: t.Callable[..., t.Any],
|
f: t.Callable[..., t.Any],
|
||||||
validate_input: bool = True,
|
validate_input: bool = True,
|
||||||
|
@ -165,14 +187,14 @@ class Teleporter:
|
||||||
|
|
||||||
:param kwargs: The keyword arguments that should be passed to the model.
|
:param kwargs: The keyword arguments that should be passed to the model.
|
||||||
:return: The created model.
|
:return: The created model.
|
||||||
:raises .exc.InTeleporterError: If the kwargs fail the validation.
|
:raises .InTeleporterError: If the kwargs fail the validation.
|
||||||
"""
|
"""
|
||||||
log.debug(f"Teleporting in: {kwargs!r}")
|
log.debug(f"Teleporting in: {kwargs!r}")
|
||||||
try:
|
try:
|
||||||
return self.InputModel(**kwargs)
|
return self.InputModel(**kwargs)
|
||||||
except pydantic.ValidationError as e:
|
except pydantic.ValidationError as e:
|
||||||
log.error(f"Teleport in failed: {e!r}")
|
log.error(f"Teleport in failed: {e!r}")
|
||||||
raise exc.InTeleporterError(errors=e.raw_errors, model=e.model)
|
raise InTeleporterError(errors=e.raw_errors, model=e.model)
|
||||||
|
|
||||||
def teleport_out(self, value: Value) -> pydantic.BaseModel:
|
def teleport_out(self, value: Value) -> pydantic.BaseModel:
|
||||||
"""
|
"""
|
||||||
|
@ -180,14 +202,14 @@ class Teleporter:
|
||||||
|
|
||||||
:param value: The value that should be validated.
|
:param value: The value that should be validated.
|
||||||
:return: The created model.
|
:return: The created model.
|
||||||
:raises .exc.OutTeleporterError: If the value fails the validation.
|
:raises .OutTeleporterError: If the value fails the validation.
|
||||||
"""
|
"""
|
||||||
log.debug(f"Teleporting out: {value!r}")
|
log.debug(f"Teleporting out: {value!r}")
|
||||||
try:
|
try:
|
||||||
return self.OutputModel(__root__=value)
|
return self.OutputModel(__root__=value)
|
||||||
except pydantic.ValidationError as e:
|
except pydantic.ValidationError as e:
|
||||||
log.error(f"Teleport out failed: {e!r}")
|
log.error(f"Teleport out failed: {e!r}")
|
||||||
raise exc.OutTeleporterError(errors=e.raw_errors, model=e.model)
|
raise OutTeleporterError(errors=e.raw_errors, model=e.model)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _split_kwargs(**kwargs) -> t.Tuple[t.Dict[str, t.Any], t.Dict[str, t.Any]]:
|
def _split_kwargs(**kwargs) -> t.Tuple[t.Dict[str, t.Any], t.Dict[str, t.Any]]:
|
||||||
|
@ -212,7 +234,7 @@ class Teleporter:
|
||||||
|
|
||||||
def _run(self, **kwargs) -> t.Any:
|
def _run(self, **kwargs) -> t.Any:
|
||||||
"""
|
"""
|
||||||
Run the teleporter synchronously.
|
Run the :class:`.Teleporter` synchronously.
|
||||||
"""
|
"""
|
||||||
if self.InputModel:
|
if self.InputModel:
|
||||||
log.debug("Validating input...")
|
log.debug("Validating input...")
|
||||||
|
@ -226,7 +248,7 @@ class Teleporter:
|
||||||
|
|
||||||
async def _run_async(self, **kwargs) -> t.Awaitable[t.Any]:
|
async def _run_async(self, **kwargs) -> t.Awaitable[t.Any]:
|
||||||
"""
|
"""
|
||||||
Run the teleporter asynchronously.
|
Run the :class:`.Teleporter` asynchronously.
|
||||||
"""
|
"""
|
||||||
if self.InputModel:
|
if self.InputModel:
|
||||||
log.debug("Validating input...")
|
log.debug("Validating input...")
|
||||||
|
@ -246,5 +268,8 @@ class Teleporter:
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"InTeleporterError",
|
||||||
|
"OutTeleporterError",
|
||||||
"Teleporter",
|
"Teleporter",
|
||||||
|
"TeleporterError",
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,18 @@ from . import discard
|
||||||
from . import exc
|
from . import exc
|
||||||
|
|
||||||
|
|
||||||
|
class WrenchException(exc.EngineerException):
|
||||||
|
"""
|
||||||
|
The base class for errors in :mod:`royalnet.engineer.wrench`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeliberateException(WrenchException):
|
||||||
|
"""
|
||||||
|
This exception was deliberately raised by :class:`.ErrorAll`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Wrench(metaclass=abc.ABCMeta):
|
class Wrench(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
The abstract base class for Wrenches.
|
The abstract base class for Wrenches.
|
||||||
|
@ -67,7 +79,7 @@ class ErrorAll(Wrench):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def filter(self, obj: t.Any) -> t.Any:
|
async def filter(self, obj: t.Any) -> t.Any:
|
||||||
raise exc.DeliberateException("ErrorAll received an object")
|
raise DeliberateException("ErrorAll received an object")
|
||||||
|
|
||||||
|
|
||||||
class CheckBase(Wrench, metaclass=abc.ABCMeta):
|
class CheckBase(Wrench, metaclass=abc.ABCMeta):
|
||||||
|
@ -293,15 +305,17 @@ class Check(CheckBase):
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Wrench",
|
"Check",
|
||||||
"CheckBase",
|
"CheckBase",
|
||||||
"Type",
|
|
||||||
"StartsWith",
|
|
||||||
"EndsWith",
|
|
||||||
"Choice",
|
"Choice",
|
||||||
|
"DeliberateException",
|
||||||
|
"EndsWith",
|
||||||
|
"Lambda",
|
||||||
"RegexCheck",
|
"RegexCheck",
|
||||||
"RegexMatch",
|
"RegexMatch",
|
||||||
"RegexReplace",
|
"RegexReplace",
|
||||||
"Lambda",
|
"StartsWith",
|
||||||
"Check",
|
"Type",
|
||||||
|
"Wrench",
|
||||||
|
"WrenchException",
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue