diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae9acfd..7a5d497 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,2 @@ /prawdditions/patch/ @RGood +/prawdditions/filters/ @PythonCoderAS \ No newline at end of file diff --git a/docs/filters.rst b/docs/filters.rst new file mode 100644 index 0000000..fbaff74 --- /dev/null +++ b/docs/filters.rst @@ -0,0 +1,32 @@ +prawdditions.filters +==================== + +The prawdditions.filters contains one class, :class:`.Filterable`, and many +other helper classes & functions, that can be used with :class:`.Filterable`. + +.. codeauthor:: PokestarFan <@PythonCoderAS> + +The Filterable Class +-------------------- + +.. autoclass:: prawdditions.filters.filter.Filterable + :members: + +Filter Callbacks +---------------- + +:class:`.Filterable` takes callback functions as filterable items, similar +to :func:`filter`. Custom functions can be provided, although there are a +lot of built-in functions that return callbacks. + +.. toctree:: + :maxdepth: 2 + :caption: Filter Callbacks: + + filters/base + +Filter Capsules +--------------- + +.. autoclass:: prawdditions.filters.capsule.FilterCapsule + :members: \ No newline at end of file diff --git a/docs/filters/base.rst b/docs/filters/base.rst new file mode 100644 index 0000000..7ae599e --- /dev/null +++ b/docs/filters/base.rst @@ -0,0 +1,21 @@ +Base Callbacks +-------------- + +These are basic callback functions that most of the advanced callbacks +functions use to provide their functionality. These callbacks should not be +used, unless it is to filter an attribute of an unsupported model. + +prawdditions.filters.base.filter_attribute +++++++++++++++++++++++++++++++++++++++++++ + +.. autofunction:: prawdditions.filters.base.filter_attribute + +prawdditions.filters.base.filter_number ++++++++++++++++++++++++++++++++++++++++ + +.. autofunction:: prawdditions.filters.base.filter_number + +prawdditions.filters.base.filter_true ++++++++++++++++++++++++++++++++++++++++ + +.. autofunction:: prawdditions.filters.base.filter_true \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 75ab6bb..97f1fb6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,14 +1,17 @@ -PRAWdittions +PRAWdditions ============ -PRAWdittions is a high-end library wrapper for PRAW that provides several +PRAWdditions is a high-end library wrapper for PRAW that provides several functions that are missing from the main package. The only dependency is PRAW. .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Table of Contents: + + filters.rst + util.rst patch.rst diff --git a/docs/util.rst b/docs/util.rst new file mode 100644 index 0000000..f2b3c91 --- /dev/null +++ b/docs/util.rst @@ -0,0 +1,5 @@ +prawdditions.util +------------------------- + +.. automodule:: prawdditions.util + :members: \ No newline at end of file diff --git a/prawdditions/filters/__init__.py b/prawdditions/filters/__init__.py new file mode 100644 index 0000000..fe70654 --- /dev/null +++ b/prawdditions/filters/__init__.py @@ -0,0 +1,7 @@ +"""Init file for the filter sub-module.""" + +from .capsule import FilterCapsule +from .filter import Filterable +from .redditor import * +from .subreddit import * +from .user_content import * diff --git a/prawdditions/filters/base.py b/prawdditions/filters/base.py new file mode 100644 index 0000000..13590c4 --- /dev/null +++ b/prawdditions/filters/base.py @@ -0,0 +1,170 @@ +"""Base functions shared by all other filter modules.""" +from typing import Union, Any, Callable, Type, Tuple +from praw.models import Comment, Redditor, Submission, Subreddit +from prawdditions.util import symbol_action + + +class BaseFilter: + """Base Filters. + + .. note:: This class should never be initialized directly. Instead, + call them from :class:`.Filterable`. + """ + + @staticmethod + def filter_attribute( + check_class: Union[ + Type[Union[Any, Comment, Redditor, Submission, Subreddit]], + Tuple[Type[Union[Any, Comment, Redditor, Submission, Subreddit]]], + ], + classattr: str, + attribute: str, + value: [Union[Any, Comment, Redditor, Submission, Subreddit]], + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a specific attribute of a class. + + :param check_class: The class to check for, such as + :class:`praw.models.Redditor` or :class`praw.models.Subreddit`. + :param classattr: The attribute to implement the base function for, + such as ``author`` for Redditor, ``subreddit`` for Subreddits, etc. + :param attribute: The attribute to check for. + :param value: The value that the attribute should equal. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + .. note:: This function is for equality. If you want to do numerical + comparisons, such as karma, use :meth:`.BaseFilter.filter_number`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + return ( + getattr( + ( + item + if isinstance(item, check_class) + else getattr(item, classattr) + ), + attribute, + ) + == value + ) + + return filter_func + + @classmethod + def filter_number( + cls, + check_class: Union[ + Type[Union[Any, Comment, Redditor, Submission, Subreddit]], + Tuple[Type[Union[Any, Comment, Redditor, Submission, Subreddit]]], + ], + classattr: str, + attribute: str, + symbol: str, + value: Union[Any, int], + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a numerical attribute of a class. + + :param check_class: The class to check for, such as + :class:`praw.models.Redditor` or :class`praw.models.Subreddit`. + :param classattr: The attribute to implement the base function for, + such as ``author`` for Redditor, ``subreddit`` for Subreddits, etc. + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + < value, + lambda item: getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + > value, + lambda item: getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + <= value, + lambda item: getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + >= value, + cls.filter_attribute(classattr, attribute, value), + lambda item: getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + != value, + ) + + @staticmethod + def filter_true( + check_class: Union[ + Type[Union[Any, Comment, Redditor, Submission, Subreddit]], + Tuple[Type[Union[Any, Comment, Redditor, Submission, Subreddit]]], + ], + classattr: str, + attribute: str, + opposite: bool = False, + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by the boolean value of an attribute. + + :param check_class: The class to check for, such as + :class:`praw.models.Redditor` or :class`praw.models.Subreddit`. + :param classattr: The attribute to implement the base function for, + such as ``author`` for Redditor, ``subreddit`` for Subreddits, etc. + :param attribute: The attribute to check for. + :param opposite: Whether to return items that matched False ( + Default: False) + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + result = bool( + getattr( + item + if isinstance(item, check_class) + else getattr(item, classattr), + attribute, + ) + ) + return (not result) if opposite else result + + return filter_func diff --git a/prawdditions/filters/capsule.py b/prawdditions/filters/capsule.py new file mode 100644 index 0000000..1970689 --- /dev/null +++ b/prawdditions/filters/capsule.py @@ -0,0 +1,187 @@ +"""Filter capsules are stored here.""" +from typing import ( + List, + Union, + Any, + Iterator, + Callable, + TypeVar, +) +from praw.models import Comment, Redditor, Submission, Subreddit + +_FilterCapsule = TypeVar("_FilterCapsule") + + +class FilterCapsule: + """A filter capsule. + + The filter capsule allows you to to have a mini-``AND filter`` or a + mini-``OR filter`` without having to use another Filterable class. + For example, if you wanted a filter that had several filters, but some + are compound filters, such as: + + 1. Return posts from users below 7 days that are either approved + submitters or have over 200 karma. + 2. Return posts from users below 30 days that are either approved + submitters or have 100 karma. + 3. Return all posts from users above 30 days. + + It would be impossible to use the ``AND filter`` list, as it would be + impossible to fill the 100 karma requirement due to the 200 karma filter + from the first hypothetical. A custom function would have to be added + that does each of the checks, which defeats the whole point of the + Filterable class being a class built up of individual parts. + + With a filter capsule, each step can become a filter capsule. + + .. code-block:: python + + from prawdditions.filters import (Filterable, + FilterCapsule, + filter_account_age, + filter_account_karma, + filter_approved_submitter) + submission_stream = reddit.subreddit("mod").stream.submissions() + filtered_stream = Filterable(submission_stream) + sub_filter_1 = FilterCapsule() + sub_filter_1.add_and_filter(filter_account_age("<", 7, "days")) + sub_filter_1.add_or_filter(filter_approved_submitter()) + sub_filter_1.add_or_filter(filter_account_karma(">=", 200)) + sub_filter_2 = FilterCapsule() + sub_filter_2.add_and_filter(filter_account_age("<", 30, "days")) + sub_filter_2.add_or_filter(filter_approved_submitter()) + sub_filter_2.add_or_filter(filter_account_karma(">=", 100)) + filtered_stream.filter_or(sub_filter_1) + filtered_stream.filter_or(sub_filter_2) + filtered_stream.filter_or(filter_account_age(">=", 30, "days")) + for post in filtered_stream: + print(post) + """ + + @property + def and_filters( + self, + ) -> List[ + Callable[[Union[Any, Comment, Redditor, Submission, Subreddit]], bool] + ]: + """View the list of ``AND filters``.""" + return self._and_list + + @property + def or_filters( + self, + ) -> List[ + Callable[[Union[Any, Comment, Redditor, Submission, Subreddit]], bool] + ]: + """View the list of ``OR filters``.""" + return self._or_list + + def __init__(self): + """Initialize the class.""" + self._and_list = [] + self._or_list = [] + + def __len__(self) -> int: + """Get the length of both filter lists.""" + return len(self.and_filters + self.or_filters) + + def __repr__(self) -> str: + """Return the REPR of the instance.""" + return "{} with {} AND filters & {} OR filters >".format( + self.__class__.__name__, + len(self.and_filters), + len(self.or_filters), + ) + + def add_filter( + self, + filter_type: str, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _FilterCapsule: + """Add a filter of type ``filter_type`` to the corresponding list. + + This method implements the same logic as :class:`.Filterable`. + + The function returns the class, so it is possible to chain filters, + like this: + + .. code-block:: python + + from prawdditions.filters import (Filterable, filter_author, + filter_subreddit) + subreddit = reddit.subreddit("all") + filtered_stream = Filterable(subreddit.stream.submissions) + sub_filter_1 = FilterCapsule().add_and_filter( + filter_account_age("<", 7, "days")).add_or_filter( + filter_approved_submitter()).add_or_filter( + filter_account_karma(">=", 200)) + for submission in filtered_stream.filter(sub_filter_1): + print(submission) + + The previous example will filter + + :param filter_type: The filter type, ``or`` & ``and``. + :param filter_func: The filter function generated from a template or a + custom function. Must take an item and return a boolean. + :returns: The class + """ + if isinstance(filter_func, FilterCapsule): + filter_func = filter_func.filter + if filter_type.lower() == "and": + self.and_filters.append(filter_func) + elif filter_type.lower() == "or": + self.or_filters.append(filter_func) + else: + raise ValueError( + "Unrecognized filter type: {!r}. Valid filter types are: " + "'and' & 'or' filter types.".format(filter_type) + ) + return self + + def add_and_filter( + self, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _FilterCapsule: + """Convenience function to add to the ``AND filters`` list.""" + return self.add_filter("and", filter_func) + + def add_or_filter( + self, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _FilterCapsule: + """Convenience function to add to the ``OR filters`` list.""" + return self.add_filter("or", filter_func) + + def filter( + self, item: Union[Any, Comment, Redditor, Submission, Subreddit] + ) -> bool: + """Check the item against the filter rules. + + This method implements the same logic as :class:`.Filterable`. + + :param item: The item to check against the filters. + :returns: Whether or not the item matched the filters. + + .. warning:: This function should never be directly called. Instead, + it is provided as a function type that can be used by + :class:`.Filterable`. + """ + if len(self.and_filters) > 0: + and_status = True + for and_filter in self.and_filters: + and_status &= and_filter(item) + else: + and_status = True + if len(self.or_filters) > 0: + or_status = False + for or_filter in self.or_filters: + or_status |= or_filter(item) + else: + or_status = True + return and_status and or_status diff --git a/prawdditions/filters/filter.py b/prawdditions/filters/filter.py new file mode 100644 index 0000000..945c9d6 --- /dev/null +++ b/prawdditions/filters/filter.py @@ -0,0 +1,217 @@ +"""Filter items from a PRAW listing or stream.""" +from typing import List, Union, Any, Iterator, Callable, TypeVar, Dict +from praw.models import Comment, Redditor, Submission, Subreddit +from .capsule import FilterCapsule +from .redditor import RedditorFilters +from .subreddit import SubredditFilters + +# from .user_content import + +_Filterable = TypeVar("_Filterable") + + +class Filterable(RedditorFilters, SubredditFilters): + """Create a filterable generator/iterator. + + Filterable iterators can be filtered with :meth:`.filter`. + There are two types of filters, ``AND filters``, and ``OR filters``. In + order for an object to be yielded, every filter in the list of + ``AND filters`` and any one filter in the ``OR filters`` list has to + return True. + + For example, in order to filter all posts to either ``r/AskReddit`` or + ``r/programming``, but must be by a user who has a karma score >500 + + """ + + @property + def and_filters( + self, + ) -> List[ + Callable[[Union[Any, Comment, Redditor, Submission, Subreddit]], bool] + ]: + """View the list of ``AND filters``.""" + return self._and_list + + @property + def or_filters( + self, + ) -> List[ + Callable[[Union[Any, Comment, Redditor, Submission, Subreddit]], bool] + ]: + """View the list of ``OR filters``.""" + return self._or_list + + @property + def capsule(self) -> FilterCapsule: + """Obtain a filter capsule to work with.""" + return FilterCapsule() + + def __init__( + self, + generator: Union[ + Iterator[Union[Any, Comment, Redditor, Submission, Subreddit]] + ], + cache_items: int = 1000, + ): + """Initialize the class. + + :param generator: A generator or iterator that yields + :class:`praw.models.Comment`\ s, :class:`praw.models.Redditor`\ s, + :class:`praw.models.Submission`\ s, and/or + :class:`praw.models.Subreddit`\ s. + :param cache_items: The amount of items to maintain in the caches ( + Default=1000). + + """ + self.generator = generator + self._iter = iter(generator) + self._and_list = [] + self._or_list = [] + self._cache_count = 0 + self._set_up_redditor_cache(keep=cache_items) + self._set_up_subreddit_cache(keep=cache_items) + + def __iter__(self) -> _Filterable: + """Return the iterator, also this class. + + :returns: This class + :meta private: + """ + return self + + def __len__(self) -> int: + """Get the length of both filter lists. + + :returns: The combined length of :meth:`.and_filters` and + :meth:`.or_filters`. + :meta private: + """ + return len(self.and_filters + self.or_filters) + + def __next__(self) -> Any: + """Return the next item in the wrapper iterator after filtering. + + :returns: An object from the wrapped iterator. + :meta private: + """ + if self._cache_count >= 2000: + self.clean_cache() + self._cache_count = 0 + status = False + while not status: + item = next(self._iter) + if len(self.and_filters) > 0: + and_status = True + for and_filter in self.and_filters: + and_status &= and_filter(item) + else: + and_status = True + if len(self.or_filters) > 0: + or_status = False + for or_filter in self.or_filters: + or_status |= or_filter(item) + else: + or_status = True + status = and_status and or_status + self._cache_count += 1 + return item + + def __repr__(self) -> str: + """Return the REPR of the instance. + + :returns: The REPR as a string + :meta private: + """ + return "<{} {!r} with {} AND filters & {} OR filters>".format( + self.__class__.__name__, + self.generator.__class__.__name__, + len(self.and_filters), + len(self.or_filters), + ) + + def clean_cache(self): + """Clean the cache to reduce resource usage. + + .. note:: It is run once every 2000 yields. + """ + self._redditor_cache["muted"] = dict( + sorted( + self._redditor_cache.get("muted", {}), + key=lambda entry: entry["timestamp"], + )[0 : self._redditor_cache_keep] + ) + self._redditor_cache["approved_submitter"] = dict( + sorted( + self._redditor_cache.get("approved_submitter", {}), + key=lambda entry: entry["timestamp"], + )[0 : self._redditor_cache_keep] + ) + self._subreddit_cache = dict( + sorted( + self._subreddit_cache.items(), + key=lambda entry: entry["timestamp"], + )[0 : self._subreddit_cache_keep] + ) + + def filter( + self, + filter_type: str, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _Filterable: + """Add a filter of type ``filter_type`` to the corresponding list. + + The function returns the class, so it is possible to chain filters, + like this: + + .. code-block:: python + + from prawdditions.filters import (Filterable, filter_author, + filter_subreddit) + subreddit = reddit.subreddit("all") + filtered_stream = Filterable(subreddit.stream.submissions) + for submission in filtered_stream.filter("and", + filter_subreddit(reddit.subreddit("test"))).filter("and", + filter_author(reddit.redditor("spez"))): + print(submission) + + The previous example will filter + + :param filter_type: The filter type, ``or`` & ``and``. + :param filter_func: The filter function generated from a template or a + custom function. Must take an item and return a boolean. + :returns: The class + + """ + if isinstance(filter_func, FilterCapsule): + filter_func = filter_func.filter + if filter_type.lower() == "and": + self.and_filters.append(filter_func) + elif filter_type.lower() == "or": + self.or_filters.append(filter_func) + else: + raise ValueError( + "Unrecognized filter type: {!r}. Valid filter types are: " + "'and' & 'or' filter types.".format(filter_type) + ) + return self + + def filter_and( + self, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _Filterable: + """Convenience function to add to the ``AND filters`` list.""" + return self.filter("and", filter_func) + + def filter_or( + self, + filter_func: Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ], + ) -> _Filterable: + """Convenience function to add to the ``OR filters`` list.""" + return self.filter("or", filter_func) diff --git a/prawdditions/filters/redditor.py b/prawdditions/filters/redditor.py new file mode 100644 index 0000000..e330f48 --- /dev/null +++ b/prawdditions/filters/redditor.py @@ -0,0 +1,340 @@ +"""Holds functions for comparing redditors.""" +from datetime import datetime +from time import time +from typing import Union, Any, Callable +from praw.models import Comment, Redditor, Submission, Subreddit +from .base import BaseFilter +from prawdditions.util import get_seconds, symbol_action + + +class RedditorFilters(BaseFilter): + """Filter functions that apply to Redditors.""" + + def _set_up_redditor_cache(self, keep=1000): + self._redditor_cache = dict() + self._redditor_cache_keep = keep + + @staticmethod + def filter_redditor( + redditor: Redditor, + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Generate a filter function for the given redditor. + + :param redditor: An instance of :class:`Redditor`. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + return ( + item if isinstance(item, Redditor) else item.author == redditor + ) + + return filter_func + + def filter_redditor_attribute( + self, + attribute: str, + value: [Union[Any, Comment, Redditor, Submission, Subreddit]], + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a specific attribute of :class:`praw.models.Redditor`. + + :param attribute: The attribute to check for. + :param value: The value that the attribute should equal. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + .. note:: This function is for equality. If you want to do numerical + comparisons, such as karma, use :meth:`.filter_redditor_number`. + """ + + return self.filter_attribute(Redditor, "author", attribute, value) + + def filter_redditor_number( + self, attribute: str, symbol: str, value: Union[Any, int] + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a numerical attribute of :class:`praw.models.Redditor`. + + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_number(Redditor, "author", attribute, symbol, value) + + def filter_redditor_true( + self, attribute: str, opposite: bool = False + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by the boolean value of :class:`praw.models.Redditor`. + + :param attribute: The attribute to check for. + :param opposite: Whether to return items that matched False ( + Default: False) + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_true(Redditor, "author", attribute) + + @staticmethod + def filter_account_age( + symbol: str, amount: int, unit="days" + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items by redditor age. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount: The amount of time to compare by. + :param unit: The unit of time the amount represents. Defaults to + ``days``. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + Units of time usable by the function: + + * Seconds: ``s``, ``sec``, ``secs``, ``second``, ``seconds`` + * Minutes: ``min``, ``mins``, ``minute``, ``minutes`` + * Hours: ``h``, ``hr``, ``hrs``, ``hour``, ``hours`` + * Days: ``d``, ``day``, ``days`` + * Weeks: ``w``, ``wk``, ``wks``, ``week``, ``weeks`` + * Months: ``mon``, ``month``, ``months`` + * Years: ``y``, ``yr``, ``yrs``, ``year``, ``years`` + + .. note:: A month is regarded as 30 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + + .. note:: A year is regarded as 365 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + """ + comparison_time = get_seconds(amount, unit) + return symbol_action( + symbol, + return_symbol_1=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + > comparison_time, + return_symbol_2=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + < comparison_time, + return_symbol_3=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + >= comparison_time, + return_symbol_4=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + <= comparison_time, + return_symbol_5=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + == comparison_time, + return_symbol_6=lambda item: datetime.utcnow().timestamp() + - (item if isinstance(item, Redditor) else item.author).created_utc + != comparison_time, + ) + + def filter_account_link_karma( + self, symbol: str, karma: int + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items based on the redditor's link karma. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param karma: The amount of karma to compare with. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_redditor_number("link_karma", symbol, karma) + + def filter_account_comment_karma( + self, symbol: str, karma: int + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items based on the redditor's comment karma. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param karma: The amount of karma to compare with. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_redditor_number("comment_karma", symbol, karma) + + @staticmethod + def filter_account_karma( + symbol: str, karma: int + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items based on the redditor's total (link + comment) karma. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param karma: The amount of karma to compare with. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + < karma, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + > karma, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + <= karma, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + >= karma, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + >= karma, + lambda item: ( + item if isinstance(item, Redditor) else item.author + ).link_karma + + ( + item if isinstance(item, Redditor) else item.author + ).comment_karma + != karma, + ) + + def filter_account_muted( + self, get_mute_info=600 + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items based on the redditor's muted status. + + .. note:: The streamed type must be a type that has a ``subreddit`` + attribute, such as a Comment or Submission. + + .. note:: The authenticated account must be a moderator of any + subreddits that are filtered with this filter. + + :param get_mute_info: The class will obtain and refresh a list of + muted accounts every ``get_mute_info`` seconds. (Default: 600 + seconds or 10 minutes). + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + if "muted" not in self._redditor_cache: + self._redditor_cache["muted"] = {} + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + subreddit = str(item.subreddit) + if subreddit not in self._redditor_cache["muted"]: + self._redditor_cache["muted"][subreddit] = { + "timestamp": time(), + "data": list(item.subreddit.muted()), + } + else: + cached = self._redditor_cache["muted"][subreddit]["timestamp"] + if time() - cached > get_mute_info: + self._redditor_cache["muted"][subreddit] = { + "timestamp": time(), + "data": list(item.subreddit.muted()), + } + return ( + item.author in self._redditor_cache["muted"][subreddit]["data"] + ) + + return filter_func + + def filter_account_approved_submitter( + self, get_approved_submitter_info=600 + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items based on the redditor's approved_submitter status. + + .. note:: The streamed type must be a type that has a ``subreddit`` + attribute, such as a Comment or Submission. + + .. note:: The authenticated account must be a moderator of any + subreddits that are filtered with this filter. + + :param get_approved_submitter_info: The class will obtain and refresh + a list of approved submitter accounts every + ``get_approved_submitter_info`` seconds. (Default: 600 seconds + or 10 minutes). + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + if "approved_submitter" not in self._redditor_cache: + self._redditor_cache["approved_submitter"] = {} + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + subreddit = str(item.subreddit) + if subreddit not in self._redditor_cache["approved_submitter"]: + self._redditor_cache["approved_submitter"][subreddit] = { + "timestamp": time(), + "data": list(item.subreddit.contributor()), + } + else: + cached = self._redditor_cache["approved_submitter"][subreddit][ + "timestamp" + ] + if time() - cached > get_approved_submitter_info: + self._redditor_cache["approved_submitter"][subreddit] = { + "timestamp": time(), + "data": list(item.subreddit.contributor()), + } + return ( + item.author + in self._redditor_cache["approved_submitter"][subreddit][ + "data" + ] + ) + + return filter_func diff --git a/prawdditions/filters/subreddit.py b/prawdditions/filters/subreddit.py new file mode 100644 index 0000000..5b465f7 --- /dev/null +++ b/prawdditions/filters/subreddit.py @@ -0,0 +1,829 @@ +"""Holds filters for comparing subreddits.""" +from datetime import datetime +from time import time +from typing import Union, Any, Callable +from praw.models import Comment, Redditor, Submission, Subreddit +from .base import BaseFilter +from prawdditions.util import get_seconds, symbol_action + + +class SubredditFilters(BaseFilter): + """Filter functions that apply to Subreddits. + + .. note:: Since these filters will most likely be used in high-traffic + streams such as ``r/all``, to prevent the delay of a stream, results + will be cached. The cache time can be configured by + :meth:`.set_subreddit_cache`. + """ + + def _set_up_subreddit_cache(self, keep=1000): + self._subreddit_cache = {} + self._subreddit_cache_time = 3600 + self._subreddit_cache_keep = keep + + @staticmethod + def filter_subreddit( + subreddit: Subreddit, + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Generate a filter function for the given subreddit. + + :param subreddit: An instance of :class:`Subreddit`. + :return: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + return ( + item if isinstance(item, Subreddit) else item.subreddit + ) == subreddit + + return filter_func + + def filter_subreddit_attribute( + self, + attribute: str, + value: [Union[Any, Comment, Redditor, Submission, Subreddit]], + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a specific attribute of :class:`praw.models.Subreddit`. + + .. note:: Results will be cached. + + :param attribute: The attribute to check for. + :param value: The value that the attribute should equal. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + .. note:: This function is for equality. If you want to do numerical + comparisons, such as karma, use :meth:`.filter_subreddit_number`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + == value + ) + + return filter_func + + def filter_subreddit_number( + self, attribute: str, symbol: str, value: Union[Any, int] + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by a numerical attribute of :class:`praw.models.Subreddit`. + + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def return_function_1(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + < value + ) + + def return_function_2(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + > value + ) + + def return_function_3(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + <= value + ) + + def return_function_4(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + >= value + ) + + def return_function_5(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + == value + ) + + def return_function_6(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + len( + getattr( + self._subreddit_cache[subreddit]["value"], attribute + ) + ) + != value + ) + + return symbol_action( + symbol, + return_function_1, + return_function_2, + return_function_3, + return_function_4, + return_function_5, + return_function_6, + ) + + def filter_subreddit_length( + self, attribute: str, symbol: str, value: Union[Any, int] + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by the length of an attribute of a subreddit. + + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def return_function_1(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + < value + ) + + def return_function_2(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + > value + ) + + def return_function_3(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + <= value + ) + + def return_function_4(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + >= value + ) + + def return_function_5(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + == value + ) + + def return_function_6(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + != value + ) + + return symbol_action( + symbol, + return_function_1, + return_function_2, + return_function_3, + return_function_4, + return_function_5, + return_function_6, + ) + + def filter_subreddit_true( + self, attribute: str, opposite: bool = False + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter by the boolean value of :class:`praw.models.Subreddit`. + + :param attribute: The attribute to check for. + :param opposite: Whether to return items that matched False ( + Default: False) + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func( + item: [Union[Any, Comment, Redditor, Submission, Subreddit]] + ) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + result = bool( + getattr(self._subreddit_cache[subreddit]["value"], attribute) + ) + return (not result) if opposite else result + + return filter_func + + def filter_subreddit_age( + self, symbol: str, amount: int, unit="days" + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter items by subreddit age. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount: The amount of time to compare by. + :param unit: The unit of time the amount represents. Defaults to + ``days``. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + Units of time usable by the function: + + * Seconds: ``s``, ``sec``, ``secs``, ``second``, ``seconds`` + * Minutes: ``min``, ``mins``, ``minute``, ``minutes`` + * Hours: ``h``, ``hr``, ``hrs``, ``hour``, ``hours`` + * Days: ``d``, ``day``, ``days`` + * Weeks: ``w``, ``wk``, ``wks``, ``week``, ``weeks`` + * Months: ``mon``, ``month``, ``months`` + * Years: ``y``, ``yr``, ``yrs``, ``year``, ``years`` + + .. note:: A month is regarded as 30 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + + .. note:: A year is regarded as 365 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + """ + comparison_time = get_seconds(amount, unit) + if "age" not in self._subreddit_cache: + self._subreddit_cache["age"] = {} + + def return_function_1(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + < comparison_time + ) + + def return_function_2(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + > comparison_time + ) + + def return_function_3(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + <= comparison_time + ) + + def return_function_4(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + >= comparison_time + ) + + def return_function_5(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + == comparison_time + ) + + def return_function_6(item) -> bool: + """A return function used by :func:`.symbol_action`. + + :param item: The item to compare with + :returns: The result of the comparison + """ + subreddit = str( + item if isinstance(item, Subreddit) else item.subreddit + ) + if subreddit not in self._subreddit_cache: + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + else: + if ( + time() - self._subreddit_cache[subreddit]["timestamp"] + > self._subreddit_cache_time + ): + self._subreddit_cache[subreddit] = { + "timestamp": time(), + "value": item._reddit.subreddit(subreddit), + } + + return ( + datetime.utcnow().timestamp() + - self._subreddit_cache[subreddit]["value"].created_utc + != comparison_time + ) + + return symbol_action( + symbol, + return_function_1, + return_function_2, + return_function_3, + return_function_4, + return_function_5, + return_function_6, + ) + + def filter_subreddit_subscribers( + self, symbol: str, amount: int + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filters a subreddit based on the amount of subscribers it has. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount: The amount of subscribers to check for + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_subreddit_number("subscribers", symbol, amount) + + def filter_subreddit_nsfw( + self, negate=False + ) -> Callable[ + [Union[Any, Comment, Redditor, Submission, Subreddit]], bool + ]: + """Filter upon the NSFW status of the subreddit. + + :param negate: Negates the NSFW check, so only non-nsfw subreddits + are returned. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_subreddit_true("over18", opposite=negate) + + def filter_subreddit_name(self, symbol: str, value: int): + """Filter by the amount of characters in a subreddit name. + + .. note:: Reddit has a 21-character limit for subreddit names, + so any values over 21 will not yield anything. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The amount of characters in a subreddit name. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_subreddit_length("display_name", symbol, value) + + def set_subreddit_cache(self, time: int): + """Set the subreddit cache time. + + :param time: The amount of time, in seconds, to cache results + """ + self._subreddit_cache_time = time diff --git a/prawdditions/filters/user_content.py b/prawdditions/filters/user_content.py new file mode 100644 index 0000000..8bff302 --- /dev/null +++ b/prawdditions/filters/user_content.py @@ -0,0 +1,814 @@ +from datetime import datetime +from typing import Union, Callable +from praw.models import Comment, Submission +from prawdditions.util import get_seconds, symbol_action + + +class UserContentFilters: + """Filter functions that apply to user content (Comments/Submissions).""" + + @staticmethod + def filter_parent_submission( + submission: Submission, + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter a stream for comments that are part of the submission. + + :param submission: The parent submission that comments need to be + part of. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func(item: [Union[Comment, Submission]]) -> bool: + """Filter an item. + + :param item: The item to check for equality + :returns: The status of the check. + """ + return ( + item if isinstance(item, Submission) else item.submission + ) == submission + + return filter_func + + @staticmethod + def filter_parent_comment(comment: Comment) -> Callable[[Comment], bool]: + """Filter a stream for comments that are part of the comment. + + :param comment: The parent comment that comments need to be part of. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func(item: Comment) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + return item.parent == comment + + return filter_func + + @staticmethod + def filter_content_attribute( + attribute: str, value: str, submission_only: bool = False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by the attribute of comments and submissions. + + :param attribute: The attribute to check for. + :param value: The value that the attribute should equal. + :param submission_only: Only submissions should be checked for. If + this parameter is set to True, and a comment is returned, + it will act on the comment's submission. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + .. note:: This function is for equality. If you want to do numerical + comparisons, such as karma, use :meth:`.filter_content_number`. + """ + + def filter_func(item: Union[Comment, Submission]) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + return ( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + == value + ) + + return filter_func + + @staticmethod + def filter_content_number( + attribute: str, symbol: str, value: int, submission_only: bool = False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by a numerical attribute of a Comment or Submission. + + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :param submission_only: Only submissions should be checked for. If + this parameter is set to True, and a comment is returned, + it will act on the comment's submission. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + < value, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + > value, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + <= value, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + >= value, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + == value, + lambda item: getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + != value, + ) + + @staticmethod + def filter_content_length( + attribute: str, symbol: str, value: int, submission_only: bool = False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by a numerical attribute of a Comment or Submission. + + :param attribute: The attribute to check for. + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :param submission_only: Only submissions should be checked for. If + this parameter is set to True, and a comment is returned, + it will act on the comment's submission. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + < value, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + > value, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + <= value, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + >= value, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + == value, + lambda item: len( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + != value, + ) + + @staticmethod + def filter_content_true( + attribute: str, opposite: bool = False, submission_only: bool = False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by the boolean attribute of comments and submissions. + + :param attribute: The attribute to check for. + :param opposite: Whether to return items that matched False ( + Default: False) + :param submission_only: Only submissions should be checked for. If + this parameter is set to True, and a comment is returned, + it will act on the comment's submission. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def filter_func(item: Union[Comment, Submission]) -> bool: + """Filter an item. + + :param item: The item to check for equality + """ + result = bool( + getattr( + item.submission + if (isinstance(item, Comment) and submission_only) + else item, + attribute, + ) + ) + return (not result) if opposite else result + + return filter_func + + @staticmethod + def filter_content_age( + symbol: str, amount: int, unit="days" + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter items by content age. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount: The amount of time to compare by. + :param unit: The unit of time the amount represents. Defaults to + ``days``. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + + Units of time usable by the function: + + * Seconds: ``s``, ``sec``, ``secs``, ``second``, ``seconds`` + * Minutes: ``min``, ``mins``, ``minute``, ``minutes`` + * Hours: ``h``, ``hr``, ``hrs``, ``hour``, ``hours`` + * Days: ``d``, ``day``, ``days`` + * Weeks: ``w``, ``wk``, ``wks``, ``week``, ``weeks`` + * Months: ``mon``, ``month``, ``months`` + * Years: ``y``, ``yr``, ``yrs``, ``year``, ``years`` + + .. note:: A month is regarded as 30 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + + .. note:: A year is regarded as 365 days. If finer control is needed on + the exact amount of days to check for, use a unit of days. + """ + comparison_time = get_seconds(amount, unit) + return symbol_action( + symbol, + return_symbol_1=lambda item: datetime.utcnow().timestamp() + - item.created_utc + > comparison_time, + return_symbol_2=lambda item: datetime.utcnow().timestamp() + - item.created_utc + < comparison_time, + return_symbol_3=lambda item: datetime.utcnow().timestamp() + - item.created_utc + >= comparison_time, + return_symbol_4=lambda item: datetime.utcnow().timestamp() + - item.created_utc + <= comparison_time, + return_symbol_5=lambda item: datetime.utcnow().timestamp() + - item.created_utc + == comparison_time, + return_symbol_6=lambda item: datetime.utcnow().timestamp() + - item.created_utc + != comparison_time, + ) + + @staticmethod + def filter_content_karma( + symbol: str, karma: int + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter items based on the content's karma (score). + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param karma: The amount of karma to compare with. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: item.score < karma, + lambda item: item.score > karma, + lambda item: item.score <= karma, + lambda item: item.score >= karma, + lambda item: item.score == karma, + lambda item: item.score != karma, + ) + + def filter_content_gilded( + self, + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter items based on their gilded status. + + .. note:: Reddit considers an item as gilded if it has been awarded + either Reddit Gold or Reddit Platinum. If you are looking for + filters for any awardings, including Reddit Silver, + use :meth:`.filter_content_awarded`. + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_true("gilded") + + def filter_content_awarded( + self, + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter items based on their awarding status. + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_true("all_awardings") + + @staticmethod + def filter_content_awards( + symbol: str, + amount_silver=None, + amount_gold=None, + amount_platinum=None, + total_awards=None, + ): + """Filter by the amount of awards in a comment/submission. + + .. note:: As Reddit is constantly changing their award catalog, + only parameters for silver, gold, platinum, and total awards are + included. In order to filter a specific award, + use :meth:`.filter_content_award`. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount_silver: The amount of silver awards the content needs to + contain (Default: 0) + :param amount_gold: The amount of gold awards the content needs to + contain (Default: 0) + :param amount_platinum: The amount of platinum awards the content needs + to contain (Default: 0) + :param total_awards: The total amount of awards (including all other + awards, such as subreddit awards. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def return_function_1(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver < amount_silver) + if amount_silver is not None + else True + ) + and ((gold < amount_gold) if amount_gold is not None else True) + and ( + (platinum < amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total < total_awards) + if total_awards is not None + else True + ) + ) + + def return_function_2(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver > amount_silver) + if amount_silver is not None + else True + ) + and ((gold > amount_gold) if amount_gold is not None else True) + and ( + (platinum > amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total > total_awards) + if total_awards is not None + else True + ) + ) + + def return_function_3(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver <= amount_silver) + if amount_silver is not None + else True + ) + and ( + (gold <= amount_gold) if amount_gold is not None else True + ) + and ( + (platinum <= amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total <= total_awards) + if total_awards is not None + else True + ) + ) + + def return_function_4(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver >= amount_silver) + if amount_silver is not None + else True + ) + and ( + (gold >= amount_gold) if amount_gold is not None else True + ) + and ( + (platinum >= amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total >= total_awards) + if total_awards is not None + else True + ) + ) + + def return_function_5(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver == amount_silver) + if amount_silver is not None + else True + ) + and ( + (gold == amount_gold) if amount_gold is not None else True + ) + and ( + (platinum == amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total == total_awards) + if total_awards is not None + else True + ) + ) + + def return_function_6(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + silver = item.gildings.get("gid_1", 0) + gold = item.gildings.get("gid_2", 0) + platinum = item.gildings.get("gid_3", 0) + total = item.total_awards_received + return ( + ( + (silver != amount_silver) + if amount_silver is not None + else True + ) + and ( + (gold != amount_gold) if amount_gold is not None else True + ) + and ( + (platinum != amount_platinum) + if amount_platinum is not None + else True + ) + and ( + (total != total_awards) + if total_awards is not None + else True + ) + ) + + return symbol_action( + symbol, + return_function_1, + return_function_2, + return_function_3, + return_function_4, + return_function_5, + return_function_6, + ) + + @staticmethod + def filter_content_award(symbol: str, award_name: str, amount: int): + """Filter based on the amount of a certain award name. + + .. note:: If you want to check if a specific award exists, invoke + the method as following: + + .. code-block:: python + + filter.filter(filter.filter_content_award(">=", "AWARDNAME", 1) + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param award_name: The name of the award to check for. + :param amount: The amount of awards to check for. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + + def return_function_1(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count < amount + + def return_function_2(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count > amount + + def return_function_3(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count <= amount + + def return_function_4(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count >= amount + + def return_function_5(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count == amount + + def return_function_6(item: Union[Submission, Comment]) -> bool: + """Return function for comparison.""" + award_names = [award["name"] for award in item.all_awardings] + try: + index = award_names.index(award_name) + count = item.all_awardings[index]["count"] + except ValueError: + count = 0 + return count != amount + + return symbol_action( + symbol, + return_function_1, + return_function_2, + return_function_3, + return_function_4, + return_function_5, + return_function_6, + ) + + @staticmethod + def filter_content_reply_count(symbol: str, amount: int): + """Filter by the amount of replies that a comment/submission has. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param amount: The amount of replies to check for. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return symbol_action( + symbol, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + < amount, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + > amount, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + <= amount, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + >= amount, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + == amount, + lambda item: ( + len(item.comments) + if isinstance(item, Submission) + else len(item.replies) + ) + != amount, + ) + + def filter_submission_selftext( + self, + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter submissions based on whether or not they are selftexts. + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_true("is_self", submission_only=True) + + def filter_submission_selftext_length( + self, symbol: str, value: int + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by the length of a submission selftext. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the attribute should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_length( + "selftext", symbol, value, submission_only=True + ) + + def filter_submission_title_length( + self, symbol: str, value: int + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter by the length of a submission title. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the title should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_length( + "title", symbol, value, submission_only=True + ) + + def filter_submission_url( + self, + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter submissions based on whether or not they are URL posts. + + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_true("is_self", opposite=True) + + def filter_submission_nsfw(self, negate=False): + """Filter upon the NSFW status of the submission. + + :param negate: Negates the NSFW check, so only non-nsfw submissions + are returned. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_true( + "over_18", opposite=negate, submission_only=True + ) + + def filter_submission_reddit_image( + self, negate=False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter submissions made to ``i.redd.it``. + + :param negate: Return items that are not from ``i.redd.it``. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + redditmedia = self.filter_content_true( + "is_reddit_media_domain", submission_only=True + ) + video = self.filter_content_true("is_video", submission_only=True) + + def filter_func(item: [Union[Comment, Submission]]) -> bool: + """Filter an item. + + :param item: The item to check. + :returns: The status of the check. + """ + result = redditmedia(item) and not (video(item)) + return (not result) if negate else result + + return filter_func + + def filter_submission_reddit_video( + self, negate=False + ) -> Callable[[Union[Submission, Comment]], bool]: + """Filter submissions made to ``v.redd.it``. + + :param negate: Return items that are not from ``i.redd.it``. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + redditmedia = self.filter_content_true( + "is_reddit_media_domain", submission_only=True + ) + video = self.filter_content_true("is_video", submission_only=True) + + def filter_func(item: [Union[Comment, Submission]]) -> bool: + """Filter an item. + + :param item: The item to check. + :returns: The status of the check. + """ + result = redditmedia(item) and (video(item)) + return (not result) if negate else result + + return filter_func + + def filter_comment_body( + self, symbol: str, value: int + ) -> Callable[[Comment], bool]: + """Filter based on the length of the comment's body. + + :param symbol: The comparison symbol. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param value: The value that the body should compare to. + :raises: A :class:`ValueError` if the symbol given is invalid. + :returns: A filter function for use in :meth:`.Filterable.filter`. + """ + return self.filter_content_length("body", symbol, value) diff --git a/prawdditions/util.py b/prawdditions/util.py new file mode 100644 index 0000000..e5e8859 --- /dev/null +++ b/prawdditions/util.py @@ -0,0 +1,121 @@ +"""Constant values and utility function.""" +from typing import Any, Union + +############################################################################### +# Time Manipulation # +############################################################################### + +# Units + +seconds_values = ["s", "sec", "secs", "second", "seconds"] +minutes_values = ["min", "mins", "minute", "minutes"] +hours_values = ["h", "hr", "hrs", "hour", "hours"] +days_values = ["d", "day", "days"] +weeks_values = ["w", "wk", "wks", "week", "weeks"] +months_values = ["mon", "month", "months"] +years_values = ["y", "yr", "yrs", "year", "years"] +all_time_values = ( + seconds_values + + minutes_values + + hours_values + + days_values + + weeks_values + + months_values + + years_values +) + +# Time units in seconds + +second = 1 +minute = second * 60 +hour = minute * 60 +day = hour * 24 +week = day * 7 +month = day * 30 +year = day * 365 + + +# Time functions + + +def get_seconds(amount: Union[int, float], unit: str) -> Union[int, float]: + """Get the representation of the given time value in seconds. + + :param amount: The amount of time to compare by. + :param unit: The unit of time the amount represents. + :raises: A :class:`ValueError` if the unit given is invalid. + :returns: The time converted to seconds. + """ + unit = unit.lower() + if unit not in all_time_values: + raise ValueError( + "The given unit ({!r}) is invalid. The unit has to be one of " + "the following: {}".format( + unit, ", ".join(["{!r}".format(u) for u in all_time_values]) + ) + ) + if unit in seconds_values: + return amount + elif unit in minutes_values: + return amount * minute + elif unit in hours_values: + return amount * hour + elif unit in days_values: + return amount * day + elif unit in weeks_values: + return amount * week + elif unit in months_values: + return amount * month + elif unit in years_values: + return amount * year + + +############################################################################### +# Miscellaneous Functions and Classes # +############################################################################### + + +def symbol_action( + symbol: str, + return_symbol_1: Any, + return_symbol_2: Any, + return_symbol_3: Any, + return_symbol_4: Any, + return_symbol_5: Any, + return_symbol_6: Any, +) -> Any: + """Return 6 different values based on the given symbol. + + Valid symbols: + + :param symbol: The symbol to check for. Currently supported symbols are + the Python comparison symbols ``<``, ``>``, ``<=``, ``>=``, ``==``, + and ``!=``. + :param return_symbol_1: The item to return if the symbol is ``<``. + :param return_symbol_2: The item to return if the symbol is ``>``. + :param return_symbol_3: The item to return if the symbol is ``<=``. + :param return_symbol_4: The item to return if the symbol is ``>=``. + :param return_symbol_5: The item to return if the symbol is ``==``. + :param return_symbol_6: The item to return if the symbol is ``!=``. + :raises: :class:`ValueError` if the given symbol is not one of the 6 + supported symbols. + :returns: Any one of the 6 ``return_symbol`` parameters. + """ + if symbol not in ["<", ">", "<=", ">=", "==", "!="]: + raise ValueError( + "The symbol {!r} is not one of the Python " + "comaprison symbols: ``<``, ``>``, ``<=``, ``>=``, " + "``==``, and ``!=``. ".format(symbol) + ) + elif symbol == "<": + return return_symbol_1 + elif symbol == ">": + return return_symbol_2 + elif symbol == "<=": + return return_symbol_3 + elif symbol == ">=": + return return_symbol_4 + elif symbol == "==": + return return_symbol_5 + elif symbol == "!=": + return return_symbol_6