mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 03:24:20 +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``
|
||||
----------
|
||||
|
||||
.. 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``
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
import pkg_resources
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
|
@ -23,7 +24,7 @@ copyright = '2020, Stefano Pigozzi'
|
|||
author = 'Stefano Pigozzi'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '6.0.0a12'
|
||||
release = pkg_resources.get_distribution("royalnet").version
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
@ -69,6 +70,7 @@ html_static_path = ['_static']
|
|||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3.8", 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
|
||||
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`.
|
||||
This module contains the base :class:`.Casing` class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
@ -24,7 +9,23 @@ import abc
|
|||
|
||||
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):
|
||||
|
@ -35,6 +36,6 @@ class Casing(metaclass=abc.ABCMeta):
|
|||
@abc.abstractmethod
|
||||
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()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
The exceptions which can happen in bullets.
|
||||
This modules contains the exceptions which can happen in bullets.
|
||||
"""
|
||||
|
||||
from .. import exc
|
||||
|
@ -19,11 +19,11 @@ class FrontendError(BulletException):
|
|||
|
||||
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):
|
||||
"""
|
||||
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
|
||||
|
||||
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 ._imports import *
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
This module contains the :class:`.Reaction` projectile.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
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 ._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
|
||||
import royalnet.royaltyping as t
|
||||
|
||||
# External imports
|
||||
import logging
|
||||
import re
|
||||
|
||||
# Internal imports
|
||||
from . import conversation as c
|
||||
from . import sentry as s
|
||||
from . import bullet as b
|
||||
from . import teleporter as tp
|
||||
from . import exc
|
||||
|
||||
# Special global objects
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Code
|
||||
class Command(c.Conversation):
|
||||
class CommandException(exc.EngineerException):
|
||||
"""
|
||||
A Command is a :class:`~.conversation.Conversation` which is started by a :class:`~.bullet.Message` having a text
|
||||
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.
|
||||
The base :class:`Exception` of the :mod:`royalnet.engineer.command`\\ module.
|
||||
"""
|
||||
|
||||
def __init__(self, f: c.ConversationProtocol, *, names: t.List[str] = None, pattern: re.Pattern, lock: bool = True):
|
||||
|
||||
class CommandNameException(CommandException):
|
||||
"""
|
||||
An :class:`Exception` raised because the :attr:`.FullCommand.names` were invalid.
|
||||
"""
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Create a new :class:`.Command` .
|
||||
Instantiate a new :class:`.FullCommand`\\ .
|
||||
"""
|
||||
log.debug(f"Teleporting function: {f!r}")
|
||||
|
||||
self.plain_f = f
|
||||
"""
|
||||
A reference to the unteleported function :attr:`.f`\\ .
|
||||
"""
|
||||
|
||||
teleported = tp.Teleporter(f, validate_input=True, validate_output=False)
|
||||
|
||||
log.debug(f"Initializing Conversation with: {teleported!r}")
|
||||
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 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
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
:return: The main name of the Command.
|
||||
:return: The primary name of the :class:`.FullCommand`.
|
||||
"""
|
||||
return self.names[0]
|
||||
|
||||
def aliases(self):
|
||||
"""
|
||||
:return: The aliases (non-main names) of the Command.
|
||||
:return: The secondary names of the :class:`.FullCommand`.
|
||||
"""
|
||||
return self.names[1:]
|
||||
|
||||
def __repr__(self):
|
||||
if (nc := len(self.names)) > 1:
|
||||
plus = f" + {nc-1} other names"
|
||||
else:
|
||||
plus = ""
|
||||
plus = f" + {nc-1} other names" if (nc := len(self.names)) > 1 else ""
|
||||
return f"<{self.__class__.__qualname__}: {self.name()!r}{plus}>"
|
||||
|
||||
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...")
|
||||
projectile: b.Projectile = await _sentry
|
||||
|
||||
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):
|
||||
log.debug(f"Returning: {projectile!r} is not a message")
|
||||
return
|
||||
|
@ -112,7 +151,7 @@ class Command(c.Conversation):
|
|||
|
||||
with _sentry.dispenser().lock(self):
|
||||
log.debug(f"Passing args to function: {message_kwargs!r}")
|
||||
return await super().run(
|
||||
return await super().__call__(
|
||||
_sentry=_sentry,
|
||||
_proj=projectile,
|
||||
_msg=msg,
|
||||
|
@ -123,7 +162,7 @@ class Command(c.Conversation):
|
|||
|
||||
else:
|
||||
log.debug(f"Passing args to function: {message_kwargs!r}")
|
||||
return await super().run(
|
||||
return await super().__call__(
|
||||
_sentry=_sentry,
|
||||
_proj=projectile,
|
||||
_msg=msg,
|
||||
|
@ -143,21 +182,28 @@ class Command(c.Conversation):
|
|||
|
||||
class PartialCommand:
|
||||
"""
|
||||
A PartialCommand is a :class:`.Command` having an unknown :attr:`~.Command.name` and :attr:`~.Command.pattern` at
|
||||
the moment of creation.
|
||||
:class:`.PartialCommand` is a class meant for **building command packs**.
|
||||
|
||||
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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
@ -173,11 +219,20 @@ class PartialCommand:
|
|||
@classmethod
|
||||
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.
|
||||
It can still be called in the same way as the previous function!
|
||||
>>> import royalnet.engineer as engi
|
||||
|
||||
>>> @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):
|
||||
partial_command = cls(f=f, *args, **kwargs)
|
||||
log.debug(f"Created: {partial_command!r}")
|
||||
|
@ -185,36 +240,42 @@ class PartialCommand:
|
|||
|
||||
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 pattern: The pattern to add to the PartialCommand. It is first :meth:`~str.format`\\ ted with the keyword
|
||||
arguments ``name`` and ``syntax`` and later :func:`~re.compile`\\ d with the
|
||||
:data:`re.IGNORECASE` flag.
|
||||
:return: The complete :class:`Command`.
|
||||
:param names: The :attr:`~.FullCommand.names` that the :class:`.FullCommand` should have.
|
||||
:param pattern: The pattern to add to the :class:`.PartialCommand`\\ .
|
||||
It is first :meth:`~str.format`\\ ted with the keyword arguments ``name`` and ``syntax``
|
||||
and later :func:`~re.compile`\\ d with the :data:`re.IGNORECASE` flag.
|
||||
|
||||
:return: The completed :class:`.FullCommand`.
|
||||
|
||||
:raises .MissingNameError: If no :attr:`.FullCommand.names` are specified.
|
||||
"""
|
||||
if names is None:
|
||||
names = []
|
||||
|
||||
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:
|
||||
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)})"
|
||||
log.debug(f"Completed pattern: {name_regex!r}")
|
||||
|
||||
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):
|
||||
return f"<{self.__class__.__qualname__} {self.f!r}>"
|
||||
|
||||
|
||||
# Objects exported by this module
|
||||
__all__ = (
|
||||
"Command",
|
||||
"CommandException",
|
||||
"CommandNameException",
|
||||
"FullCommand",
|
||||
"MissingNameError",
|
||||
"NotAlphanumericNameError",
|
||||
"PartialCommand",
|
||||
)
|
||||
|
|
|
@ -1,28 +1,21 @@
|
|||
# Module docstring
|
||||
"""
|
||||
Conversations are wrapper classes that can be applied to functions which await
|
||||
:class:`~royalnet.engineer.bullet.Bullet`\\ s from a :class:`~royalnet.engineer.sentry.Sentry`.
|
||||
This module contains :class:`.ConversationProtocol`, the typing stub for conversation functions, and
|
||||
:class:`.Conversation`, a decorator for functions which should help in debugging conversations.
|
||||
"""
|
||||
|
||||
# Special imports
|
||||
from __future__ import annotations
|
||||
import royalnet.royaltyping as t
|
||||
|
||||
# External imports
|
||||
import logging
|
||||
|
||||
# Internal imports
|
||||
from . import sentry as s
|
||||
|
||||
|
||||
# Special global objects
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Code
|
||||
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]]:
|
||||
...
|
||||
|
@ -30,15 +23,20 @@ class ConversationProtocol(t.Protocol):
|
|||
|
||||
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, *_, **__):
|
||||
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)
|
||||
"""
|
||||
|
@ -46,7 +44,14 @@ class Conversation:
|
|||
@classmethod
|
||||
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.
|
||||
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]]:
|
||||
log.debug(f"Calling: {self!r}")
|
||||
return self.run(_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)
|
||||
return self.f(_sentry=_sentry, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__qualname__} #{id(self)}>"
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
"""
|
||||
This module contains the :class:`.Discard` special exception.
|
||||
"""
|
||||
|
||||
|
||||
class Discard(BaseException):
|
||||
"""
|
||||
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
|
||||
|
@ -10,13 +10,36 @@ import contextlib
|
|||
|
||||
from .sentry import SentrySource
|
||||
from .conversation import Conversation
|
||||
from .exc import LockedDispenserError
|
||||
from .exc import EngineerException
|
||||
from .bullet.projectiles import Projectile
|
||||
|
||||
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:
|
||||
"""
|
||||
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):
|
||||
self.sentries: t.List[SentrySource] = []
|
||||
"""
|
||||
|
@ -32,47 +55,48 @@ class Dispenser:
|
|||
|
||||
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:
|
||||
await sentry.put(item)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def sentry(self, *args, **kwargs):
|
||||
"""
|
||||
A context manager which creates a :class:`.SentrySource` and keeps it in :attr:`.sentries` while it is being
|
||||
used.
|
||||
A :func:`~contextlib.contextmanager` which creates a :class:`.SentrySource` and keeps it in :attr:`.sentries`
|
||||
while it is being used.
|
||||
"""
|
||||
log.debug("Creating a new SentrySource...")
|
||||
sentry = SentrySource(dispenser=self, *args, **kwargs)
|
||||
|
||||
log.debug(f"Adding: {sentry}")
|
||||
log.debug(f"Adding: {sentry!r}")
|
||||
self.sentries.append(sentry)
|
||||
|
||||
log.debug(f"Yielding: {sentry}")
|
||||
log.debug(f"Yielding: {sentry!r}")
|
||||
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)
|
||||
|
||||
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`.
|
||||
"""
|
||||
log.debug(f"Trying to run: {conv!r}")
|
||||
|
||||
if self._locked_by:
|
||||
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 "
|
||||
f"cannot start new conversations.")
|
||||
raise LockedDispenserError(
|
||||
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:
|
||||
state = conv(_sentry=sentry, **kwargs)
|
||||
|
||||
|
@ -83,10 +107,10 @@ class Dispenser:
|
|||
@contextlib.contextmanager
|
||||
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`
|
||||
instead.
|
||||
A locked :class:`.Dispenser` will refuse to :meth:`.run` any new conversations,
|
||||
raising :exc:`.LockedDispenserError` instead.
|
||||
|
||||
:param conv: The conversation that requested the lock.
|
||||
|
||||
|
@ -104,4 +128,6 @@ class Dispenser:
|
|||
|
||||
__all__ = (
|
||||
"Dispenser",
|
||||
"DispenserException",
|
||||
"LockedDispenserError",
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import pydantic
|
||||
"""
|
||||
This module contains the base :class:`.EngineerException` .
|
||||
"""
|
||||
|
||||
|
||||
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__ = (
|
||||
"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.
|
||||
"""
|
||||
|
@ -22,7 +23,13 @@ log = logging.getLogger(__name__)
|
|||
|
||||
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
|
||||
|
@ -32,11 +39,14 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
@abc.abstractmethod
|
||||
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 .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.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
@ -44,10 +54,12 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
@abc.abstractmethod
|
||||
async def get(self):
|
||||
"""
|
||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, blocking until something is available, but
|
||||
without handling discards.
|
||||
Try to get a single :class:`~royalnet.engineer.bullet.projectiles._base.Projectile` from the pipeline,
|
||||
**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 Exception: If an exception was **raised** in the pipeline.
|
||||
"""
|
||||
|
@ -55,10 +67,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
|
||||
async def wait(self):
|
||||
"""
|
||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, blocking until something is available and is
|
||||
not discarded.
|
||||
Try to get a single :class:`~.bullet.Projectile` from the pipeline, **blocking** until something is available
|
||||
and is **not discarded**.
|
||||
|
||||
:return: The **returned** :class:`~.bullet.Projectile`.
|
||||
|
||||
:raises Exception: If an exception was **raised** in the pipeline.
|
||||
"""
|
||||
while True:
|
||||
|
@ -72,7 +85,7 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
|
||||
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__()
|
||||
|
||||
|
@ -89,9 +102,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
"""
|
||||
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
|
||||
function accepting a single object as parameter and returning the same or a different one.
|
||||
:return: A new :class:`.SentryFilter` which includes the filter.
|
||||
:param wrench: The filter to add to the chain. It can either be a :class:`~royalnet.engineer.wrench.Wrench`,
|
||||
or a coroutine function accepting a single object as parameter and returning another one.
|
||||
: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__`
|
||||
"""
|
||||
|
@ -106,8 +121,11 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
|
||||
.. 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:
|
||||
return self.filter(other)
|
||||
|
@ -117,9 +135,9 @@ class Sentry(metaclass=abc.ABCMeta):
|
|||
@abc.abstractmethod
|
||||
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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -15,7 +15,29 @@ Value = t.TypeVar("Value")
|
|||
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:
|
||||
"""
|
||||
A :class:`.Teleporter` is a function wrapper which uses :mod:`pydantic` to perform type checking
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
f: t.Callable[..., t.Any],
|
||||
validate_input: bool = True,
|
||||
|
@ -165,14 +187,14 @@ class Teleporter:
|
|||
|
||||
:param kwargs: The keyword arguments that should be passed to the 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}")
|
||||
try:
|
||||
return self.InputModel(**kwargs)
|
||||
except pydantic.ValidationError as e:
|
||||
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:
|
||||
"""
|
||||
|
@ -180,14 +202,14 @@ class Teleporter:
|
|||
|
||||
:param value: The value that should be validated.
|
||||
: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}")
|
||||
try:
|
||||
return self.OutputModel(__root__=value)
|
||||
except pydantic.ValidationError as e:
|
||||
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
|
||||
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:
|
||||
"""
|
||||
Run the teleporter synchronously.
|
||||
Run the :class:`.Teleporter` synchronously.
|
||||
"""
|
||||
if self.InputModel:
|
||||
log.debug("Validating input...")
|
||||
|
@ -226,7 +248,7 @@ class Teleporter:
|
|||
|
||||
async def _run_async(self, **kwargs) -> t.Awaitable[t.Any]:
|
||||
"""
|
||||
Run the teleporter asynchronously.
|
||||
Run the :class:`.Teleporter` asynchronously.
|
||||
"""
|
||||
if self.InputModel:
|
||||
log.debug("Validating input...")
|
||||
|
@ -246,5 +268,8 @@ class Teleporter:
|
|||
|
||||
|
||||
__all__ = (
|
||||
"InTeleporterError",
|
||||
"OutTeleporterError",
|
||||
"Teleporter",
|
||||
"TeleporterError",
|
||||
)
|
||||
|
|
|
@ -12,6 +12,18 @@ from . import discard
|
|||
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):
|
||||
"""
|
||||
The abstract base class for Wrenches.
|
||||
|
@ -67,7 +79,7 @@ class ErrorAll(Wrench):
|
|||
"""
|
||||
|
||||
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):
|
||||
|
@ -293,15 +305,17 @@ class Check(CheckBase):
|
|||
|
||||
|
||||
__all__ = (
|
||||
"Wrench",
|
||||
"Check",
|
||||
"CheckBase",
|
||||
"Type",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Choice",
|
||||
"DeliberateException",
|
||||
"EndsWith",
|
||||
"Lambda",
|
||||
"RegexCheck",
|
||||
"RegexMatch",
|
||||
"RegexReplace",
|
||||
"Lambda",
|
||||
"Check",
|
||||
"StartsWith",
|
||||
"Type",
|
||||
"Wrench",
|
||||
"WrenchException",
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue