mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-26 21:14:19 +00:00
💥 Cooler filters!
This commit is contained in:
parent
0a7607c3c3
commit
b44290bd98
3 changed files with 140 additions and 76 deletions
|
@ -56,7 +56,7 @@ class Blueprint(metaclass=abc.ABCMeta):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def requires(self, *fields) -> None:
|
||||
def requires(self, *fields) -> True:
|
||||
"""
|
||||
Ensure that this blueprint has the specified fields, re-raising the highest priority exception raised between
|
||||
all of them.
|
||||
|
@ -85,6 +85,8 @@ class Blueprint(metaclass=abc.ABCMeta):
|
|||
if len(exceptions) > 0:
|
||||
raise max(exceptions, key=lambda e: e.priority)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Blueprint",
|
||||
|
|
|
@ -38,84 +38,159 @@ class Filter:
|
|||
return result
|
||||
|
||||
@staticmethod
|
||||
def _deco_type(t: type):
|
||||
def _deco_filter(c: Callable[[Any], bool], *, error: str):
|
||||
"""
|
||||
A decorator which checks the condition ``c`` on all objects transiting through the queue:
|
||||
- If the check **passes**, the object itself is returned;
|
||||
- If the check **fails**, :exc:`.exc.Discard` is raised, with the object and the ``error`` string as parameters;
|
||||
- If an error is raised, propagate the error upwards.
|
||||
|
||||
.. warning:: Raising :exc:`.exc.Discard` in ``c`` will automatically cause the object to be discarded, as if
|
||||
:data:`False` was returned.
|
||||
|
||||
:param c: A function that takes in input an enqueued object and returns either the same object or a new one to
|
||||
pass to the next filter in the queue.
|
||||
:param error: The string that :exc:`.exc.Discard` should display if the object is discarded.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def decorated(obj):
|
||||
result: Any = func(obj)
|
||||
if not isinstance(result, t):
|
||||
raise exc.Discard(result, f"Not instance of type {t}")
|
||||
return result
|
||||
async def decorated(obj):
|
||||
result: Any = await func(obj)
|
||||
if c(result):
|
||||
return result
|
||||
else:
|
||||
raise exc.Discard(obj=result, message=error)
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
def filter(self, c: Callable[[Any], bool], error: str) -> Filter:
|
||||
"""
|
||||
Check the condition ``c`` on all objects transiting through the queue:
|
||||
- If the check **passes**, the object goes on to the next filter;
|
||||
- If the check **fails**, the object is discarded, with ``error`` as reason;
|
||||
- If an error is raised, propagate the error upwards.
|
||||
|
||||
.. seealso:: :meth:`._deco_filter`, :func:`filter`
|
||||
|
||||
:param c: A function that takes in input an object and performs a check on it, returning either :data:`True`
|
||||
or :data:`False`.
|
||||
:param error: The reason for which objects should be discarded.
|
||||
:return: A new :class:`Filter` with this new condition.
|
||||
"""
|
||||
return self.__class__(self._deco_filter(c, error=error)(self.func))
|
||||
|
||||
@staticmethod
|
||||
def _deco_map(c: Callable[[Any], object]):
|
||||
"""
|
||||
A decorator which applies the function ``c`` on all objects transiting through the queue:
|
||||
- If the function **returns**, return its return value;
|
||||
- If the function **raises** an error, it is propagated upwards.
|
||||
|
||||
.. seealso:: :func:`map`
|
||||
|
||||
:param c: A function that takes in input an enqueued object and returns either the same object or something
|
||||
else.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
async def decorated(obj):
|
||||
result: Any = await func(obj)
|
||||
return c(result)
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
def map(self, c: Callable[[Any], bool]) -> Filter:
|
||||
"""
|
||||
Apply the function ``c`` on all objects transiting through the queue:
|
||||
- If the function **returns**, its return value replaces the object in the queue;
|
||||
- If the function **raises** :exc:`.exc.Discard`, the object is discarded;
|
||||
- If the function **raises another error**, propagate the error upwards.
|
||||
|
||||
.. seealso:: :meth:`._deco_map`, :func:`filter`
|
||||
|
||||
:param c: A function that takes in input an enqueued object and returns either the same object or something
|
||||
else.
|
||||
:return: A new :class:`Filter` with this new condition.
|
||||
"""
|
||||
return self.__class__(self._deco_map(c)(self.func))
|
||||
|
||||
def type(self, t: type) -> Filter:
|
||||
"""
|
||||
:exc:`exc.Discard` all objects that are not an instance of ``t``.
|
||||
Check if an object passing through the queue :func:`isinstance` of the type ``t``.
|
||||
|
||||
:param t: The type that objects should be instances of.
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
:return: A new :class:`Filter` with this new condition.
|
||||
"""
|
||||
return self.__class__(self._deco_type(t)(self.func))
|
||||
return self.filter(lambda o: isinstance(o, t), error=f"Not instance of type {t}")
|
||||
|
||||
def msg(self) -> Filter:
|
||||
"""
|
||||
:exc:`exc.Discard` all objects that are not an instance of :class:`.blueprints.Message`.
|
||||
Check if an object passing through the queue :func:`isinstance` of :class:`.blueprints.Message`.
|
||||
|
||||
:return: A new :class:`Filter` with this new condition.
|
||||
"""
|
||||
return self.type(blueprints.Message)
|
||||
|
||||
def requires(self, *fields,
|
||||
propagate_not_available=False,
|
||||
propagate_never_available=True) -> Filter:
|
||||
"""
|
||||
Test a :class:`.blueprints.Blueprint`'s fields by using its ``.requires()`` method:
|
||||
- If the :class:`.blueprints.Blueprint` has the appropriate fields, return it;
|
||||
- If the :class:`.blueprints.Blueprint` doesn't have data for at least one of the fields, the object is
|
||||
discarded;
|
||||
- the :class:`.blueprints.Blueprint` never has data for at least one of the fields,
|
||||
:exc:`.exc.NotAvailableError` is propagated upwards.
|
||||
|
||||
:param fields: The fields to test for.
|
||||
:param propagate_not_available: If :exc:`.exc.NotAvailableError` should be propagated
|
||||
instead of discarding the errored object.
|
||||
:param propagate_never_available: If :exc:`.exc.NeverAvailableError` should be propagated
|
||||
instead of discarding the errored object.
|
||||
:return: A new :class:`Filter` with this new condition.
|
||||
"""
|
||||
def check(obj):
|
||||
try:
|
||||
return obj.requires(*fields)
|
||||
except exc.NotAvailableError:
|
||||
if not propagate_not_available:
|
||||
raise
|
||||
raise exc.Discard(obj, "Data is not available")
|
||||
except exc.NeverAvailableError:
|
||||
if not propagate_never_available:
|
||||
raise
|
||||
raise exc.Discard(obj, "Data is never available")
|
||||
|
||||
return self.filter(check, error=".requires() method returned False")
|
||||
|
||||
def field(self) -> Filter:
|
||||
"""
|
||||
# TODO
|
||||
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
"""
|
||||
return self.__class__(self._deco_type(blueprints.Message)(self.func))
|
||||
return self.requires(blueprints.Message.text).map(lambda o: o.text())
|
||||
|
||||
@staticmethod
|
||||
def _deco_requires(*fields):
|
||||
def _deco_startswith(prefix: str):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def decorated(obj):
|
||||
result: blueprints.Blueprint = func(obj)
|
||||
try:
|
||||
result.requires(*fields)
|
||||
except exc.NotAvailableError:
|
||||
raise exc.Discard(result, "Missing data")
|
||||
except AttributeError:
|
||||
raise exc.Discard(result, "Missing .requires() method")
|
||||
result: str = func(obj)
|
||||
if not result.startswith(prefix):
|
||||
raise exc.Discard(result, f"Text didn't start with {prefix}")
|
||||
return result
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
def requires(self, *fields) -> Filter:
|
||||
def startswith(self, prefix: str):
|
||||
"""
|
||||
Test an object's fields by using its ``.requires()`` method (expecting it to be
|
||||
:meth:`.blueprints.Blueprint.requires`) and discard everything that does not pass the check.
|
||||
Check if an object starts with the specified prefix and discard the objects that do not.
|
||||
|
||||
:param fields: The fields to test for.
|
||||
:param prefix: The prefix object should start with.
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
"""
|
||||
return self.__class__(self._deco_requires(*fields)(self.func))
|
||||
|
||||
@staticmethod
|
||||
def _deco_text():
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def decorated(obj):
|
||||
result: blueprints.Message = func(obj)
|
||||
try:
|
||||
text = result.text()
|
||||
except exc.NotAvailableError:
|
||||
raise exc.Discard(result, "No text")
|
||||
except AttributeError:
|
||||
raise exc.Discard(result, "Missing text method")
|
||||
return text
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
def text(self) -> Filter:
|
||||
"""
|
||||
Get the text of the passed object by using its ``.text()`` method (expecting it to be
|
||||
:meth:`.blueprints.Message.text`), while discarding all objects that don't have a text.
|
||||
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
"""
|
||||
return self.__class__(self._deco_text()(self.func))
|
||||
return self.__class__(self._deco_startswith(prefix)(self.func))
|
||||
|
||||
@staticmethod
|
||||
def _deco_regex(pattern: Pattern):
|
||||
|
@ -132,26 +207,13 @@ class Filter:
|
|||
|
||||
def regex(self, pattern: Pattern):
|
||||
"""
|
||||
Apply a regex over an object's text (obtained through its ``.text()`` method, expecting it to be
|
||||
:meth:`.blueprints.Message.text`) and discard the object if it does not match.
|
||||
Apply a regex over an object and discard the object if it does not match.
|
||||
|
||||
:param pattern: The pattern that should be matched by the text.
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
"""
|
||||
return self.__class__(self._deco_regex(pattern)(self.func))
|
||||
|
||||
@staticmethod
|
||||
def _deco_choices(*choices):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def decorated(obj: blueprints.Message):
|
||||
result = func(obj)
|
||||
if result not in choices:
|
||||
raise exc.Discard(result, "Not a valid choice")
|
||||
return result
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
def choices(self, *choices):
|
||||
"""
|
||||
Ensure an object is in the ``choices`` list, discarding the object otherwise.
|
||||
|
@ -159,7 +221,7 @@ class Filter:
|
|||
:param choices: The pattern that should be matched by the text.
|
||||
:return: A new :class:`Filter` with the new requirements.
|
||||
"""
|
||||
return self.__class__(self._deco_choices(*choices)(self.func))
|
||||
return self.__class__(self._deco_check(lambda o: o in choices, error="Not a valid choice")(self.func))
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
|
@ -27,23 +27,23 @@ class Sentry:
|
|||
def __repr__(self):
|
||||
return f"<Sentry>"
|
||||
|
||||
async def get(self, *_, **__) -> Any:
|
||||
"""
|
||||
Wait until an :class:`object` leaves the queue, then return it.
|
||||
|
||||
:return: The :class:`object` which entered the queue.
|
||||
"""
|
||||
return await self.queue.get()
|
||||
|
||||
async def filter(self):
|
||||
async def f(self):
|
||||
"""
|
||||
Create a :class:`.filters.Filter` object, which can be configured through its fluent interface.
|
||||
|
||||
Remember to call ``.get()`` on the end of the chain.
|
||||
Remember to call ``.get()`` on the end of the chain to finally get the object.
|
||||
|
||||
To get any object, call:
|
||||
|
||||
.. code-block::
|
||||
|
||||
await sentry.f().get()
|
||||
|
||||
.. seealso:: :class:`.filters.Filter`
|
||||
|
||||
:return: The created :class:`.filters.Filter`.
|
||||
"""
|
||||
return Filter(self.get)
|
||||
return Filter(self.queue.get)
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
Loading…
Reference in a new issue