diff --git a/docs/source/autodoc/alchemist.rst b/docs/source/autodoc/alchemist.rst index 9ef253ed..8c148660 100644 --- a/docs/source/autodoc/alchemist.rst +++ b/docs/source/autodoc/alchemist.rst @@ -1,7 +1,4 @@ ``alchemist`` - SQLAlchemy utilities ==================================== -.. currentmodule:: royalnet.alchemist .. automodule:: royalnet.alchemist - :members: - :undoc-members: diff --git a/docs/source/autodoc/campaigns.rst b/docs/source/autodoc/campaigns.rst index 3e6ee453..ad9a5a0d 100644 --- a/docs/source/autodoc/campaigns.rst +++ b/docs/source/autodoc/campaigns.rst @@ -1,7 +1,4 @@ ``campaigns`` - Conversation state manager ========================================== -.. currentmodule:: royalnet.campaigns .. automodule:: royalnet.campaigns - :members: - :undoc-members: diff --git a/docs/source/autodoc/engineer.rst b/docs/source/autodoc/engineer.rst index 56a90eb2..c1c39669 100644 --- a/docs/source/autodoc/engineer.rst +++ b/docs/source/autodoc/engineer.rst @@ -1,8 +1,34 @@ -``engineer`` - Chat command router +``engineer`` - Chatbot framework ================================== -.. currentmodule:: royalnet.engineer .. automodule:: royalnet.engineer - :members: - :undoc-members: - :imported-members: + + +``blueprints`` - ABCs for common chat entities +---------------------------------------------- + +.. automodule:: royalnet.engineer.blueprints + + +``teleporter`` - Function parameter validation +---------------------------------------------- + +.. automodule:: royalnet.engineer.teleporter + + +``sentry`` - Async queue +---------------------------------------------- + +.. automodule:: royalnet.engineer.sentry + + +``dispenser`` - Function parameter validation +---------------------------------------------- + +.. automodule:: royalnet.engineer.dispenser + + +``exc`` - Exceptions +---------------------------------------------- + +.. automodule:: royalnet.engineer.exc diff --git a/docs/source/autodoc/lazy.rst b/docs/source/autodoc/lazy.rst index f51ef51c..d8856c05 100644 --- a/docs/source/autodoc/lazy.rst +++ b/docs/source/autodoc/lazy.rst @@ -3,7 +3,4 @@ .. note:: The documentation for this section hasn't been written yet. -.. currentmodule:: royalnet.lazy .. automodule:: royalnet.lazy - :members: - :undoc-members: diff --git a/docs/source/autodoc/royaltyping.rst b/docs/source/autodoc/royaltyping.rst index fe4670d7..f5b9f1e2 100644 --- a/docs/source/autodoc/royaltyping.rst +++ b/docs/source/autodoc/royaltyping.rst @@ -1,7 +1,4 @@ ``royaltyping`` - Additional type information ============================================= -.. currentmodule:: royalnet.royaltyping .. automodule:: royalnet.royaltyping - :members: - :undoc-members: diff --git a/docs/source/autodoc/scrolls.rst b/docs/source/autodoc/scrolls.rst index d7a9b888..bb9ba601 100644 --- a/docs/source/autodoc/scrolls.rst +++ b/docs/source/autodoc/scrolls.rst @@ -1,7 +1,4 @@ ``scrolls`` - Configuration loader ================================== -.. currentmodule:: royalnet.scrolls .. automodule:: royalnet.scrolls - :members: - :undoc-members: diff --git a/docs/source/conf.py b/docs/source/conf.py index ce676d70..fcc5a94e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -73,12 +73,21 @@ intersphinx_mapping = { # -- Setup function ---------------------------------------------------------- def setup(app): - app.connect("autodoc-skip-member", skip) app.add_css_file('royalblue.css') -# -- Skip function ----------------------------------------------------------- -def skip(app, what, name: str, obj, would_skip, options): - if name == "__init__" or name == "__getitem__" or name == "__getattr__": - return not bool(obj.__doc__) - return would_skip +# -- Substitutions ----------------------------------------------------------- + + +rst_prolog = """ + +""" + +# -- Automodule settings ----------------------------------------------------- + +autodoc_default_options = { + 'members': True, + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': True, +} diff --git a/royalnet/engineer/__init__.py b/royalnet/engineer/__init__.py index 61a8cc68..d688bb27 100644 --- a/royalnet/engineer/__init__.py +++ b/royalnet/engineer/__init__.py @@ -1,5 +1,10 @@ """ A chatbot command router inspired by :mod:`fastapi`. + +All names are inspired by the `Engineer Class of Team Fortress 2 `_. """ +from .blueprints import * from .teleporter import * +from .sentry import * +from .exc import * diff --git a/royalnet/engineer/blueprints.py b/royalnet/engineer/blueprints.py new file mode 100644 index 00000000..eb91d248 --- /dev/null +++ b/royalnet/engineer/blueprints.py @@ -0,0 +1,81 @@ +from __future__ import annotations +from royalnet.royaltyping import * +import abc +import datetime +import functools + +from . import exc + + +class Message(metaclass=abc.ABCMeta): + """ + An abstract class representing a generic chat message sent in any platform. + + To implement it for a specific platform, override :meth:`__hash__` and the methods returning information that the + platform sometimes provides, either returning the value or raising :exc:`.exc.NotAvailableError`. + + All properties are cached using :func:`functools.lru_cache`, so that if they are successful, they are executed only + one time. + """ + + @abc.abstractmethod + def __hash__(self): + """ + :return: A value that uniquely identifies the message inside this Python process. + """ + raise NotImplementedError() + + def requires(self, *fields) -> None: + """ + Ensure that this message has the specified fields, raising the highest priority exception between all the + fields. + + .. code-block:: + + def print_msg(message: Message): + message.requires(Message.text, Message.timestamp) + print(f"{message.timestamp().isoformat()}: {message.text()}") + + :raises .exc.NeverAvailableError: If at least one of the fields raised a :exc:`.exc.NeverAvailableError`. + :raises .exc.NotAvailableError: If no field raised a :exc:`.exc.NeverAvailableError`, but at least one raised a + :exc:`.exc.NotAvailableError`. + """ + + exceptions = [] + + for field in fields: + try: + field(self) + except exc.NeverAvailableError as ex: + exceptions.append(ex) + except exc.NotAvailableError as ex: + exceptions.append(ex) + + if len(exceptions) > 0: + raise max(exceptions, key=lambda e: e.priority) + + @functools.lru_cache() + def text(self) -> str: + """ + :return: The raw text contents of the message. + :raises .exc.NeverAvailableError: If the chat platform does not support text messages. + :raises .exc.NotAvailableError: If this message does not have any text. + """ + raise exc.NeverAvailableError() + + @functools.lru_cache() + def timestamp(self) -> datetime.datetime: + """ + :return: The :class:`datetime.datetime` at which the message was sent / received. + :raises .exc.NeverAvailableError: If the chat platform does not support timestamps. + :raises .exc.NotAvailableError: If this message is special and does not have any timestamp. + """ + raise exc.NeverAvailableError() + + @functools.lru_cache() + def reply_to(self) -> Message: + """ + :return: The :class:`.Message` this message is a reply to. + :raises .exc.NeverAvailableError: If the chat platform does not support replies. + :raises .exc.NotAvailableError: If this message is not a reply to any other message. + """ diff --git a/royalnet/engineer/dispenser.py b/royalnet/engineer/dispenser.py new file mode 100644 index 00000000..e69de29b diff --git a/royalnet/engineer/exc.py b/royalnet/engineer/exc.py index e577a687..39b1eb0d 100644 --- a/royalnet/engineer/exc.py +++ b/royalnet/engineer/exc.py @@ -8,6 +8,28 @@ class EngineerException(royalnet.exc.RoyalnetException): """ +class BlueprintError(EngineerException): + """ + An error related to the :mod:`royalnet.engineer.blueprints`. + """ + + +class NeverAvailableError(BlueprintError, NotImplementedError): + """ + The requested property is never supplied by the chat platform the message was sent in. + """ + + priority = -1 + + +class NotAvailableError(BlueprintError): + """ + The requested property was not supplied by the chat platform for the specific message this exception was raised in. + """ + + priority = -2 + + class TeleporterError(EngineerException, pydantic.ValidationError): """ The validation of some object though a :mod:`pydantic` model failed. diff --git a/royalnet/engineer/sentry.py b/royalnet/engineer/sentry.py new file mode 100644 index 00000000..8b3d3acd --- /dev/null +++ b/royalnet/engineer/sentry.py @@ -0,0 +1,21 @@ +from royalnet.royaltyping import * +import logging +import asyncio + +log = logging.getLogger(__name__) + + +class Sentry: + """ + A class that allows using the ``await`` keyword to suspend a command execution until a new message is received. + """ + + def __init__(self): + self.queue = asyncio.queues.Queue() + + def __repr__(self): + return f"" + + async def wait_for_item(self) -> Any: + log.debug("Waiting for an item...") + return await self.queue.get()