diff --git a/pyproject.toml b/pyproject.toml index 15f86f34..f97e1dfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "royalnet" -version = "6.5.6" +version = "6.6.0" description = "A multipurpose bot framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0-or-later" diff --git a/royalnet/__init__.py b/royalnet/__init__.py new file mode 100644 index 00000000..644b6baf --- /dev/null +++ b/royalnet/__init__.py @@ -0,0 +1,15 @@ +""" +Royalnet is a collection of many Python modules useful to build chat-like applications. + +The modules currently are: + +- :mod:`.alchemist`, containing utility extensions to :mod:`sqlalchemy`; +- :mod:`.engineer`, containing a framework for building chat bots; +- :mod:`.lazy`, containing utilities to delay the evaluation of values until they are actually used; +- :mod:`.scrolls`, containing configuration utilities; +- :mod:`.sculptor`, containing common :mod:`pydantic` models for serialization and deserialization of data structures; + +To prevent the library from breaking if optional dependencies are not installed, this module does not export any object; submodules should be directly imported instead. + +All modules use exceptions based on :exc:`.exc.RoyalnetException`, and may subclass it to provide more detail on the errors. +""" diff --git a/royalnet/alchemist/func.py b/royalnet/alchemist/func.py index ff6a3db0..787be0ff 100644 --- a/royalnet/alchemist/func.py +++ b/royalnet/alchemist/func.py @@ -1,3 +1,7 @@ +""" +This submodule implements quick alias for some common SQL filtering functions. +""" + import sqlalchemy diff --git a/royalnet/alchemist/make.py b/royalnet/alchemist/make.py index 34c7e6bb..39c795ac 100644 --- a/royalnet/alchemist/make.py +++ b/royalnet/alchemist/make.py @@ -1,3 +1,7 @@ +""" +This submodule implements the :class:`.Makeable` mixin. +""" + from royalnet.royaltyping import * import sqlalchemy.orm as o diff --git a/royalnet/alchemist/repr.py b/royalnet/alchemist/repr.py index 2bc1482f..0ca24987 100644 --- a/royalnet/alchemist/repr.py +++ b/royalnet/alchemist/repr.py @@ -1,3 +1,8 @@ +""" +This submodule implements :func:`repr`\\ esentation mixins. +""" + + class ColRepr: """ A mixin that can be added to a declared class to display all columns of the table with their values in the diff --git a/royalnet/alchemist/update.py b/royalnet/alchemist/update.py index ad756525..a1bda23a 100644 --- a/royalnet/alchemist/update.py +++ b/royalnet/alchemist/update.py @@ -1,3 +1,7 @@ +""" +This module implements the :class:`.Updatable` mixin, along with the special class :class:`.DoNotUpdateType` and the singleton :data:`.DoNotUpdate`. +""" + class Updatable: """ A mixin that can be added to a declared class to add update methods, allowing attributes to be set from diff --git a/royalnet/engineer/pda/implementations/base.py b/royalnet/engineer/pda/implementations/base.py index e76bd5fc..ec6c2d8f 100644 --- a/royalnet/engineer/pda/implementations/base.py +++ b/royalnet/engineer/pda/implementations/base.py @@ -4,6 +4,7 @@ This module contains the base :class:`.PDAImplementation` and its basic implemen """ import royalnet.royaltyping as t +import royalnet.exc as exc import abc import sys import asyncio @@ -21,28 +22,28 @@ DispenserKey = t.Hashable class PDAImplementation(metaclass=abc.ABCMeta): """ - .. todo:: Document this. + An abstract class describing the interface of a PDA implementation. """ def __init__(self, name: str): self.name: str = f"{self.namespace}.{name}" """ - .. todo:: Document this. + The namespaced name of the PDA implementation. """ self.bound_to: t.Optional["PDA"] = None """ - .. todo:: Document this. + The PDA this implementation is bound to. """ self.logger_name: str = f"{__name__}.PDAImplementation.{self.namespace}.{name}" """ - .. todo:: Document this. + The namespaced name of the :class:`logging.Logger` that is being used by this PDA implementation. """ self.log: logging.Logger = logging.getLogger(self.logger_name) """ - .. todo:: Document this. + The :class:`logging.Logger` that is being used by this PDA implementation. """ def __repr__(self): @@ -53,7 +54,11 @@ class PDAImplementation(metaclass=abc.ABCMeta): def bind(self, pda: "PDA") -> None: """ - .. todo:: Document this. + Bind this PDA implementation to a specific PDA. + + Required for objects of this class to function. + + :raises .ImplemetationAlreadyBoundError: if the PDA implementation is already bound to a PDA. """ self.log.debug(f"Trying to bind to {pda!r}...") @@ -66,9 +71,9 @@ class PDAImplementation(metaclass=abc.ABCMeta): @property @abc.abstractmethod - def namespace(self): + def namespace(self) -> str: """ - .. todo:: Document this. + The namespace of this PDA implementation. """ raise NotImplementedError() @@ -82,15 +87,15 @@ class PDAImplementation(metaclass=abc.ABCMeta): raise NotImplementedError() -class ImplementationException(Exception): +class ImplementationException(exc.RoyalnetException, metaclass=abc.ABCMeta): """ - .. todo:: Document this. + Base class for exceptions raised by a PDA implementation. """ class ImplementationAlreadyBoundError(ImplementationException): """ - .. todo:: Document this. + The PDA implementation is already bound to a PDA. """ diff --git a/royalnet/engineer/router.py b/royalnet/engineer/router.py index 8817e7c6..bfc03393 100644 --- a/royalnet/engineer/router.py +++ b/royalnet/engineer/router.py @@ -14,21 +14,33 @@ log = logging.getLogger(__name__) class Router(c.Conversation, metaclass=abc.ABCMeta): """ - .. todo:: Document this. + A conversation which delegates event handling to other conversations by matching the contents of the first message received to one or multiple regexes. """ def __init__(self): - """ - .. todo:: Document this. - """ - self.by_pattern: dict[t.Pattern, t.ConversationProtocol] = {} - self.by_name: dict[str, t.ConversationProtocol] = {} - self.by_command: dict[t.ConversationProtocol, t.List[str]] = {} - - def register_conversation(self, conv: t.ConversationProtocol, names: t.List[str], patterns: t.List[t.Pattern]): """ - .. todo:: Document this. + A :class:`dict` mapping regex patterns to conversations registered with this router. + """ + + self.by_name: dict[str, t.ConversationProtocol] = {} + """ + A :class:`dict` mapping command names to conversations registered with this router. + """ + + self.by_command: dict[t.ConversationProtocol, t.List[str]] = {} + """ + A :class:`dict` mapping conversations registered with this router to lists of command names. + """ + + self.else_convs: list[t.ConversationProtocol] = [] + """ + A :class:`list` of conversations to delegate event handling to in case no other pattern is matched. + """ + + def register_conversation(self, conv: t.ConversationProtocol, names: t.List[str], patterns: t.List[t.Pattern]) -> None: + """ + Registers a new conversation with the :class:`.Router`, allowing it to run if one of the specified ``patterns`` is matched. """ log.debug(f"Registering {conv!r}...") @@ -45,10 +57,6 @@ class Router(c.Conversation, metaclass=abc.ABCMeta): self.by_pattern[pattern] = conv async def run(self, _sentry: s.Sentry, _conv: t.ConversationProtocol, **kwargs) -> None: - """ - .. todo:: Document this. - """ - dispenser = _sentry.dispenser() log.debug(f"Locking {dispenser!r}...") @@ -93,8 +101,16 @@ class Router(c.Conversation, metaclass=abc.ABCMeta): ) return else: - log.debug("No matches found") - + for conversation in self.else_convs: + log.debug(f"No matches found, running conversation {conversation}") + await conversation( + **kwargs, + _sentry=_sentry, + _conv=conversation, + _msg=msg, + _text=text, + _router=self, + ) __all__ = ( "Router", diff --git a/royalnet/engineer/sentry.py b/royalnet/engineer/sentry.py index 5d4fa462..2832054e 100644 --- a/royalnet/engineer/sentry.py +++ b/royalnet/engineer/sentry.py @@ -1,7 +1,6 @@ """ This module contains the :class:`.Sentry` class and its descendents :class:`SentryFilter` and :class:`SentrySource`\\ . - They support event filtering through Wrenches and coroutine functions. """ @@ -113,7 +112,7 @@ class Sentry(metaclass=abc.ABCMeta): if callable(wrench): return SentryFilter(previous=self, wrench=wrench) else: - raise TypeError("wrench must be either a Wrench or a coroutine function") + raise TypeError("wrench parameter must be either a Wrench or a coroutine function") def __or__(self, other: t.WrenchLike) -> SentryFilter: """ @@ -130,7 +129,7 @@ class Sentry(metaclass=abc.ABCMeta): try: return self.filter(other) except TypeError: - raise TypeError("Right-side must be either a Wrench or a coroutine function") + raise TypeError("Right-side of bitwise-or operator must be either a Wrench or a coroutine function") @abc.abstractmethod def dispenser(self) -> Dispenser: diff --git a/royalnet/exc.py b/royalnet/exc.py index c805ad87..b1106d9a 100644 --- a/royalnet/exc.py +++ b/royalnet/exc.py @@ -1,3 +1,8 @@ +""" +This module exports the base exceptions used in all :mod:`royalnet` modules. +""" + + class RoyalnetException(Exception): """An exception raised by a Royalnet module.""" diff --git a/royalnet/royaltyping.py b/royalnet/royaltyping.py index 85b69ab1..4d14457d 100644 --- a/royalnet/royaltyping.py +++ b/royalnet/royaltyping.py @@ -1,9 +1,11 @@ """ -This module adds some new common royaltyping to the default typing package. +This module defines adds some common types to the default :mod:`typing` module present in the standard library. -It should be imported with: :: +It is recommended to import it with *one* of the following statements:: + import royalnet.royaltyping as t from royalnet.royaltyping import * + from royalnet.royaltyping import """ @@ -45,4 +47,11 @@ class ConversationProtocol(Protocol): Args = Collection[Any] +""" +Any possible combination of positional arguments. +""" + Kwargs = Mapping[str, Any] +""" +Any possible combination of keyword arguments. +"""