Created
January 29, 2024 10:15
-
-
Save Dr-Blank/b583efc6dbc1301a3c7d33064c50cc08 to your computer and use it in GitHub Desktop.
descriptors for making plex objects' attr able to use in filters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from dataclasses import dataclass | |
from pprint import pprint | |
from typing import Any, Dict, List, Type, TypeVar, Union | |
@dataclass | |
class SimpleFilter: | |
"""a filter for the `key` with the `value`""" | |
key: str | |
value: Any | |
operator: str = "" | |
def to_dict(self) -> Dict[str, Any]: | |
"""generate a dict that can be used as a filter for plexapi""" | |
return {f"{self.key}{self.operator}": self.value} | |
def __or__(self, other: SimpleFilter): | |
return build_logical_filter(self, other, Or) | |
def __and__(self, other: SimpleFilter): | |
return build_logical_filter(self, other, And) | |
class PlexFilterableAttribute: | |
"""Base class for Plex attributes that can be used in filters.""" | |
def __init__(self, key: Union[str, None] = None): | |
self._key: str = key # type: ignore[assignment] | |
self._name: str | |
def __set_name__(self, owner: Any, name: str): | |
self._name = f"__{name}" | |
if self._key is not None: # type: ignore[unreachable] | |
return | |
if hasattr(owner, "_plex_type"): | |
owner_name = owner._plex_type | |
else: | |
owner_name = str(owner.__name__).lower() | |
self._key = f"{owner_name}.{name}" if owner_name else name | |
def __get__(self, instance: Any, _): | |
if instance is None: | |
return self | |
return getattr(instance, self._name) | |
def __set__(self, instance: Any, value: Any): | |
setattr(instance, self._name, value) | |
class FilterAttrSupportsEquality(PlexFilterableAttribute): | |
"""Base class for Plex filter attributes that support equality.""" | |
def __eq__(self, other: Any): # type: ignore | |
return SimpleFilter(self._key, other, operator="") | |
def __ne__(self, other: Any): # type: ignore | |
return SimpleFilter(self._key, other, operator="!") | |
class FilterAttrSupportsComparison(PlexFilterableAttribute): | |
def __gt__(self, other: Any): | |
return SimpleFilter(self._key, other, operator=">>") | |
def __lt__(self, other: Any): | |
return SimpleFilter(self._key, other, operator="<<") | |
class IntAttr( | |
FilterAttrSupportsEquality, | |
FilterAttrSupportsComparison, | |
): | |
"""Plex attribute that is an integer.""" | |
class StrAttr( | |
PlexFilterableAttribute, | |
): | |
"""Plex attribute that is a string.""" | |
def contains(self, other: str): | |
"""Filter for a attr that contains the given str.""" | |
return SimpleFilter(self._key, other, operator="") | |
def not_contains(self, other: Any): | |
"""Filter for a attr that does not contain the given str.""" | |
return SimpleFilter(self._key, other, operator="!") | |
def __eq__(self, other: Any): # type: ignore | |
"""is exactly equal to the given str.""" | |
return SimpleFilter(self._key, other, operator="=") | |
def __ne__(self, other: Any): # type: ignore | |
"""is not equal to the given str.""" | |
return SimpleFilter(self._key, other, operator="!=") | |
def startswith(self, other: Any): | |
"""Filter for a attr that starts with the given str.""" | |
return SimpleFilter(self._key, other, operator="<") | |
def endswith(self, other: Any): | |
"""Filter for a attr that ends with the given str.""" | |
return SimpleFilter(self._key, other, operator=">") | |
class BoolAttr(FilterAttrSupportsEquality): | |
"""Plex attribute that is a boolean.""" | |
class DatetimeAttr(FilterAttrSupportsComparison): | |
"""Plex attribute that is a datetime.""" | |
class Filters: | |
"""builds filters for plexapi""" | |
def __init__(self, *args: SimpleFilter): | |
self.concrete_filters = args | |
def to_dict(self) -> Dict[str, Any]: | |
"""generate a dict that can be used as a filter for plexapi""" | |
result: Dict[str, Any] = {} | |
for _filter in self.concrete_filters: | |
result.update(_filter.to_dict()) | |
return result | |
class LogicalFilter(Filters, SimpleFilter): | |
"""a filter that combines multiple filters with a logical operator""" | |
operator: str | |
def to_dict(self): | |
return {self.operator: [x.to_dict() for x in self.concrete_filters]} | |
LogicalFilterType = TypeVar("LogicalFilterType", bound=LogicalFilter) | |
def build_logical_filter( | |
x: SimpleFilter, | |
y: SimpleFilter, | |
of_type: Type[LogicalFilterType], | |
) -> LogicalFilterType: | |
_filters_to_combine: List[SimpleFilter] = [x] | |
if isinstance(x, of_type): | |
_filters_to_combine = list(x.concrete_filters) | |
if isinstance(y, of_type): | |
_filters_to_combine.extend(y.concrete_filters) | |
else: | |
_filters_to_combine.append(y) | |
return of_type(*_filters_to_combine) | |
class And(LogicalFilter): | |
"""a filter that combines multiple filters with a logical and""" | |
operator = "and" | |
class Or(LogicalFilter): | |
"""a filter that combines multiple filters with a logical or""" | |
operator = "or" | |
class AddedAtSupportedAttr: | |
addedAt = DatetimeAttr() | |
class DurationSupportedAttr: | |
duration = IntAttr() | |
inProgress = BoolAttr() | |
class Track(AddedAtSupportedAttr, DurationSupportedAttr): | |
"""a track in plex""" | |
class Show(AddedAtSupportedAttr): | |
collection = StrAttr() | |
class Episode(AddedAtSupportedAttr, DurationSupportedAttr): | |
"""an episode in plex""" | |
class Artist(AddedAtSupportedAttr): | |
genre = StrAttr() | |
mood = StrAttr() | |
class Album: | |
decade = IntAttr() | |
def main(): | |
rock_artist = Artist.genre == "Rock" | |
pop_artist = Artist.genre == "Pop" | |
and_filters = Filters( | |
(Show.collection.contains("Rocky") & (Episode.inProgress == True)) | |
& (rock_artist | pop_artist | (Track.addedAt > "-24h")) | |
) | |
filter2 = Filters(Track.duration > 100, Album.decade == 1980) | |
rock_or_pop = rock_artist | pop_artist | |
print(filter2.to_dict()) | |
print(rock_or_pop.to_dict()) | |
pprint(and_filters.to_dict()) | |
artist_1 = Artist() | |
artist_1.genre = "Rock" | |
print(artist_1.genre) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment