From 3a18ee6a4df80a5ea00c9a2d36b1f317238b4ea5 Mon Sep 17 00:00:00 2001 From: Steven Marks Date: Fri, 28 Jul 2023 13:59:16 +0000 Subject: [PATCH] feat: added ability to create alias method names --- pyarr/lib/__init__.py | 0 pyarr/lib/alias_decorator.py | 55 ++++++++++++++++++++++++++++++++++++ pyarr/radarr.py | 54 +++++++++++++++++++++++++++++++++++ pyarr/sonarr.py | 11 ++++++-- pyproject.toml | 2 +- tests/test_radarr.py | 38 +++++++++++++++++++++++++ tests/test_sonarr.py | 2 +- 7 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 pyarr/lib/__init__.py create mode 100644 pyarr/lib/alias_decorator.py diff --git a/pyarr/lib/__init__.py b/pyarr/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyarr/lib/alias_decorator.py b/pyarr/lib/alias_decorator.py new file mode 100644 index 0000000..e9c2e3c --- /dev/null +++ b/pyarr/lib/alias_decorator.py @@ -0,0 +1,55 @@ +import functools +from typing import Any, Callable, Dict, Optional, Set +import warnings + + +class FunctionWrapper: + """Function wrapper""" + + def __init__(self, func: Callable[..., Any]) -> None: + self.func = func + self._aliases: Set[str] = set() + + +class alias(object): + """Add an alias to a function""" + + def __init__(self, *aliases: str, deprecated_version: str = None) -> None: + """Constructor + + Args: + deprecated_version (str, optional): Version number that deprecation will happen. Defaults to None. + """ + self.aliases: Set[str] = set(aliases) + self.deprecated_version: Optional[str] = deprecated_version + + def __call__(self, f: Callable[..., Any]) -> FunctionWrapper: + """call""" + wrapped_func = FunctionWrapper(f) + wrapped_func._aliases = self.aliases + + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + """Alias wrapper""" + if self.deprecated_version: + aliases_str = ", ".join(self.aliases) + msg = f"{aliases_str} is deprecated and will be removed in version {self.deprecated_version}. Use {f.__name__} instead." + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + wrapped_func.func = wrapper # Assign wrapper directly to func attribute + return wrapped_func + + +def aliased(aliased_class: Any) -> Any: + """Class has aliases""" + original_methods: Dict[str, Any] = aliased_class.__dict__.copy() + for name, method in original_methods.items(): + if isinstance(method, FunctionWrapper) and hasattr(method, "_aliases"): + for alias in method._aliases: + setattr(aliased_class, alias, method.func) + + # Also replace the original method with the wrapped function + setattr(aliased_class, name, method.func) + + return aliased_class diff --git a/pyarr/radarr.py b/pyarr/radarr.py index 36b00da..5b1220f 100644 --- a/pyarr/radarr.py +++ b/pyarr/radarr.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any, Optional, Union from warnings import warn @@ -634,3 +635,56 @@ def upd_manual_import(self, data: JsonObject) -> JsonObject: JsonObject: Dictionary of updated record """ return self._put("manualimport", self.ver_uri, data=data) + + ## RELEASE + + # GET /release + def get_release(self, id_: Optional[int] = None) -> JsonArray: + """Query indexers for latest releases. + + Args: + id_ (int): Database id for movie to check + + Returns: + JsonArray: List of dictionaries with items + """ + return self._get("release", self.ver_uri, {"movieId": id_} if id_ else None) + + # POST /release + def post_release(self, guid: str, indexer_id: int) -> JsonObject: + """Adds a previously searched release to the download client, if the release is + still in Radarr's search cache (30 minute cache). If the release is not found + in the cache Radarr will return a 404. + + Args: + guid (str): Recently searched result guid + indexer_id (int): Database id of indexer to use + + Returns: + JsonObject: Dictionary with download release details + """ + data = {"guid": guid, "indexerId": indexer_id} + return self._post("release", self.ver_uri, data=data) + + # POST /release/push + def post_release_push( + self, title: str, download_url: str, protocol: str, publish_date: datetime + ) -> Any: + """If the title is wanted, Radarr will grab it. + + Args: + title (str): Release name + download_url (str): .torrent file URL + protocol (str): "Usenet" or "Torrent + publish_date (datetime): ISO8601 date + + Returns: + JSON: Array + """ + data = { + "title": title, + "downloadUrl": download_url, + "protocol": protocol, + "publishDate": publish_date.isoformat(), + } + return self._post("release/push", self.ver_uri, data=data) diff --git a/pyarr/sonarr.py b/pyarr/sonarr.py index 456c209..f0ce53d 100644 --- a/pyarr/sonarr.py +++ b/pyarr/sonarr.py @@ -9,10 +9,12 @@ from pyarr.types import JsonArray, JsonObject from .base import BaseArrAPI +from .lib.alias_decorator import alias, aliased from .models.common import PyarrHistorySortKey, PyarrSortDirection from .models.sonarr import SonarrCommands, SonarrSortKey +@aliased class SonarrAPI(BaseArrAPI): """API wrapper for Sonarr endpoints.""" @@ -398,7 +400,8 @@ def get_parsed_path(self, file_path: str) -> JsonObject: ## RELEASE # GET /release - def get_releases(self, id_: Optional[int] = None) -> JsonArray: + @alias("get_releases", deprecated_version="6.0.0") + def get_release(self, id_: Optional[int] = None) -> JsonArray: """Query indexers for latest releases. Args: @@ -410,7 +413,8 @@ def get_releases(self, id_: Optional[int] = None) -> JsonArray: return self._get("release", self.ver_uri, {"episodeId": id_} if id_ else None) # POST /release - def download_release(self, guid: str, indexer_id: int) -> JsonObject: + @alias("download_release", "6.0.0") + def post_release(self, guid: str, indexer_id: int) -> JsonObject: """Adds a previously searched release to the download client, if the release is still in Sonarr's search cache (30 minute cache). If the release is not found in the cache Sonarr will return a 404. @@ -427,7 +431,8 @@ def download_release(self, guid: str, indexer_id: int) -> JsonObject: # POST /release/push # TODO: find response - def push_release( + @alias("push_release", "6.0.0") + def post_release_push( self, title: str, download_url: str, protocol: str, publish_date: datetime ) -> Any: """If the title is wanted, Sonarr will grab it. diff --git a/pyproject.toml b/pyproject.toml index 575b639..03f43fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyarr" -version = "5.1.2" +version = "5.2.0" description = "Synchronous Sonarr, Radarr, Lidarr and Readarr API's for Python" authors = ["Steven Marks "] license = "MIT" diff --git a/tests/test_radarr.py b/tests/test_radarr.py index 09bd85a..15cc288 100644 --- a/tests/test_radarr.py +++ b/tests/test_radarr.py @@ -840,6 +840,11 @@ def test_get_indexer(radarr_client: RadarrAPI): assert isinstance(data, list) +def test_get_release(radarr_client: RadarrAPI): + data = radarr_client.get_release() + assert isinstance(data, list) + + # TODO: get correct fixture @pytest.mark.usefixtures @responses.activate @@ -902,6 +907,39 @@ def test_upd_manual_import(radarr_mock_client: RadarrAPI): assert isinstance(data, dict) +@pytest.mark.usefixtures +@responses.activate +def test_post_release(radarr_mock_client: RadarrAPI): + responses.add( + responses.POST, + "https://127.0.0.1:7878/api/v3/release", + headers={"Content-Type": "application/json"}, + body=load_fixture("common/blank_dict.json"), + status=201, + ) + data = radarr_mock_client.post_release(guid="1450590", indexer_id=2) + assert isinstance(data, dict) + + +@pytest.mark.usefixtures +@responses.activate +def test_post_release_push(radarr_mock_client: RadarrAPI): + responses.add( + responses.POST, + "https://127.0.0.1:7878/api/v3/release/push", + headers={"Content-Type": "application/json"}, + body=load_fixture("common/blank_dict.json"), + status=201, + ) + data = radarr_mock_client.post_release_push( + title="test", + download_url="https://ipt.beelyrics.net/t/1450590", + protocol="Torrent", + publish_date=datetime(2020, 5, 17), + ) + assert isinstance(data, dict) + + #### DELETES MUST BE LAST diff --git a/tests/test_sonarr.py b/tests/test_sonarr.py index 5f0ec89..c8d36d5 100644 --- a/tests/test_sonarr.py +++ b/tests/test_sonarr.py @@ -830,7 +830,7 @@ def test_get_parsed_path(sonarr_mock_client: SonarrAPI): @pytest.mark.usefixtures @responses.activate -def test_download_release(sonarr_mock_client: SonarrAPI): +def test_post_release(sonarr_mock_client: SonarrAPI): responses.add( responses.POST, "https://127.0.0.1:8989/api/v3/release",