1
Fork 0
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:
Steffo 2021-04-07 13:27:25 +02:00
parent d9c5545033
commit 74a376e65d
Signed by: steffo
GPG key ID: 6965406171929D01
16 changed files with 335 additions and 205 deletions

View file

@ -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``

View file

@ -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),
}

View file

@ -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()

View file

@ -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.
"""

View file

@ -1,3 +1,7 @@
"""
This module contains the base :class:`.Projectile` class.
"""
from __future__ import annotations
import abc

View file

@ -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 *

View file

@ -1,3 +1,7 @@
"""
This module contains the :class:`.Reaction` projectile.
"""
from __future__ import annotations
from ._imports import *

View file

@ -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 *

View file

@ -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",
)

View file

@ -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)}>"

View file

@ -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

View file

@ -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",
)

View file

@ -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",
)

View file

@ -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()

View file

@ -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",
)

View file

@ -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",
)