1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 05:24:20 +00:00

💥 Cooler filters!

This commit is contained in:
Steffo 2020-12-14 02:32:12 +01:00
parent 0a7607c3c3
commit b44290bd98
3 changed files with 140 additions and 76 deletions

View file

@ -56,7 +56,7 @@ class Blueprint(metaclass=abc.ABCMeta):
""" """
raise NotImplementedError() 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 Ensure that this blueprint has the specified fields, re-raising the highest priority exception raised between
all of them. all of them.
@ -85,6 +85,8 @@ class Blueprint(metaclass=abc.ABCMeta):
if len(exceptions) > 0: if len(exceptions) > 0:
raise max(exceptions, key=lambda e: e.priority) raise max(exceptions, key=lambda e: e.priority)
return True
__all__ = ( __all__ = (
"Blueprint", "Blueprint",

View file

@ -38,84 +38,159 @@ class Filter:
return result return result
@staticmethod @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): def decorator(func):
@functools.wraps(func) @functools.wraps(func)
def decorated(obj): async def decorated(obj):
result: Any = func(obj) result: Any = await func(obj)
if not isinstance(result, t): if c(result):
raise exc.Discard(result, f"Not instance of type {t}")
return result return result
else:
raise exc.Discard(obj=result, message=error)
return decorated return decorated
return decorator 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: 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. :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: 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: 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 @staticmethod
def _deco_requires(*fields): def _deco_startswith(prefix: str):
def decorator(func): def decorator(func):
@functools.wraps(func) @functools.wraps(func)
def decorated(obj): def decorated(obj):
result: blueprints.Blueprint = func(obj) result: str = func(obj)
try: if not result.startswith(prefix):
result.requires(*fields) raise exc.Discard(result, f"Text didn't start with {prefix}")
except exc.NotAvailableError:
raise exc.Discard(result, "Missing data")
except AttributeError:
raise exc.Discard(result, "Missing .requires() method")
return result return result
return decorated return decorated
return decorator 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 Check if an object starts with the specified prefix and discard the objects that do not.
:meth:`.blueprints.Blueprint.requires`) and discard everything that does not pass the check.
: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: A new :class:`Filter` with the new requirements.
""" """
return self.__class__(self._deco_requires(*fields)(self.func)) return self.__class__(self._deco_startswith(prefix)(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))
@staticmethod @staticmethod
def _deco_regex(pattern: Pattern): def _deco_regex(pattern: Pattern):
@ -132,26 +207,13 @@ class Filter:
def regex(self, pattern: Pattern): def regex(self, pattern: Pattern):
""" """
Apply a regex over an object's text (obtained through its ``.text()`` method, expecting it to be Apply a regex over an object and discard the object if it does not match.
:meth:`.blueprints.Message.text`) and discard the object if it does not match.
:param pattern: The pattern that should be matched by the text. :param pattern: The pattern that should be matched by the text.
:return: A new :class:`Filter` with the new requirements. :return: A new :class:`Filter` with the new requirements.
""" """
return self.__class__(self._deco_regex(pattern)(self.func)) 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): def choices(self, *choices):
""" """
Ensure an object is in the ``choices`` list, discarding the object otherwise. 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. :param choices: The pattern that should be matched by the text.
:return: A new :class:`Filter` with the new requirements. :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__ = ( __all__ = (

View file

@ -27,23 +27,23 @@ class Sentry:
def __repr__(self): def __repr__(self):
return f"<Sentry>" return f"<Sentry>"
async def get(self, *_, **__) -> Any: async def f(self):
"""
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):
""" """
Create a :class:`.filters.Filter` object, which can be configured through its fluent interface. 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: The created :class:`.filters.Filter`.
""" """
return Filter(self.get) return Filter(self.queue.get)
__all__ = ( __all__ = (