From b84d0e13a7a9c9aa42564de21cd36f44c2f7f8cd Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 9 Oct 2023 14:25:54 -0400 Subject: [PATCH] Integrate typing into source --- .pre-commit-config.yaml | 27 +- src/qbittorrentapi/_attrdict.py | 98 +- src/qbittorrentapi/_attrdict.pyi | 37 - src/qbittorrentapi/_types.pyi | 29 - src/qbittorrentapi/_version_support.py | 38 +- src/qbittorrentapi/_version_support.pyi | 28 - src/qbittorrentapi/app.py | 144 +-- src/qbittorrentapi/app.pyi | 96 -- src/qbittorrentapi/auth.py | 73 +- src/qbittorrentapi/auth.pyi | 47 - src/qbittorrentapi/client.py | 33 +- src/qbittorrentapi/client.pyi | 46 - src/qbittorrentapi/decorators.py | 84 +- src/qbittorrentapi/decorators.pyi | 37 - src/qbittorrentapi/definitions.py | 88 +- src/qbittorrentapi/definitions.pyi | 106 -- src/qbittorrentapi/exceptions.py | 20 +- src/qbittorrentapi/exceptions.pyi | 36 - src/qbittorrentapi/log.py | 117 ++- src/qbittorrentapi/log.pyi | 94 -- src/qbittorrentapi/py.typed | 0 src/qbittorrentapi/request.py | 569 +++++++---- src/qbittorrentapi/request.pyi | 238 ----- src/qbittorrentapi/rss.py | 183 +++- src/qbittorrentapi/rss.pyi | 179 ---- src/qbittorrentapi/search.py | 203 +++- src/qbittorrentapi/search.pyi | 175 ---- src/qbittorrentapi/sync.py | 80 +- src/qbittorrentapi/sync.pyi | 63 -- src/qbittorrentapi/torrents.py | 1232 +++++++++++++++-------- src/qbittorrentapi/torrents.pyi | 949 ----------------- src/qbittorrentapi/transfer.py | 141 ++- src/qbittorrentapi/transfer.pyi | 149 --- tests/conftest.py | 4 + tests/test_definitions.py | 30 +- tests/test_log.py | 10 +- tests/test_request.py | 41 +- tests/test_torrent.py | 7 +- tests/utils.py | 3 +- 39 files changed, 2105 insertions(+), 3429 deletions(-) delete mode 100644 src/qbittorrentapi/_attrdict.pyi delete mode 100644 src/qbittorrentapi/_types.pyi delete mode 100644 src/qbittorrentapi/_version_support.pyi delete mode 100644 src/qbittorrentapi/app.pyi delete mode 100644 src/qbittorrentapi/auth.pyi delete mode 100644 src/qbittorrentapi/client.pyi delete mode 100644 src/qbittorrentapi/decorators.pyi delete mode 100644 src/qbittorrentapi/definitions.pyi delete mode 100644 src/qbittorrentapi/exceptions.pyi delete mode 100644 src/qbittorrentapi/log.pyi delete mode 100644 src/qbittorrentapi/py.typed delete mode 100644 src/qbittorrentapi/request.pyi delete mode 100644 src/qbittorrentapi/rss.pyi delete mode 100644 src/qbittorrentapi/search.pyi delete mode 100644 src/qbittorrentapi/sync.pyi delete mode 100644 src/qbittorrentapi/torrents.pyi delete mode 100644 src/qbittorrentapi/transfer.pyi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5c36ea3..4a123e3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,32 +52,17 @@ repos: rev: v1.5.1 hooks: - id: mypy - files: '.*\.pyi' + files: ^src/ additional_dependencies: - types-requests - types-six + - packaging args: - --strict - - --disallow-any-unimported - - --disallow-any-expr - - --disallow-any-decorated +# - --disallow-any-unimported +# - --disallow-any-expr +# - --disallow-any-decorated - --warn-unreachable - - --warn-unused-ignores - - --warn-redundant-casts - --strict-optional - --show-traceback - - - repo: local - hooks: - - id: stubtest - name: mypy.stubtest - language: system - entry: stubtest - args: - - qbittorrentapi - - --allowlist=tests/_resources/mypy_stubtest_allowlist.txt - pass_filenames: false - types_or: - - python - - text - files: '.*\.pyi?' + - --implicit-reexport diff --git a/src/qbittorrentapi/_attrdict.py b/src/qbittorrentapi/_attrdict.py index ebded754..a4e29a0d 100644 --- a/src/qbittorrentapi/_attrdict.py +++ b/src/qbittorrentapi/_attrdict.py @@ -27,16 +27,26 @@ Since AttrDict is abandoned, I've consolidated the code here for future use. AttrMap and AttrDefault are left for posterity but commented out. """ +from __future__ import annotations from abc import ABCMeta from abc import abstractmethod -from collections.abc import Mapping -from collections.abc import MutableMapping -from collections.abc import Sequence from re import match as re_match +from typing import Any +from typing import Dict +from typing import Mapping +from typing import MutableMapping +from typing import Sequence +from typing import TypeVar +K = TypeVar("K") +V = TypeVar("V") +T = TypeVar("T") +KOther = TypeVar("KOther") +VOther = TypeVar("VOther") -def merge(left, right): + +def merge(left: Mapping[K, V], right: Mapping[K, V]) -> dict[K, V]: """ Merge two mappings objects together, combining overlapping Mappings, and favoring right-values. @@ -64,17 +74,17 @@ def merge(left, right): left_value = left[key] right_value = right[key] - if isinstance(left_value, Mapping) and isinstance( - right_value, Mapping - ): # recursive merge - merged[key] = merge(left_value, right_value) - else: # overwrite with right value + # recursive merge + if isinstance(left_value, Mapping) and isinstance(right_value, Mapping): + merged[key] = merge(left_value, right_value) # type: ignore + # overwrite with right value + else: merged[key] = right_value return merged -class Attr(Mapping, metaclass=ABCMeta): +class Attr(Mapping[K, V], metaclass=ABCMeta): """ A ``mixin`` class for a mapping that allows for attribute-style access of values. @@ -98,12 +108,13 @@ class Attr(Mapping, metaclass=ABCMeta): """ @abstractmethod - def _configuration(self): + def _configuration(self) -> Any: """All required state for building a new instance with the same settings as the current object.""" @classmethod - def _constructor(cls, mapping, configuration): + @abstractmethod + def _constructor(cls, mapping: Mapping[K, V], configuration: Any) -> Attr[K, V]: """ A standardized constructor used internally by Attr. @@ -112,9 +123,8 @@ def _constructor(cls, mapping, configuration): that will allow nested assignment (e.g., attr.foo.bar = baz) configuration: The return value of Attr._configuration """ - raise NotImplementedError("You need to implement this") - def __call__(self, key): + def __call__(self, key: K) -> Attr[K, V]: """ Dynamically access a key-value pair. @@ -125,25 +135,21 @@ def __call__(self, key): """ if key not in self: raise AttributeError( - "'{cls} instance has no attribute '{name}'".format( - cls=self.__class__.__name__, name=key - ) + f"'{self.__class__.__name__} instance has no attribute '{key}'" ) return self._build(self[key]) - def __getattr__(self, key): + def __getattr__(self, key: Any) -> Any: """Access an item as an attribute.""" if key not in self or not self._valid_name(key): raise AttributeError( - "'{cls}' instance has no attribute '{name}'".format( - cls=self.__class__.__name__, name=key - ) + f"'{self.__class__.__name__}' instance has no attribute '{key}'" ) return self._build(self[key]) - def __add__(self, other): + def __add__(self, other: Mapping[K, V]) -> Attr[K, V]: """ Add a mapping to this Attr, creating a new, merged Attr. @@ -152,11 +158,11 @@ def __add__(self, other): NOTE: Addition is not commutative. a + b != b + a. """ if not isinstance(other, Mapping): - return NotImplemented + return NotImplemented # type: ignore return self._constructor(merge(self, other), self._configuration()) - def __radd__(self, other): + def __radd__(self, other: Mapping[K, V]) -> Attr[K, V]: """ Add this Attr to a mapping, creating a new, merged Attr. @@ -165,11 +171,11 @@ def __radd__(self, other): NOTE: Addition is not commutative. a + b != b + a. """ if not isinstance(other, Mapping): - return NotImplemented + return NotImplemented # type: ignore return self._constructor(merge(other, self), self._configuration()) - def _build(self, obj): + def _build(self, obj: Any) -> AttrDict[K, V]: """ Conditionally convert an object to allow for recursive mapping access. @@ -184,14 +190,13 @@ def _build(self, obj): obj = self._constructor(obj, self._configuration()) elif isinstance(obj, Sequence) and not isinstance(obj, (str, bytes)): sequence_type = getattr(self, "_sequence_type", None) - if sequence_type: obj = sequence_type(self._build(element) for element in obj) - return obj + return obj # type: ignore @classmethod - def _valid_name(cls, key): + def _valid_name(cls, key: Any) -> bool: """ Check whether a key is a valid attribute name. @@ -202,23 +207,22 @@ def _valid_name(cls, key): those would be 'get', 'items', 'keys', 'values', 'mro', and 'register'). """ - return ( + return bool( isinstance(key, str) and re_match("^[A-Za-z][A-Za-z0-9_]*$", key) and not hasattr(cls, key) ) -class MutableAttr(Attr, MutableMapping, metaclass=ABCMeta): - """A ``mixin`` class for a mapping that allows for attribute-style access of - values.""" +class MutableAttr(Attr[K, V], MutableMapping[K, V], metaclass=ABCMeta): + """A ``mixin`` mapping class that allows for attribute-style access of values.""" - def _setattr(self, key, value): + def _setattr(self, key: str, value: Any) -> None: """Add an attribute to the object, without attempting to add it as a key to the mapping.""" super().__setattr__(key, value) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: V) -> None: """ Add an attribute. @@ -226,7 +230,7 @@ def __setattr__(self, key, value): value: The attributes contents """ if self._valid_name(key): - self[key] = value + self[key] = value # type: ignore elif getattr(self, "_allow_invalid_attributes", True): super().__setattr__(key, value) else: @@ -236,19 +240,19 @@ def __setattr__(self, key, value): ) ) - def _delattr(self, key): + def _delattr(self, key: str) -> None: """Delete an attribute from the object, without attempting to remove it from the mapping.""" super().__delattr__(key) - def __delattr__(self, key, force=False): + def __delattr__(self, key: str, force: bool = False) -> None: # type: ignore """ Delete an attribute. key: The name of the attribute """ if self._valid_name(key): - del self[key] + del self[key] # type: ignore elif getattr(self, "_allow_invalid_attributes", True): super().__delattr__(key) else: @@ -259,35 +263,35 @@ def __delattr__(self, key, force=False): ) -class AttrDict(dict, MutableAttr): +class AttrDict(Dict[K, V], MutableAttr[K, V]): """A dict that implements MutableAttr.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._setattr("_sequence_type", tuple) self._setattr("_allow_invalid_attributes", False) - def _configuration(self): + def _configuration(self) -> Any: """The configuration for an attrmap instance.""" return self._sequence_type - def __getstate__(self): + def __getstate__(self) -> Any: """Serialize the object.""" return self.copy(), self._sequence_type, self._allow_invalid_attributes - def __setstate__(self, state): + def __setstate__(self, state: Any) -> None: """Deserialize the object.""" mapping, sequence_type, allow_invalid_attributes = state self.update(mapping) self._setattr("_sequence_type", sequence_type) self._setattr("_allow_invalid_attributes", allow_invalid_attributes) - def __repr__(self): - return f"AttrDict({super().__repr__()})" + def __repr__(self) -> str: + return f"{self.__class__.__name__}({super().__repr__()})" @classmethod - def _constructor(cls, mapping, configuration): + def _constructor(cls, mapping: Mapping[K, V], configuration: Any) -> AttrDict[K, V]: """A standardized constructor.""" attr = cls(mapping) attr._setattr("_sequence_type", configuration) diff --git a/src/qbittorrentapi/_attrdict.pyi b/src/qbittorrentapi/_attrdict.pyi deleted file mode 100644 index e6e7dd1d..00000000 --- a/src/qbittorrentapi/_attrdict.pyi +++ /dev/null @@ -1,37 +0,0 @@ -from abc import ABCMeta -from typing import Any -from typing import Dict -from typing import Mapping -from typing import MutableMapping -from typing import Text -from typing import TypeVar - -K = TypeVar("K") -KOther = TypeVar("KOther") -V = TypeVar("V") -VOther = TypeVar("VOther") -KwargsT = Any - -def merge( - left: Mapping[K, V], - right: Mapping[KOther, VOther], -) -> Dict[K | KOther, V | VOther]: ... - -class Attr(Mapping[K, V], metaclass=ABCMeta): - def __call__(self, key: K) -> V: ... - def __getattr__(self, key: Text) -> V: ... - def __add__( - self, - other: Mapping[KOther, VOther], - ) -> Attr[K | KOther, V | VOther]: ... - def __radd__( - self, - other: Mapping[KOther, VOther], - ) -> Attr[K | KOther, V | VOther]: ... - -class MutableAttr(Attr[K, V], MutableMapping[K, V], metaclass=ABCMeta): - def __setattr__(self, key: Text, value: V) -> None: ... - def __delattr__(self, key: Text, force: bool = ...) -> None: ... - -class AttrDict(Dict[K, V], MutableAttr[K, V]): - def __init__(self, *args: Any, **kwargs: KwargsT) -> None: ... diff --git a/src/qbittorrentapi/_types.pyi b/src/qbittorrentapi/_types.pyi deleted file mode 100644 index 56fbc62d..00000000 --- a/src/qbittorrentapi/_types.pyi +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Any -from typing import Iterable -from typing import Mapping -from typing import MutableMapping -from typing import Sequence -from typing import Text -from typing import Tuple -from typing import Union - -from qbittorrentapi.definitions import Dictionary - -KwargsT = Any -# Type to define JSON -JsonValueT = Union[ - None, - int, - Text, - bool, - Sequence[JsonValueT], - Mapping[Text, JsonValueT], -] -JsonDictionaryT = Dictionary[Text, JsonValueT] -# Type for inputs to build a Dictionary -DictInputT = Mapping[Text, JsonValueT] -DictMutableInputT = MutableMapping[Text, JsonValueT] -# Type for inputs to build a List -ListInputT = Iterable[Mapping[Text, JsonValueT]] -# Type for `files` in requests.get()/post() -FilesToSendT = Mapping[Text, bytes | Tuple[Text, bytes]] diff --git a/src/qbittorrentapi/_version_support.py b/src/qbittorrentapi/_version_support.py index 8e88af89..52491252 100644 --- a/src/qbittorrentapi/_version_support.py +++ b/src/qbittorrentapi/_version_support.py @@ -1,8 +1,12 @@ +from __future__ import annotations + from functools import lru_cache +from typing import Final +from typing import Literal -from packaging.version import Version as _Version +import packaging.version -APP_VERSION_2_API_VERSION_MAP = { +APP_VERSION_2_API_VERSION_MAP: dict[str, str] = { "v4.1.0": "2.0", "v4.1.1": "2.0.1", "v4.1.2": "2.0.2", @@ -51,14 +55,14 @@ "v4.5.5": "2.8.19", } -MOST_RECENT_SUPPORTED_APP_VERSION = "v4.5.5" -MOST_RECENT_SUPPORTED_API_VERSION = "2.8.19" +MOST_RECENT_SUPPORTED_APP_VERSION: Final[Literal["v4.5.5"]] = "v4.5.5" +MOST_RECENT_SUPPORTED_API_VERSION: Final[Literal["2.8.19"]] = "2.8.19" @lru_cache(maxsize=None) -def v(version): +def v(version: str) -> packaging.version.Version: """Caching version parser.""" - return _Version(version) + return packaging.version.Version(version) class Version: @@ -72,25 +76,25 @@ class Version: notable exceptions. """ - _supported_app_versions = None - _supported_api_versions = None + _supported_app_versions: set[str] | None = None + _supported_api_versions: set[str] | None = None @classmethod - def supported_app_versions(cls): + def supported_app_versions(cls) -> set[str]: """Set of all supported qBittorrent application versions.""" if cls._supported_app_versions is None: cls._supported_app_versions = set(APP_VERSION_2_API_VERSION_MAP.keys()) return cls._supported_app_versions @classmethod - def supported_api_versions(cls): + def supported_api_versions(cls) -> set[str]: """Set of all supported qBittorrent Web API versions.""" if cls._supported_api_versions is None: cls._supported_api_versions = set(APP_VERSION_2_API_VERSION_MAP.values()) return cls._supported_api_versions @classmethod - def is_app_version_supported(cls, app_version): + def is_app_version_supported(cls, app_version: str) -> bool: """ Returns whether a version of the qBittorrent application is fully supported by this API client. @@ -104,7 +108,7 @@ def is_app_version_supported(cls, app_version): return app_version in cls.supported_app_versions() @classmethod - def is_api_version_supported(cls, api_version): + def is_api_version_supported(cls, api_version: str) -> bool: """ Returns whether a version of the qBittorrent Web API is fully supported by this API client. @@ -118,13 +122,11 @@ def is_api_version_supported(cls, api_version): return api_version in Version.supported_api_versions() @classmethod - def latest_supported_app_version(cls): - """Returns the most recent version of qBittorrent application that is fully - supported.""" + def latest_supported_app_version(cls) -> str: + """Returns the most recent version of qBittorrent that is supported.""" return MOST_RECENT_SUPPORTED_APP_VERSION @classmethod - def latest_supported_api_version(cls): - """Returns the most recent version of qBittorrent Web API that is fully - supported.""" + def latest_supported_api_version(cls) -> str: + """Returns the most recent version of qBittorrent Web API that is supported.""" return MOST_RECENT_SUPPORTED_API_VERSION diff --git a/src/qbittorrentapi/_version_support.pyi b/src/qbittorrentapi/_version_support.pyi deleted file mode 100644 index 1ad4202e..00000000 --- a/src/qbittorrentapi/_version_support.pyi +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Dict -from typing import Optional -from typing import Set -from typing import Text - -from packaging.version import Version as _Version # type: ignore - -MOST_RECENT_SUPPORTED_APP_VERSION: Text -MOST_RECENT_SUPPORTED_API_VERSION: Text -APP_VERSION_2_API_VERSION_MAP: Dict[Text, Text] - -def v(version: Text) -> _Version: ... # type: ignore - -class Version: - _supported_app_versions: Optional[Set[str]] = None - _supported_api_versions: Optional[Set[str]] = None - @classmethod - def supported_app_versions(cls) -> Set[str]: ... - @classmethod - def supported_api_versions(cls) -> Set[str]: ... - @classmethod - def is_app_version_supported(cls, app_version: Text) -> bool: ... - @classmethod - def is_api_version_supported(cls, api_version: Text) -> bool: ... - @classmethod - def latest_supported_app_version(cls) -> str: ... - @classmethod - def latest_supported_api_version(cls) -> str: ... diff --git a/src/qbittorrentapi/app.py b/src/qbittorrentapi/app.py index f0c8ef63..f06fe23e 100644 --- a/src/qbittorrentapi/app.py +++ b/src/qbittorrentapi/app.py @@ -1,48 +1,59 @@ +from __future__ import annotations + from json import dumps +from logging import Logger from logging import getLogger +from typing import Any +from typing import Iterable +from typing import Mapping +from typing import Union from qbittorrentapi.auth import AuthAPIMixIn from qbittorrentapi.decorators import alias from qbittorrentapi.decorators import aliased from qbittorrentapi.decorators import endpoint_introduced from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import JsonValueT from qbittorrentapi.definitions import List from qbittorrentapi.definitions import ListEntry +from qbittorrentapi.definitions import ListInputT -logger = getLogger(__name__) +logger: Logger = getLogger(__name__) -class ApplicationPreferencesDictionary(Dictionary): - """Response for :meth:`~AppAPIMixIn.app_preferences`""" +class ApplicationPreferencesDictionary(Dictionary[str, JsonValueT]): + """Response for :meth:`~AppAPIMixIn.app_preferences`.""" -class BuildInfoDictionary(Dictionary): - """Response for :meth:`~AppAPIMixIn.app_build_info`""" +class BuildInfoDictionary(Dictionary[str, Union[str, int]]): + """Response for :meth:`~AppAPIMixIn.app_build_info`.""" -class NetworkInterfaceList(List): - """Response for :meth:`~AppAPIMixIn.app_network_interface_list`""" +class NetworkInterface(ListEntry): + """Item in :class:`NetworkInterfaceList`.""" - def __init__(self, list_entries, client=None): - super().__init__(list_entries, entry_class=NetworkInterface, client=client) +class NetworkInterfaceList(List[NetworkInterface]): + """Response for :meth:`~AppAPIMixIn.app_network_interface_list`.""" -class NetworkInterface(ListEntry): - """Item in :class:`NetworkInterfaceList`""" + def __init__(self, list_entries: ListInputT, client: AppAPIMixIn | None = None): + super().__init__(list_entries, entry_class=NetworkInterface) -class NetworkInterfaceAddressList(List): - """Response for :meth:`~AppAPIMixIn.app_network_interface_address_list`""" +# only API response that's a list of strings...so just ignore the typing for now +class NetworkInterfaceAddressList(List[str]): # type: ignore + """Response for :meth:`~AppAPIMixIn.app_network_interface_address_list`.""" - def __init__(self, list_entries, client=None): - super().__init__(list_entries) + def __init__(self, list_entries: Iterable[str], client: AppAPIMixIn | None = None): + super().__init__(list_entries) # type: ignore @aliased -class Application(ClientCache): +class Application(ClientCache["AppAPIMixIn"]): """ Allows interaction with ``Application`` API endpoints. @@ -66,61 +77,69 @@ class Application(ClientCache): """ @property - def version(self): - """Implements :meth:`~AppAPIMixIn.app_version`""" + def version(self) -> str: + """Implements :meth:`~AppAPIMixIn.app_version`.""" return self._client.app_version() @property - def web_api_version(self): - """Implements :meth:`~AppAPIMixIn.app_web_api_version`""" + def web_api_version(self) -> str: + """Implements :meth:`~AppAPIMixIn.app_web_api_version`.""" return self._client.app_web_api_version() webapiVersion = web_api_version @property - def build_info(self): - """Implements :meth:`~AppAPIMixIn.app_build_info`""" + def build_info(self) -> BuildInfoDictionary: + """Implements :meth:`~AppAPIMixIn.app_build_info`.""" return self._client.app_build_info() buildInfo = build_info - def shutdown(self): - """Implements :meth:`~AppAPIMixIn.app_shutdown`""" - return self._client.app_shutdown() + def shutdown(self, **kwargs: APIKwargsT) -> None: + """Implements :meth:`~AppAPIMixIn.app_shutdown`.""" + self._client.app_shutdown(**kwargs) @property - def preferences(self): + def preferences(self) -> ApplicationPreferencesDictionary: """Implements :meth:`~AppAPIMixIn.app_preferences` and - :meth:`~AppAPIMixIn.app_set_preferences`""" + :meth:`~AppAPIMixIn.app_set_preferences`.""" return self._client.app_preferences() @preferences.setter - def preferences(self, value): - """Implements :meth:`~AppAPIMixIn.app_set_preferences`""" + def preferences(self, value: Mapping[str, Any]) -> None: + """Implements :meth:`~AppAPIMixIn.app_set_preferences`.""" self.set_preferences(prefs=value) @alias("setPreferences") - def set_preferences(self, prefs=None, **kwargs): - """Implements :meth:`~AppAPIMixIn.app_set_preferences`""" - return self._client.app_set_preferences(prefs=prefs, **kwargs) + def set_preferences( + self, + prefs: Mapping[str, Any] | None = None, + **kwargs: APIKwargsT, + ) -> None: + """Implements :meth:`~AppAPIMixIn.app_set_preferences`.""" + self._client.app_set_preferences(prefs=prefs, **kwargs) @property - def default_save_path(self): - """Implements :meth:`~AppAPIMixIn.app_default_save_path`""" + def default_save_path(self) -> str: + """Implements :meth:`~AppAPIMixIn.app_default_save_path`.""" return self._client.app_default_save_path() defaultSavePath = default_save_path @property - def network_interface_list(self): - """Implements :meth:`~AppAPIMixIn.app_network_interface_list`""" + def network_interface_list(self) -> NetworkInterfaceList: + """Implements :meth:`~AppAPIMixIn.app_network_interface_list`.""" return self._client.app_network_interface_list() networkInterfaceList = network_interface_list @alias("networkInterfaceAddressList") - def network_interface_address_list(self, interface_name="", **kwargs): - """Implements :meth:`~AppAPIMixIn.app_network_interface_list`""" + def network_interface_address_list( + self, + interface_name: str = "", + **kwargs: APIKwargsT, + ) -> NetworkInterfaceAddressList: + """Implements :meth:`~AppAPIMixIn.app_network_interface_list`.""" return self._client.app_network_interface_address_list( interface_name=interface_name, **kwargs, @@ -139,8 +158,10 @@ class AppAPIMixIn(AuthAPIMixIn): >>> client.app_preferences() """ + _application: Application | None = None + @property - def app(self): + def app(self) -> Application: """ Allows for transparent interaction with Application endpoints. @@ -154,25 +175,28 @@ def app(self): application = app @login_required - def app_version(self, **kwargs): + def app_version(self, **kwargs: APIKwargsT) -> str: """ Retrieve application version. :return: string """ - return self._get( - _name=APINames.Application, _method="version", response_class=str, **kwargs + return self._get_cast( + _name=APINames.Application, + _method="version", + response_class=str, + **kwargs, ) @alias("app_webapiVersion") @login_required - def app_web_api_version(self, **kwargs): + def app_web_api_version(self, **kwargs: APIKwargsT) -> str: """ Retrieve web API version. :return: string """ - return self._MOCK_WEB_API_VERSION or self._get( + return self._get_cast( _name=APINames.Application, _method="webapiVersion", response_class=str, @@ -182,13 +206,13 @@ def app_web_api_version(self, **kwargs): @alias("app_buildInfo") @endpoint_introduced("2.3", "app/buildInfo") @login_required - def app_build_info(self, **kwargs): + def app_build_info(self, **kwargs: APIKwargsT) -> BuildInfoDictionary: """ Retrieve build info. :return: :class:`BuildInfoDictionary` - ``_ """ # noqa: E501 - return self._get( + return self._get_cast( _name=APINames.Application, _method="buildInfo", response_class=BuildInfoDictionary, @@ -196,18 +220,18 @@ def app_build_info(self, **kwargs): ) @login_required - def app_shutdown(self, **kwargs): + def app_shutdown(self, **kwargs: APIKwargsT) -> None: """Shutdown qBittorrent.""" self._post(_name=APINames.Application, _method="shutdown", **kwargs) @login_required - def app_preferences(self, **kwargs): + def app_preferences(self, **kwargs: APIKwargsT) -> ApplicationPreferencesDictionary: """ Retrieve qBittorrent application preferences. :return: :class:`ApplicationPreferencesDictionary` - ``_ """ # noqa: E501 - return self._get( + return self._get_cast( _name=APINames.Application, _method="preferences", response_class=ApplicationPreferencesDictionary, @@ -216,7 +240,11 @@ def app_preferences(self, **kwargs): @alias("app_setPreferences") @login_required - def app_set_preferences(self, prefs=None, **kwargs): + def app_set_preferences( + self, + prefs: Mapping[str, Any] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set one or more preferences in qBittorrent application. @@ -233,13 +261,13 @@ def app_set_preferences(self, prefs=None, **kwargs): @alias("app_defaultSavePath") @login_required - def app_default_save_path(self, **kwargs): + def app_default_save_path(self, **kwargs: APIKwargsT) -> str: """ Retrieves the default path for where torrents are saved. :return: string """ - return self._get( + return self._get_cast( _name=APINames.Application, _method="defaultSavePath", response_class=str, @@ -249,13 +277,13 @@ def app_default_save_path(self, **kwargs): @alias("app_networkInterfaceList") @endpoint_introduced("2.3", "app/networkInterfaceList") @login_required - def app_network_interface_list(self, **kwargs): + def app_network_interface_list(self, **kwargs: APIKwargsT) -> NetworkInterfaceList: """ Retrieves the list of network interfaces. :return: :class:`NetworkInterfaceList` """ - return self._get( + return self._get_cast( _name=APINames.Application, _method="networkInterfaceList", response_class=NetworkInterfaceList, @@ -265,7 +293,11 @@ def app_network_interface_list(self, **kwargs): @alias("app_networkInterfaceAddressList") @endpoint_introduced("2.3", "app/networkInterfaceAddressList") @login_required - def app_network_interface_address_list(self, interface_name="", **kwargs): + def app_network_interface_address_list( + self, + interface_name: str = "", + **kwargs: APIKwargsT, + ) -> NetworkInterfaceAddressList: """ Retrieves the addresses for a network interface; omit name for all addresses. @@ -273,7 +305,7 @@ def app_network_interface_address_list(self, interface_name="", **kwargs): :return: :class:`NetworkInterfaceAddressList` """ data = {"iface": interface_name} - return self._post( + return self._post_cast( _name=APINames.Application, _method="networkInterfaceAddressList", data=data, diff --git a/src/qbittorrentapi/app.pyi b/src/qbittorrentapi/app.pyi deleted file mode 100644 index 47467682..00000000 --- a/src/qbittorrentapi/app.pyi +++ /dev/null @@ -1,96 +0,0 @@ -from logging import Logger -from typing import Mapping -from typing import Optional -from typing import Text - -import six - -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import JsonValueT -from qbittorrentapi._types import KwargsT -from qbittorrentapi._types import ListInputT -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import List -from qbittorrentapi.definitions import ListEntry -from qbittorrentapi.request import Request - -logger: Logger - -class ApplicationPreferencesDictionary(JsonDictionaryT): ... -class BuildInfoDictionary(JsonDictionaryT): ... -class NetworkInterface(ListEntry): ... - -class NetworkInterfaceList(List[NetworkInterface]): - def __init__( - self, list_entries: ListInputT, client: Optional[AppAPIMixIn] = None - ) -> None: ... - -class NetworkInterfaceAddressList(List[six.text_type]): - def __init__( - self, list_entries: ListInputT, client: Optional[AppAPIMixIn] = None - ) -> None: ... - -class Application(ClientCache): - @property - def version(self) -> Text: ... - @property - def web_api_version(self) -> Text: ... - @property - def webapiVersion(self) -> Text: ... - @property - def build_info(self) -> BuildInfoDictionary: ... - @property - def buildInfo(self) -> BuildInfoDictionary: ... - def shutdown(self) -> None: ... - @property - def preferences(self) -> ApplicationPreferencesDictionary: ... - @preferences.setter - def preferences(self, value: Mapping[Text, JsonValueT]) -> None: ... - def set_preferences( - self, - prefs: Optional[Mapping[Text, JsonValueT]] = None, - **kwargs: KwargsT, - ) -> None: ... - setPreferences = set_preferences - @property - def default_save_path(self) -> Text: ... - @property - def defaultSavePath(self) -> Text: ... - @property - def network_interface_list(self, **kwargs: KwargsT) -> NetworkInterfaceList: ... - @property - def networkInterfaceList(self, **kwargs: KwargsT) -> NetworkInterfaceList: ... - def network_interface_address_list( - self, interface_name: Optional[Text] = "", **kwargs: KwargsT - ) -> NetworkInterfaceAddressList: ... - networkInterfaceAddressList = network_interface_address_list - -class AppAPIMixIn(Request): - @property - def app(self) -> Application: ... - @property - def application(self) -> Application: ... - def app_version(self, **kwargs: KwargsT) -> str: ... - def app_web_api_version(self, **kwargs: KwargsT) -> str: ... - app_webapiVersion = app_web_api_version - def app_build_info(self, **kwargs: KwargsT) -> BuildInfoDictionary: ... - app_buildInfo = app_build_info - def app_shutdown(self, **kwargs: KwargsT) -> None: ... - def app_preferences( - self, - **kwargs: KwargsT, - ) -> ApplicationPreferencesDictionary: ... - def app_set_preferences( - self, - prefs: Optional[Mapping[Text, JsonValueT]] = None, - **kwargs: KwargsT, - ) -> None: ... - app_setPreferences = app_set_preferences - def app_default_save_path(self, **kwargs: KwargsT) -> str: ... - app_defaultSavePath = app_default_save_path - def app_network_interface_list(self, **kwargs: KwargsT) -> NetworkInterfaceList: ... - app_networkInterfaceList = app_network_interface_list - def app_network_interface_address_list( - self, interface_name: Optional[Text] = "", **kwargs: KwargsT - ) -> NetworkInterfaceAddressList: ... - app_networkInterfaceAddressList = app_network_interface_address_list diff --git a/src/qbittorrentapi/auth.py b/src/qbittorrentapi/auth.py index 2a7828b1..3a31ca45 100644 --- a/src/qbittorrentapi/auth.py +++ b/src/qbittorrentapi/auth.py @@ -1,17 +1,28 @@ +from __future__ import annotations + +from logging import Logger from logging import getLogger +from types import TracebackType +from typing import TYPE_CHECKING + +from requests import Response from qbittorrentapi import Version from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.exceptions import LoginFailed from qbittorrentapi.exceptions import UnsupportedQbittorrentVersion from qbittorrentapi.request import Request -logger = getLogger(__name__) +if TYPE_CHECKING: + from qbittorrentapi.client import Client + +logger: Logger = getLogger(__name__) -class Authorization(ClientCache): +class Authorization(ClientCache["AuthAPIMixIn"]): """ Allows interaction with the ``Authorization`` API endpoints. @@ -24,16 +35,21 @@ class Authorization(ClientCache): """ @property - def is_logged_in(self): - """Implements :meth:`~AuthAPIMixIn.is_logged_in`""" + def is_logged_in(self) -> bool: + """Implements :meth:`~AuthAPIMixIn.is_logged_in`.""" return self._client.is_logged_in - def log_in(self, username=None, password=None, **kwargs): - """Implements :meth:`~AuthAPIMixIn.auth_log_in`""" + def log_in( + self, + username: str | None = None, + password: str | None = None, + **kwargs: APIKwargsT, + ) -> None: + """Implements :meth:`~AuthAPIMixIn.auth_log_in`.""" return self._client.auth_log_in(username=username, password=password, **kwargs) - def log_out(self, **kwargs): - """Implements :meth:`~AuthAPIMixIn.auth_log_out`""" + def log_out(self, **kwargs: APIKwargsT) -> None: + """Implements :meth:`~AuthAPIMixIn.auth_log_out`.""" return self._client.auth_log_out(**kwargs) @@ -49,8 +65,10 @@ class AuthAPIMixIn(Request): >>> client.auth_log_out() """ + _authorization: Authorization | None = None + @property - def auth(self): + def auth(self) -> Authorization: """ Allows for transparent interaction with Authorization endpoints. @@ -63,7 +81,7 @@ def auth(self): authorization = auth @property - def is_logged_in(self): + def is_logged_in(self) -> bool: """ Returns True if low-overhead API call succeeds; False otherwise. @@ -79,7 +97,12 @@ def is_logged_in(self): else: return True - def auth_log_in(self, username=None, password=None, **kwargs): + def auth_log_in( + self, + username: str | None = None, + password: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Log in to qBittorrent host. @@ -96,10 +119,11 @@ def auth_log_in(self, username=None, password=None, **kwargs): self._initialize_context() creds = {"username": self.username, "password": self._password} - auth_response = self._post( + auth_response = self._post_cast( _name=APINames.Authorization, _method="login", data=creds, + response_class=Response, **kwargs, ) @@ -110,8 +134,8 @@ def auth_log_in(self, username=None, password=None, **kwargs): # check if the connected qBittorrent is fully supported by this Client yet if self._RAISE_UNSUPPORTEDVERSIONERROR: - app_version = self.app_version() - api_version = self.app_web_api_version() + app_version = self.app_version() # type: ignore + api_version = self.app_web_api_version() # type: ignore if not ( Version.is_api_version_supported(api_version) and Version.is_app_version_supported(app_version) @@ -122,7 +146,7 @@ def auth_log_in(self, username=None, password=None, **kwargs): ) @property - def _SID(self): + def _SID(self) -> str | None: """ Authorization session cookie from qBittorrent using default cookie name `SID`. Backwards compatible for :meth:`~AuthAPIMixIn._session_cookie`. @@ -132,7 +156,7 @@ def _SID(self): """ return self._session_cookie() - def _session_cookie(self, cookie_name="SID"): + def _session_cookie(self, cookie_name: str = "SID") -> str | None: """ Authorization session cookie from qBittorrent. @@ -141,17 +165,22 @@ def _session_cookie(self, cookie_name="SID"): acquired """ if self._http_session: - return self._http_session.cookies.get(cookie_name, None) + return self._http_session.cookies.get(cookie_name, None) # type: ignore return None @login_required - def auth_log_out(self, **kwargs): + def auth_log_out(self, **kwargs: APIKwargsT) -> None: """End session with qBittorrent.""" self._post(_name=APINames.Authorization, _method="logout", **kwargs) - def __enter__(self): + def __enter__(self) -> Client: self.auth_log_in() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): + return self # type: ignore[return-value] + + def __exit__( + self, + exctype: type[BaseException] | None, + excinst: BaseException | None, + exctb: TracebackType | None, + ) -> None: self.auth_log_out() diff --git a/src/qbittorrentapi/auth.pyi b/src/qbittorrentapi/auth.pyi deleted file mode 100644 index c91ca157..00000000 --- a/src/qbittorrentapi/auth.pyi +++ /dev/null @@ -1,47 +0,0 @@ -from logging import Logger -from types import TracebackType -from typing import Optional -from typing import Text -from typing import Type - -from qbittorrentapi._types import KwargsT -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.request import Request - -logger: Logger - -class Authorization(ClientCache): - @property - def is_logged_in(self) -> bool: ... - def log_in( - self, - username: Optional[Text] = None, - password: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - def log_out(self, **kwargs: KwargsT) -> None: ... - -class AuthAPIMixIn(Request): - @property - def auth(self) -> Authorization: ... - @property - def authorization(self) -> Authorization: ... - @property - def is_logged_in(self) -> bool: ... - def auth_log_in( - self, - username: Optional[Text] = None, - password: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - @property - def _SID(self) -> Optional[Text]: ... - def _session_cookie(self, cookie_name: Text = "SID") -> Optional[Text]: ... - def auth_log_out(self, **kwargs: KwargsT) -> None: ... - def __enter__(self) -> "AuthAPIMixIn": ... - def __exit__( - self, - exctype: Optional[Type[BaseException]], - excinst: Optional[BaseException], - exctb: Optional[TracebackType], - ) -> bool: ... diff --git a/src/qbittorrentapi/client.py b/src/qbittorrentapi/client.py index 656d85f4..d9d26629 100644 --- a/src/qbittorrentapi/client.py +++ b/src/qbittorrentapi/client.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +from typing import Any +from typing import Mapping + from qbittorrentapi.log import LogAPIMixIn from qbittorrentapi.rss import RSSAPIMixIn from qbittorrentapi.search import SearchAPIMixIn @@ -99,20 +104,19 @@ class Client( def __init__( self, - host="", - port=None, - username=None, - password=None, - EXTRA_HEADERS=None, - REQUESTS_ARGS=None, - VERIFY_WEBUI_CERTIFICATE=True, - FORCE_SCHEME_FROM_HOST=False, - RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=False, - RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS=False, - VERBOSE_RESPONSE_LOGGING=False, - SIMPLE_RESPONSES=False, - DISABLE_LOGGING_DEBUG_OUTPUT=False, - **kwargs, + host: str = "", + port: str | int | None = None, + username: str | None = None, + password: str | None = None, + EXTRA_HEADERS: Mapping[str, str] | None = None, + REQUESTS_ARGS: Mapping[str, Any] | None = None, + VERIFY_WEBUI_CERTIFICATE: bool = True, + FORCE_SCHEME_FROM_HOST: bool = False, + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool = False, + RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: bool = False, + VERBOSE_RESPONSE_LOGGING: bool = False, + SIMPLE_RESPONSES: bool = False, + DISABLE_LOGGING_DEBUG_OUTPUT: bool = False, ): super().__init__( host=host, @@ -128,5 +132,4 @@ def __init__( VERBOSE_RESPONSE_LOGGING=VERBOSE_RESPONSE_LOGGING, SIMPLE_RESPONSES=SIMPLE_RESPONSES, DISABLE_LOGGING_DEBUG_OUTPUT=DISABLE_LOGGING_DEBUG_OUTPUT, - **kwargs, ) diff --git a/src/qbittorrentapi/client.pyi b/src/qbittorrentapi/client.pyi deleted file mode 100644 index 523c3db7..00000000 --- a/src/qbittorrentapi/client.pyi +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any -from typing import Mapping -from typing import Optional -from typing import Text - -from qbittorrentapi._types import KwargsT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.auth import AuthAPIMixIn -from qbittorrentapi.log import LogAPIMixIn -from qbittorrentapi.request import Request -from qbittorrentapi.rss import RSSAPIMixIn -from qbittorrentapi.search import SearchAPIMixIn -from qbittorrentapi.sync import SyncAPIMixIn -from qbittorrentapi.torrents import TorrentsAPIMixIn -from qbittorrentapi.transfer import TransferAPIMixIn - -class Client( - LogAPIMixIn, - SyncAPIMixIn, - TransferAPIMixIn, - TorrentsAPIMixIn, - RSSAPIMixIn, - SearchAPIMixIn, - AuthAPIMixIn, - AppAPIMixIn, - Request, -): - def __init__( - self, - host: Text = "", - port: Optional[Text | int] = None, - username: Optional[Text] = None, - password: Optional[Text] = None, - EXTRA_HEADERS: Optional[Mapping[Text, Text]] = None, - REQUESTS_ARGS: Optional[Mapping[Text, Any]] = None, - VERIFY_WEBUI_CERTIFICATE: Optional[bool] = True, - FORCE_SCHEME_FROM_HOST: Optional[bool] = False, - RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: Optional[ - bool - ] = False, - RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: Optional[bool] = False, - VERBOSE_RESPONSE_LOGGING: Optional[bool] = False, - SIMPLE_RESPONSES: Optional[bool] = False, - DISABLE_LOGGING_DEBUG_OUTPUT: Optional[bool] = False, - **kwargs: KwargsT, - ) -> None: ... diff --git a/src/qbittorrentapi/decorators.py b/src/qbittorrentapi/decorators.py index bf469d08..adc64b88 100644 --- a/src/qbittorrentapi/decorators.py +++ b/src/qbittorrentapi/decorators.py @@ -1,10 +1,42 @@ +from __future__ import annotations + from functools import wraps +from logging import Logger from logging import getLogger +from typing import TYPE_CHECKING +from typing import Callable +from typing import TypeVar +from typing import Union from qbittorrentapi._version_support import v from qbittorrentapi.exceptions import HTTP403Error - -logger = getLogger(__name__) +from qbittorrentapi.request import Request + +if TYPE_CHECKING: + from qbittorrentapi.app import Application + from qbittorrentapi.rss import RSS + from qbittorrentapi.search import Search + from qbittorrentapi.torrents import TorrentCategories + from qbittorrentapi.torrents import TorrentDictionary + from qbittorrentapi.torrents import TorrentTags + from qbittorrentapi.transfer import Transfer + +APIClassT = TypeVar( + "APIClassT", + bound=Union[ + "Application", + "Request", + "RSS", + "Search", + "TorrentDictionary", + "TorrentCategories", + "TorrentTags", + "Transfer", + ], +) +APIReturnValueT = TypeVar("APIReturnValueT") + +logger: Logger = getLogger(__name__) class alias: @@ -20,10 +52,12 @@ def shout(message): # .... """ - def __init__(self, *aliases): - self.aliases = set(aliases) + def __init__(self, *aliases: str): + self.aliases: set[str] = set(aliases) - def __call__(self, func): + def __call__( + self, func: Callable[..., APIReturnValueT] + ) -> Callable[..., APIReturnValueT]: """ Method call wrapper. @@ -33,11 +67,11 @@ def __call__(self, func): of decorator, this method must return the callable that will wrap the decorated function. """ - func._aliases = self.aliases + func._aliases = self.aliases # type: ignore return func -def aliased(aliased_class): +def aliased(aliased_class: type[APIClassT]) -> type[APIClassT]: """ Decorator function that *must* be used in combination with @alias decorator. This class will make the magic happen! @@ -69,10 +103,12 @@ def boring_method(): return aliased_class -def login_required(func): +def login_required( + func: Callable[..., APIReturnValueT] +) -> Callable[..., APIReturnValueT]: """Ensure client is logged in when calling API methods.""" - def get_requests_kwargs(**kwargs): + def get_requests_kwargs(**kwargs): # type: ignore """Extract kwargs for performing transparent qBittorrent login.""" return { "requests_args": kwargs.get("requests_args"), @@ -81,7 +117,7 @@ def get_requests_kwargs(**kwargs): } @wraps(func) - def wrapper(client, *args, **kwargs): + def wrapper(client, *args, **kwargs): # type: ignore """ Attempt API call; 403 is returned if the login is expired or the user is banned. @@ -91,13 +127,15 @@ def wrapper(client, *args, **kwargs): return func(client, *args, **kwargs) except HTTP403Error: logger.debug("Login may have expired...attempting new login") - client.auth_log_in(**get_requests_kwargs(**kwargs)) + client.auth_log_in(**get_requests_kwargs(**kwargs)) # type: ignore return func(client, *args, **kwargs) return wrapper -def handle_hashes(func): +def handle_hashes( + func: Callable[..., APIReturnValueT] +) -> Callable[..., APIReturnValueT]: """ Normalize torrent hash arguments. @@ -109,7 +147,7 @@ def handle_hashes(func): """ @wraps(func) - def wrapper(client, *args, **kwargs): + def wrapper(client, *args, **kwargs): # type: ignore if "torrent_hash" not in kwargs and "hash" in kwargs: kwargs["torrent_hash"] = kwargs.pop("hash") elif "torrent_hashes" not in kwargs and "hashes" in kwargs: @@ -119,7 +157,7 @@ def wrapper(client, *args, **kwargs): return wrapper -def check_for_raise(client, error_message): +def check_for_raise(client: Request, error_message: str) -> None: """For any nonexistent endpoint, log the error and conditionally raise an exception.""" logger.debug(error_message) @@ -127,7 +165,10 @@ def check_for_raise(client, error_message): raise NotImplementedError(error_message) -def endpoint_introduced(version_introduced, endpoint): +def endpoint_introduced( + version_introduced: str, + endpoint: str, +) -> Callable[[Callable[..., APIReturnValueT]], Callable[..., APIReturnValueT]]: """ Prevent hitting an endpoint if the connected qBittorrent version doesn't support it. @@ -135,9 +176,9 @@ def endpoint_introduced(version_introduced, endpoint): :param endpoint: API endpoint (e.g. /torrents/categories) """ - def _inner(func): + def _inner(func): # type: ignore @wraps(func) - def wrapper(client, *args, **kwargs): + def wrapper(client, *args, **kwargs): # type: ignore # if the endpoint doesn't exist, return None or log an error / raise an Exception if v(client.app_web_api_version()) < v(version_introduced): error_message = ( @@ -155,7 +196,10 @@ def wrapper(client, *args, **kwargs): return _inner -def version_removed(version_obsoleted, endpoint): +def version_removed( + version_obsoleted: str, + endpoint: str, +) -> Callable[[Callable[..., APIReturnValueT]], Callable[..., APIReturnValueT]]: """ Prevent hitting an endpoint that was removed in a version older than the connected qBittorrent. @@ -164,9 +208,9 @@ def version_removed(version_obsoleted, endpoint): :param endpoint: name of the removed endpoint """ - def _inner(func): + def _inner(func): # type: ignore @wraps(func) - def wrapper(client, *args, **kwargs): + def wrapper(client, *args, **kwargs): # type: ignore # if the endpoint doesn't exist, return None or log an error / raise an Exception if v(client.app_web_api_version()) >= v(version_obsoleted): error_message = ( diff --git a/src/qbittorrentapi/decorators.pyi b/src/qbittorrentapi/decorators.pyi deleted file mode 100644 index 05029f7c..00000000 --- a/src/qbittorrentapi/decorators.pyi +++ /dev/null @@ -1,37 +0,0 @@ -from logging import Logger -from typing import Callable -from typing import Set -from typing import Text -from typing import Type -from typing import TypeVar - -from qbittorrentapi.request import Request - -logger: Logger - -APIClassT = TypeVar("APIClassT", bound=Request) -APIReturnValueT = TypeVar("APIReturnValueT") - -class alias: - aliases: Set[Text] - def __init__(self, *aliases: Text) -> None: ... - def __call__( - self, func: Callable[..., APIReturnValueT] - ) -> Callable[..., APIReturnValueT]: ... - -def aliased(aliased_class: Type[APIClassT]) -> Type[APIClassT]: ... -def login_required( - func: Callable[..., APIReturnValueT] -) -> Callable[..., APIReturnValueT]: ... -def handle_hashes( - func: Callable[..., APIReturnValueT] -) -> Callable[..., APIReturnValueT]: ... -def endpoint_introduced( - version_introduced: Text, - endpoint: Text, -) -> Callable[[Callable[..., APIReturnValueT]], Callable[..., APIReturnValueT]]: ... -def version_removed( - version_obsoleted: Text, - endpoint: Text, -) -> Callable[[Callable[..., APIReturnValueT]], Callable[..., APIReturnValueT]]: ... -def check_for_raise(client: Request, error_message: Text) -> None: ... diff --git a/src/qbittorrentapi/definitions.py b/src/qbittorrentapi/definitions.py index e6a175a8..f7cc867a 100644 --- a/src/qbittorrentapi/definitions.py +++ b/src/qbittorrentapi/definitions.py @@ -1,9 +1,48 @@ +from __future__ import annotations + from collections import UserList -from collections.abc import Mapping from enum import Enum +from typing import TYPE_CHECKING +from typing import Any +from typing import Generic +from typing import Iterable +from typing import Mapping +from typing import MutableMapping +from typing import Sequence +from typing import Tuple +from typing import TypeVar +from typing import Union from qbittorrentapi._attrdict import AttrDict +if TYPE_CHECKING: + from qbittorrentapi import Request + +K = TypeVar("K") +V = TypeVar("V") +T = TypeVar("T") + +# Type to define JSON +JsonValueT = Union[ + None, + int, + str, + bool, + Sequence["JsonValueT"], + Mapping[str, "JsonValueT"], +] +APIKwargsT = Any +JsonDictionaryT = "Dictionary[str, JsonValueT]" +ClientT = TypeVar("ClientT", bound="Request") +ListEntryT = TypeVar("ListEntryT", bound="ListEntry") +# Type for inputs to build a Dictionary +DictInputT = Mapping[str, JsonValueT] +DictMutableInputT = MutableMapping[str, JsonValueT] +# Type for inputs to build a List +ListInputT = Iterable[Mapping[str, JsonValueT]] +# Type for `files` in requests.get()/post() +FilesToSendT = Mapping[str, Union[bytes, Tuple[str, bytes]]] + class APINames(str, Enum): """ @@ -66,7 +105,7 @@ class TorrentState(str, Enum): UNKNOWN = "unknown" @property - def is_downloading(self): + def is_downloading(self) -> bool: """Returns ``True`` if the State is categorized as Downloading.""" return self in { TorrentState.DOWNLOADING, @@ -80,7 +119,7 @@ def is_downloading(self): } @property - def is_uploading(self): + def is_uploading(self) -> bool: """Returns ``True`` if the State is categorized as Uploading.""" return self in { TorrentState.UPLOADING, @@ -91,7 +130,7 @@ def is_uploading(self): } @property - def is_complete(self): + def is_complete(self) -> bool: """Returns ``True`` if the State is categorized as Complete.""" return self in { TorrentState.UPLOADING, @@ -103,7 +142,7 @@ def is_complete(self): } @property - def is_checking(self): + def is_checking(self) -> bool: """Returns ``True`` if the State is categorized as Checking.""" return self in { TorrentState.CHECKING_UPLOAD, @@ -112,12 +151,12 @@ def is_checking(self): } @property - def is_errored(self): + def is_errored(self) -> bool: """Returns ``True`` if the State is categorized as Errored.""" return self in {TorrentState.MISSING_FILES, TorrentState.ERROR} @property - def is_paused(self): + def is_paused(self) -> bool: """Returns ``True`` if the State is categorized as Paused.""" return self in {TorrentState.PAUSED_UPLOAD, TorrentState.PAUSED_DOWNLOAD} @@ -150,7 +189,7 @@ class TrackerStatus(int, Enum): NOT_WORKING = 4 @property - def display(self): + def display(self) -> str: """Returns a descriptive display value for status.""" return { TrackerStatus.DISABLED: "Disabled", @@ -161,48 +200,53 @@ def display(self): }[self] -class ClientCache: +class ClientCache(Generic[ClientT]): """ Caches the client. Subclass this for any object that needs access to the Client. """ - def __init__(self, *args, **kwargs): - self._client = kwargs.pop("client") + def __init__(self, *args: Any, client: ClientT, **kwargs: Any): + self._client = client super().__init__(*args, **kwargs) -class Dictionary(ClientCache, AttrDict): +class Dictionary(AttrDict[K, V]): """Base definition of dictionary-like objects returned from qBittorrent.""" - def __init__(self, data=None, client=None): - super().__init__(self._normalize(data or {}), client=client) + def __init__(self, data: DictInputT | None = None, **kwargs: Any): + super().__init__(self._normalize(data or {})) # allows updating properties that aren't necessarily a part of the AttrDict self._setattr("_allow_invalid_attributes", True) @classmethod - def _normalize(cls, data): + def _normalize(cls, data: Mapping[K, V] | T) -> AttrDict[K, V] | T: """Iterate through a dict converting any nested dicts to AttrDicts.""" if isinstance(data, Mapping): return AttrDict({key: cls._normalize(value) for key, value in data.items()}) return data -class List(UserList): +# Python 3.8 does not support UserList as a Generic but mypy still complains +class List(UserList, Generic[ListEntryT]): # type: ignore """Base definition for list-like objects returned from qBittorrent.""" - def __init__(self, list_entries=None, entry_class=None, client=None): - is_safe_cast = None not in {client, entry_class} + def __init__( + self, + list_entries: ListInputT | None = None, + entry_class: type[ListEntryT] | None = None, + **kwargs: Any, + ): super().__init__( [ - entry_class(data=entry, client=client) - if is_safe_cast and isinstance(entry, dict) + entry_class(data=entry, **kwargs) + if entry_class is not None and isinstance(entry, Mapping) else entry - for entry in list_entries or () + for entry in list_entries or [] ] ) -class ListEntry(Dictionary): +class ListEntry(Dictionary[str, JsonValueT]): """Base definition for objects within a list returned from qBittorrent.""" diff --git a/src/qbittorrentapi/definitions.pyi b/src/qbittorrentapi/definitions.pyi deleted file mode 100644 index 05f8da2a..00000000 --- a/src/qbittorrentapi/definitions.pyi +++ /dev/null @@ -1,106 +0,0 @@ -from enum import Enum -from typing import Any -from typing import Literal -from typing import Optional -from typing import Text -from typing import Type -from typing import TypeVar -from typing import Union - -import six - -try: - from collections import UserList -except ImportError: - from UserList import UserList # type: ignore - -from qbittorrentapi._attrdict import AttrDict -from qbittorrentapi._types import DictInputT -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import KwargsT -from qbittorrentapi._types import ListInputT -from qbittorrentapi.client import Client -from qbittorrentapi.request import Request - -K = TypeVar("K") -V = TypeVar("V") -ListEntryT = TypeVar("ListEntryT", bound=Union[JsonDictionaryT, six.text_type]) - -class APINames(Enum): - Authorization: Literal["auth"] - Application: Literal["app"] - Log: Literal["log"] - Sync: Literal["sync"] - Transfer: Literal["transfer"] - Torrents: Literal["torrents"] - RSS: Literal["rss"] - Search: Literal["search"] - EMPTY: Literal[""] - -class TorrentState(Enum): - ERROR: Literal["error"] - MISSING_FILES: Literal["missingFiles"] - UPLOADING: Literal["uploading"] - PAUSED_UPLOAD: Literal["pausedUP"] - QUEUED_UPLOAD: Literal["queuedUP"] - STALLED_UPLOAD: Literal["stalledUP"] - CHECKING_UPLOAD: Literal["checkingUP"] - FORCED_UPLOAD: Literal["forcedUP"] - ALLOCATING: Literal["allocating"] - DOWNLOADING: Literal["downloading"] - METADATA_DOWNLOAD: Literal["metaDL"] - FORCED_METADATA_DOWNLOAD: Literal["forcedMetaDL"] - PAUSED_DOWNLOAD: Literal["pausedDL"] - QUEUED_DOWNLOAD: Literal["queuedDL"] - FORCED_DOWNLOAD: Literal["forcedDL"] - STALLED_DOWNLOAD: Literal["stalledDL"] - CHECKING_DOWNLOAD: Literal["checkingDL"] - CHECKING_RESUME_DATA: Literal["checkingResumeData"] - MOVING: Literal["moving"] - UNKNOWN: Literal["unknown"] - @property - def is_downloading(self) -> bool: ... - @property - def is_uploading(self) -> bool: ... - @property - def is_complete(self) -> bool: ... - @property - def is_checking(self) -> bool: ... - @property - def is_errored(self) -> bool: ... - @property - def is_paused(self) -> bool: ... - -TorrentStates = TorrentState - -class TrackerStatus(Enum): - DISABLED: Literal[0] - NOT_CONTACTED: Literal[1] - WORKING: Literal[2] - UPDATING: Literal[3] - NOT_WORKING: Literal[4] - @property - def display(self) -> Text: ... - -class ClientCache: - _client: Client - def __init__(self, *args: Any, client: Request, **kwargs: KwargsT) -> None: ... - -class Dictionary(ClientCache, AttrDict[K, V]): - def __init__( - self, - data: Optional[DictInputT] = None, - client: Optional[Request] = None, - ): ... - @classmethod - def _normalize(cls, data: DictInputT) -> AttrDict[K, V]: ... - -class List(ClientCache, UserList[ListEntryT]): - def __init__( - self, - list_entries: Optional[ListInputT] = None, - entry_class: Optional[Type[ListEntryT]] = None, - client: Optional[Request] = None, - ) -> None: ... - -class ListEntry(JsonDictionaryT): ... diff --git a/src/qbittorrentapi/exceptions.py b/src/qbittorrentapi/exceptions.py index 4faca175..9e84babc 100644 --- a/src/qbittorrentapi/exceptions.py +++ b/src/qbittorrentapi/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from requests.exceptions import HTTPError as RequestsHTTPError from requests.exceptions import RequestException @@ -43,7 +45,7 @@ class HTTPError(RequestsHTTPError, APIConnectionError): statuses. """ - http_status_code = None + http_status_code: int class HTTP4XXError(HTTPError): @@ -57,49 +59,49 @@ class HTTP5XXError(HTTPError): class HTTP400Error(HTTP4XXError): """HTTP 400 Status.""" - http_status_code = 400 + http_status_code: int = 400 class HTTP401Error(HTTP4XXError): """HTTP 401 Status.""" - http_status_code = 401 + http_status_code: int = 401 class HTTP403Error(HTTP4XXError): """HTTP 403 Status.""" - http_status_code = 403 + http_status_code: int = 403 class HTTP404Error(HTTP4XXError): """HTTP 404 Status.""" - http_status_code = 404 + http_status_code: int = 404 class HTTP405Error(HTTP4XXError): """HTTP 405 Status.""" - http_status_code = 405 + http_status_code: int = 405 class HTTP409Error(HTTP4XXError): """HTTP 409 Status.""" - http_status_code = 409 + http_status_code: int = 409 class HTTP415Error(HTTP4XXError): """HTTP 415 Status.""" - http_status_code = 415 + http_status_code: int = 415 class HTTP500Error(HTTP5XXError): """HTTP 500 Status.""" - http_status_code = 500 + http_status_code: int = 500 class MissingRequiredParameters400Error(HTTP400Error): diff --git a/src/qbittorrentapi/exceptions.pyi b/src/qbittorrentapi/exceptions.pyi deleted file mode 100644 index 5bd831f1..00000000 --- a/src/qbittorrentapi/exceptions.pyi +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Optional - -from requests.exceptions import HTTPError as RequestsHTTPError -from requests.exceptions import RequestException - -class APIError(Exception): ... -class UnsupportedQbittorrentVersion(APIError): ... -class FileError(IOError, APIError): ... -class TorrentFileError(FileError): ... -class TorrentFileNotFoundError(TorrentFileError): ... -class TorrentFilePermissionError(TorrentFileError): ... -class APIConnectionError(RequestException, APIError): ... -class LoginFailed(APIConnectionError): ... - -class HTTPError(RequestsHTTPError, APIConnectionError): - http_status_code: Optional[int] = None - -class HTTP4XXError(HTTPError): ... -class HTTP5XXError(HTTPError): ... -class HTTP400Error(HTTP4XXError): ... -class HTTP401Error(HTTP4XXError): ... -class HTTP403Error(HTTP4XXError): ... -class HTTP404Error(HTTP4XXError): ... -class HTTP405Error(HTTP4XXError): ... -class HTTP409Error(HTTP4XXError): ... -class HTTP415Error(HTTP4XXError): ... -class HTTP500Error(HTTP5XXError): ... -class MissingRequiredParameters400Error(HTTP400Error): ... -class InvalidRequest400Error(HTTP400Error): ... -class Unauthorized401Error(HTTP401Error): ... -class Forbidden403Error(HTTP403Error): ... -class NotFound404Error(HTTP404Error): ... -class MethodNotAllowed405Error(HTTP404Error): ... -class Conflict409Error(HTTP409Error): ... -class UnsupportedMediaType415Error(HTTP415Error): ... -class InternalServerError500Error(HTTP500Error): ... diff --git a/src/qbittorrentapi/log.py b/src/qbittorrentapi/log.py index 3ae3dc6d..b04ea26f 100644 --- a/src/qbittorrentapi/log.py +++ b/src/qbittorrentapi/log.py @@ -1,34 +1,38 @@ +from __future__ import annotations + from qbittorrentapi.app import AppAPIMixIn from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import List from qbittorrentapi.definitions import ListEntry - - -class LogPeersList(List): - """Response for :meth:`~LogAPIMixIn.log_peers`""" - - def __init__(self, list_entries, client=None): - super().__init__(list_entries, entry_class=LogPeer, client=client) +from qbittorrentapi.definitions import ListInputT class LogPeer(ListEntry): """Item in :class:`LogPeersList`""" -class LogMainList(List): - """Response to :meth:`~LogAPIMixIn.log_main`""" +class LogPeersList(List[LogPeer]): + """Response for :meth:`~LogAPIMixIn.log_peers`""" - def __init__(self, list_entries, client=None): - super().__init__(list_entries, entry_class=LogEntry, client=client) + def __init__(self, list_entries: ListInputT, client: LogAPIMixIn | None = None): + super().__init__(list_entries, entry_class=LogPeer) class LogEntry(ListEntry): """Item in :class:`LogMainList`""" -class Log(ClientCache): +class LogMainList(List[LogEntry]): + """Response to :meth:`~LogAPIMixIn.log_main`""" + + def __init__(self, list_entries: ListInputT, client: LogAPIMixIn | None = None): + super().__init__(list_entries, entry_class=LogEntry) + + +class Log(ClientCache["LogAPIMixIn"]): """ Allows interaction with ``Log`` API endpoints. @@ -40,28 +44,32 @@ class Log(ClientCache): >>> log_list = client.log.main() >>> peers_list = client.log.peers(hash='...') >>> # can also filter log down with additional attributes - >>> log_info = client.log.main.info(last_known_id='...') - >>> log_warning = client.log.main.warning(last_known_id='...') + >>> log_info = client.log.main.info(last_known_id=1) + >>> log_warning = client.log.main.warning(last_known_id=1) """ - def __init__(self, client): + def __init__(self, client: LogAPIMixIn): super().__init__(client=client) self.main = Log._Main(client=client) - def peers(self, last_known_id=None, **kwargs): + def peers( + self, + last_known_id: int | None = None, + **kwargs: APIKwargsT, + ) -> LogPeersList: """Implements :meth:`~LogAPIMixIn.log_peers`""" return self._client.log_peers(last_known_id=last_known_id, **kwargs) - class _Main(ClientCache): + class _Main(ClientCache["LogAPIMixIn"]): def _api_call( self, - normal=None, - info=None, - warning=None, - critical=None, - last_known_id=None, - **kwargs, - ): + normal: bool | None = None, + info: bool | None = None, + warning: bool | None = None, + critical: bool | None = None, + last_known_id: int | None = None, + **kwargs: APIKwargsT, + ) -> LogMainList: return self._client.log_main( normal=normal, info=info, @@ -73,13 +81,13 @@ def _api_call( def __call__( self, - normal=True, - info=True, - warning=True, - critical=True, - last_known_id=None, - **kwargs, - ): + normal: bool | None = True, + info: bool | None = True, + warning: bool | None = True, + critical: bool | None = True, + last_known_id: int | None = None, + **kwargs: APIKwargsT, + ) -> LogMainList: return self._api_call( normal=normal, info=info, @@ -89,18 +97,29 @@ def __call__( **kwargs, ) - def info(self, last_known_id=None, **kwargs): + def info( + self, last_known_id: int | None = None, **kwargs: APIKwargsT + ) -> LogMainList: return self._api_call(last_known_id=last_known_id, **kwargs) - def normal(self, last_known_id=None, **kwargs): + def normal( + self, last_known_id: int | None = None, **kwargs: APIKwargsT + ) -> LogMainList: return self._api_call(info=False, last_known_id=last_known_id, **kwargs) - def warning(self, last_known_id=None, **kwargs): + def warning( + self, last_known_id: int | None = None, **kwargs: APIKwargsT + ) -> LogMainList: return self._api_call( - info=False, normal=False, last_known_id=last_known_id, **kwargs + info=False, + normal=False, + last_known_id=last_known_id, + **kwargs, ) - def critical(self, last_known_id=None, **kwargs): + def critical( + self, last_known_id: int | None = None, **kwargs: APIKwargsT + ) -> LogMainList: return self._api_call( info=False, normal=False, @@ -122,7 +141,7 @@ class LogAPIMixIn(AppAPIMixIn): """ @property - def log(self): + def log(self) -> Log: """ Allows for transparent interaction with Log endpoints. @@ -136,13 +155,13 @@ def log(self): @login_required def log_main( self, - normal=None, - info=None, - warning=None, - critical=None, - last_known_id=None, - **kwargs, - ): + normal: bool | None = None, + info: bool | None = None, + warning: bool | None = None, + critical: bool | None = None, + last_known_id: bool | None = None, + **kwargs: APIKwargsT, + ) -> LogMainList: """ Retrieve the qBittorrent log entries. Iterate over returned object. @@ -160,7 +179,7 @@ def log_main( "critical": None if critical is None else bool(critical), "last_known_id": last_known_id, } - return self._get( + return self._get_cast( _name=APINames.Log, _method="main", params=params, @@ -169,7 +188,11 @@ def log_main( ) @login_required - def log_peers(self, last_known_id=None, **kwargs): + def log_peers( + self, + last_known_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> LogPeersList: """ Retrieve qBittorrent peer log. @@ -177,7 +200,7 @@ def log_peers(self, last_known_id=None, **kwargs): :return: :class:`LogPeersList` """ params = {"last_known_id": last_known_id} - return self._get( + return self._get_cast( _name=APINames.Log, _method="peers", params=params, diff --git a/src/qbittorrentapi/log.pyi b/src/qbittorrentapi/log.pyi deleted file mode 100644 index c865d207..00000000 --- a/src/qbittorrentapi/log.pyi +++ /dev/null @@ -1,94 +0,0 @@ -from typing import Optional -from typing import Text - -from qbittorrentapi._types import KwargsT -from qbittorrentapi._types import ListInputT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import List -from qbittorrentapi.definitions import ListEntry - -class LogPeer(ListEntry): ... - -class LogPeersList(List[LogPeer]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[LogAPIMixIn] = None, - ) -> None: ... - -class LogEntry(ListEntry): ... - -class LogMainList(List[LogEntry]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[LogAPIMixIn] = None, - ) -> None: ... - -class Log(ClientCache): - main: _Main - def __init__(self, client: LogAPIMixIn) -> None: ... - def peers( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogPeersList: ... - - class _Main(ClientCache): - def _api_call( - self, - normal: Optional[bool] = None, - info: Optional[bool] = None, - warning: Optional[bool] = None, - critical: Optional[bool] = None, - last_known_id: Optional[bool] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def __call__( - self, - normal: Optional[bool] = True, - info: Optional[bool] = True, - warning: Optional[bool] = True, - critical: Optional[bool] = True, - last_known_id: Optional[bool] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def info( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def normal( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def warning( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def critical( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - -class LogAPIMixIn(AppAPIMixIn): - @property - def log(self) -> Log: ... - def log_main( - self, - normal: Optional[bool] = None, - info: Optional[bool] = None, - warning: Optional[bool] = None, - critical: Optional[bool] = None, - last_known_id: Optional[bool] = None, - **kwargs: KwargsT, - ) -> LogMainList: ... - def log_peers( - self, - last_known_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> LogPeersList: ... diff --git a/src/qbittorrentapi/py.typed b/src/qbittorrentapi/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/src/qbittorrentapi/request.py b/src/qbittorrentapi/request.py index e2e15602..c95165b8 100644 --- a/src/qbittorrentapi/request.py +++ b/src/qbittorrentapi/request.py @@ -1,14 +1,23 @@ +from __future__ import annotations + from collections.abc import Iterable -from contextlib import suppress -from copy import deepcopy from json import loads +from logging import Logger from logging import NullHandler from logging import getLogger from os import environ from time import sleep +from typing import TYPE_CHECKING +from typing import Any +from typing import Final +from typing import Mapping +from typing import TypeVar +from typing import cast +from urllib.parse import ParseResult from urllib.parse import urljoin from urllib.parse import urlparse +from requests import Response from requests import Session from requests import exceptions as requests_exceptions from requests.adapters import HTTPAdapter @@ -16,8 +25,10 @@ from urllib3.exceptions import InsecureRequestWarning from urllib3.util.retry import Retry +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import FilesToSendT from qbittorrentapi.definitions import List from qbittorrentapi.exceptions import APIConnectionError from qbittorrentapi.exceptions import APIError @@ -33,17 +44,39 @@ from qbittorrentapi.exceptions import Unauthorized401Error from qbittorrentapi.exceptions import UnsupportedMediaType415Error -logger = getLogger(__name__) +if TYPE_CHECKING: + from qbittorrentapi.app import Application + from qbittorrentapi.auth import Authorization + from qbittorrentapi.log import Log + from qbittorrentapi.rss import RSS + from qbittorrentapi.search import Search + from qbittorrentapi.sync import Sync + from qbittorrentapi.torrents import TorrentCategories + from qbittorrentapi.torrents import Torrents + from qbittorrentapi.torrents import TorrentTags + from qbittorrentapi.transfer import Transfer + +T = TypeVar("T") +ExceptionT = TypeVar("ExceptionT", bound=requests_exceptions.RequestException) +ResponseT = TypeVar("ResponseT") + +logger: Logger = getLogger(__name__) getLogger("qbittorrentapi").addHandler(NullHandler()) class URL: """Management for the qBittorrent Web API URL.""" - def __init__(self, client): + def __init__(self, client: Request): self.client = client - def build_url(self, api_namespace, api_method, headers, requests_kwargs): + def build_url( + self, + api_namespace: APINames | str, + api_method: str, + headers: Mapping[str, str], + requests_kwargs: Mapping[str, Any], + ) -> str: """ Create a fully qualified URL for the API endpoint. @@ -60,7 +93,11 @@ def build_url(self, api_namespace, api_method, headers, requests_kwargs): self.build_url_path(api_namespace, api_method), ) - def build_base_url(self, headers, requests_kwargs=None): + def build_base_url( + self, + headers: Mapping[str, str], + requests_kwargs: Mapping[str, Any], + ) -> str: """ Determine the Base URL for the Web API endpoints. @@ -128,12 +165,12 @@ def build_base_url(self, headers, requests_kwargs=None): def detect_scheme( self, - base_url, - default_scheme, - alt_scheme, - headers, - requests_kwargs, - ): + base_url: ParseResult, + default_scheme: str, + alt_scheme: str, + headers: Mapping[str, str], + requests_kwargs: Mapping[str, Any], + ) -> str: """ Determine if the URL endpoint is using HTTP or HTTPS. @@ -152,7 +189,7 @@ def detect_scheme( r = self.client._session.request( "head", base_url.geturl(), headers=headers, **requests_kwargs ) - scheme_to_use = urlparse(r.url).scheme + scheme_to_use: str = urlparse(r.url).scheme break except requests_exceptions.SSLError: # an SSLError means that qBittorrent is likely listening on HTTPS @@ -168,7 +205,7 @@ def detect_scheme( logger.debug("Using %s scheme", scheme_to_use.upper()) return scheme_to_use - def build_url_path(self, api_namespace, api_method): + def build_url_path(self, api_namespace: APINames | str, api_method: str) -> str: """ Determine the full URL path for the API endpoint. @@ -178,7 +215,7 @@ def build_url_path(self, api_namespace, api_method): (e.g. ``http://localhost:8080/api/v2/torrents/info`` or ``http://example.com/qbt/api/v2/torrents/info``) """ - with suppress(AttributeError): + if isinstance(api_namespace, APINames): api_namespace = api_namespace.value return "/".join( @@ -187,20 +224,83 @@ def build_url_path(self, api_namespace, api_method): ) +class QbittorrentSession(Session): + """ + Wrapper to augment Requests Session. + + Requests doesn't allow Session to default certain configuration globally. This gets + around that by setting defaults for each request. + """ + + def request(self, method: str, url: str, **kwargs: Any) -> Response: # type: ignore + kwargs.setdefault("timeout", 15.1) + kwargs.setdefault("allow_redirects", True) + + # send Content-Length as 0 for empty POSTs...Requests will not send Content-Length + # if data is empty but qBittorrent will complain otherwise + data = kwargs.get("data") or {} + is_data = any(x is not None for x in data.values()) + if method.lower() == "post" and not is_data: + kwargs.setdefault("headers", {}).update({"Content-Length": "0"}) + + return super().request(method, url, **kwargs) + + class Request: """Facilitates HTTP requests to qBittorrent's Web API.""" - def __init__(self, host="", port=None, username=None, password=None, **kwargs): - self.host = host + def __init__( + self, + host: str | None = None, + port: str | int | None = None, + username: str | None = None, + password: str | None = None, + EXTRA_HEADERS: Mapping[str, str] | None = None, + REQUESTS_ARGS: Mapping[str, Any] | None = None, + VERIFY_WEBUI_CERTIFICATE: bool = True, + FORCE_SCHEME_FROM_HOST: bool = False, + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool = False, + RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: bool = False, + VERBOSE_RESPONSE_LOGGING: bool = False, + SIMPLE_RESPONSES: bool = False, + DISABLE_LOGGING_DEBUG_OUTPUT: bool = False, + ) -> None: + self.host = host or "" self.port = port self.username = username or "" self._password = password or "" - self._initialize_context() - self._initialize_lesser(**kwargs) + self._initialize_settings( + EXTRA_HEADERS=EXTRA_HEADERS, + REQUESTS_ARGS=REQUESTS_ARGS, + VERIFY_WEBUI_CERTIFICATE=VERIFY_WEBUI_CERTIFICATE, + FORCE_SCHEME_FROM_HOST=FORCE_SCHEME_FROM_HOST, + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=( + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS + ), + RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS=( + RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS + ), + VERBOSE_RESPONSE_LOGGING=VERBOSE_RESPONSE_LOGGING, + SIMPLE_RESPONSES=SIMPLE_RESPONSES, + DISABLE_LOGGING_DEBUG_OUTPUT=DISABLE_LOGGING_DEBUG_OUTPUT, + ) - # URL management + self._API_BASE_PATH: Final[str] = "api/v2" + self._API_BASE_URL: str | None = None self._url = URL(client=self) + self._http_session: QbittorrentSession | None = None + + self._application: Application | None = None + self._authorization: Authorization | None = None + self._log: Log | None = None + self._rss: RSS | None = None + self._search: Search | None = None + self._sync: Sync | None = None + self._torrents: Torrents | None = None + self._torrent_categories: TorrentCategories | None = None + self._torrent_tags: TorrentTags | None = None + self._transfer: Transfer | None = None # turn off console-printed warnings about SSL certificate issues. # these errors are only shown once the user has explicitly allowed @@ -209,23 +309,21 @@ def __init__(self, host="", port=None, username=None, password=None, **kwargs): if not self._VERIFY_WEBUI_CERTIFICATE: disable_warnings(InsecureRequestWarning) - def _initialize_context(self): + def _initialize_context(self) -> None: """ - Initialize and/or reset communications context with qBittorrent. + Initialize and reset communications context with qBittorrent. This is necessary on startup or when the authorization cookie needs to be replaced...perhaps because it expired, qBittorrent was restarted, significant settings changes, etc. """ logger.debug("Re-initializing context...") - # base path for all API endpoints - self._API_BASE_PATH = "api/v2" # reset URL so the full URL is derived again # (primarily allows for switching scheme for WebUI: HTTP <-> HTTPS) self._API_BASE_URL = None - # reset Requests session so it is rebuilt with new auth cookie and all + # reset comm session so it is rebuilt with new auth cookie and all self._trigger_session_initialization() # reinitialize interaction layers @@ -240,34 +338,29 @@ def _initialize_context(self): self._rss = None self._search = None - def _initialize_lesser( + def _initialize_settings( self, - EXTRA_HEADERS=None, - REQUESTS_ARGS=None, - VERIFY_WEBUI_CERTIFICATE=True, - FORCE_SCHEME_FROM_HOST=False, - RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=False, - RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=False, - RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS=False, - VERBOSE_RESPONSE_LOGGING=False, - PRINT_STACK_FOR_EACH_REQUEST=False, - SIMPLE_RESPONSES=False, - DISABLE_LOGGING_DEBUG_OUTPUT=False, - MOCK_WEB_API_VERSION=None, - ): + EXTRA_HEADERS: Mapping[str, str] | None = None, + REQUESTS_ARGS: Mapping[str, Any] | None = None, + VERIFY_WEBUI_CERTIFICATE: bool = True, + FORCE_SCHEME_FROM_HOST: bool = False, + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool = False, + RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: bool = False, + VERBOSE_RESPONSE_LOGGING: bool = False, + SIMPLE_RESPONSES: bool = False, + DISABLE_LOGGING_DEBUG_OUTPUT: bool = False, + ) -> None: """Initialize lesser used configuration.""" # Configuration parameters - self._EXTRA_HEADERS = EXTRA_HEADERS or {} - self._REQUESTS_ARGS = REQUESTS_ARGS or {} + self._EXTRA_HEADERS = dict(EXTRA_HEADERS) if EXTRA_HEADERS is not None else {} + self._REQUESTS_ARGS = dict(REQUESTS_ARGS) if REQUESTS_ARGS is not None else {} self._VERIFY_WEBUI_CERTIFICATE = bool(VERIFY_WEBUI_CERTIFICATE) self._VERBOSE_RESPONSE_LOGGING = bool(VERBOSE_RESPONSE_LOGGING) - self._PRINT_STACK_FOR_EACH_REQUEST = bool(PRINT_STACK_FOR_EACH_REQUEST) self._SIMPLE_RESPONSES = bool(SIMPLE_RESPONSES) self._FORCE_SCHEME_FROM_HOST = bool(FORCE_SCHEME_FROM_HOST) self._RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS = bool( - RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS - or RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS + RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS ) self._RAISE_UNSUPPORTEDVERSIONERROR = bool( RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS @@ -312,11 +405,10 @@ def _initialize_lesser( if env_verify_cert is not None: self._VERIFY_WEBUI_CERTIFICATE = False - # Mocking variables until better unit testing exists - self._MOCK_WEB_API_VERSION = MOCK_WEB_API_VERSION + self._PRINT_STACK_FOR_EACH_REQUEST = False @classmethod - def _list2string(cls, input_list=None, delimiter="|"): + def _list2string(cls, input_list: T, delimiter: str = "|") -> str | T: """ Convert entries in a list to a concatenated string. @@ -330,17 +422,17 @@ def _list2string(cls, input_list=None, delimiter="|"): def _get( self, - _name=APINames.EMPTY, - _method="", - requests_args=None, - requests_params=None, - headers=None, - params=None, - data=None, - files=None, - response_class=None, - **kwargs, - ): + _name: APINames | str, + _method: str, + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + response_class: type = Response, + **kwargs: APIKwargsT, + ) -> Any: """ Send ``GET`` request. @@ -364,19 +456,58 @@ def _get( **kwargs, ) + def _get_cast( + self, + _name: APINames | str, + _method: str, + response_class: type[ResponseT], + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + **kwargs: APIKwargsT, + ) -> ResponseT: + """ + Send ``GET`` request with casted response. + + :param api_namespace: the namespace for the API endpoint + (e.g. :class:`~qbittorrentapi.definitions.APINames` or ``torrents``) + :param api_method: the name for the API endpoint (e.g. ``add``) + :param kwargs: see :meth:`~Request._request` + :return: Requests :class:`~requests.Response` + """ + return cast( + ResponseT, + self._request_manager( + http_method="get", + api_namespace=_name, + api_method=_method, + requests_args=requests_args, + requests_params=requests_params, + headers=headers, + params=params, + data=data, + files=files, + response_class=response_class, + **kwargs, + ), + ) + def _post( self, - _name=APINames.EMPTY, - _method="", - requests_args=None, - requests_params=None, - headers=None, - params=None, - data=None, - files=None, - response_class=None, - **kwargs, - ): + _name: APINames | str, + _method: str, + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + response_class: type = Response, + **kwargs: APIKwargsT, + ) -> Any: """ Send ``POST`` request. @@ -400,22 +531,61 @@ def _post( **kwargs, ) + def _post_cast( + self, + _name: APINames | str, + _method: str, + response_class: type[ResponseT], + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + **kwargs: APIKwargsT, + ) -> ResponseT: + """ + Send ``POST`` request with casted response. + + :param api_namespace: the namespace for the API endpoint + (e.g. :class:`~qbittorrentapi.definitions.APINames` or ``torrents``) + :param api_method: the name for the API endpoint (e.g. ``add``) + :param kwargs: see :meth:`~Request._request` + :return: Requests :class:`~requests.Response` + """ + return cast( + ResponseT, + self._request_manager( + http_method="post", + api_namespace=_name, + api_method=_method, + requests_args=requests_args, + requests_params=requests_params, + headers=headers, + params=params, + data=data, + files=files, + response_class=response_class, + **kwargs, + ), + ) + def _request_manager( self, - http_method, - api_namespace, - api_method, - _retries=1, - _retry_backoff_factor=0.3, - requests_args=None, - requests_params=None, - headers=None, - params=None, - data=None, - files=None, - response_class=None, - **kwargs, - ): + http_method: str, + api_namespace: APINames | str, + api_method: str, + _retries: int = 1, + _retry_backoff_factor: float = 0.3, + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + response_class: type = Response, + **kwargs: APIKwargsT, + ) -> Any: """ Wrapper to manage request retries and severe exceptions. @@ -423,40 +593,6 @@ def _request_manager( to HTTPS. During the second attempt, the URL is rebuilt using HTTP or HTTPS as appropriate. """ - - def build_error_msg(exc): - """Create error message for exception to be raised to user.""" - error_prologue = "Failed to connect to qBittorrent. " - error_messages = { - requests_exceptions.SSLError: "This is likely due to using an untrusted certificate " - "(likely self-signed) for HTTPS qBittorrent WebUI. To suppress this error (and skip " - "certificate verification consequently exposing the HTTPS connection to man-in-the-middle " - "attacks), set VERIFY_WEBUI_CERTIFICATE=False when instantiating Client or set " - "environment variable PYTHON_QBITTORRENTAPI_DO_NOT_VERIFY_WEBUI_CERTIFICATE " - f"to a non-null value. SSL Error: {repr(exc)}", - requests_exceptions.HTTPError: f"Invalid HTTP Response: {repr(exc)}", - requests_exceptions.TooManyRedirects: f"Too many redirects: {repr(exc)}", - requests_exceptions.ConnectionError: f"Connection Error: {repr(exc)}", - requests_exceptions.Timeout: f"Timeout Error: {repr(exc)}", - requests_exceptions.RequestException: f"Requests Error: {repr(exc)}", - } - err_msg = error_messages.get(type(exc), f"Unknown Error: {repr(exc)}") - err_msg = error_prologue + err_msg - logger.debug(err_msg) - return err_msg - - def retry_backoff(retry_count): - """ - Back off on attempting each subsequent request retry. - - The first retry is always immediate. if the backoff factor is 0.3, then will - sleep for 0s then .3s, then .6s, etc. between retries. - """ - if retry_count > 0: - backoff_time = _retry_backoff_factor * (2 ** ((retry_count + 1) - 1)) - sleep(backoff_time if backoff_time <= 10 else 10) - logger.debug("Retry attempt %d", retry_count + 1) - max_retries = _retries if _retries >= 1 else 1 for retry in range(0, (max_retries + 1)): # pragma: no branch try: @@ -482,27 +618,46 @@ def retry_backoff(retry_count): raise except Exception as exc: if retry >= max_retries: - error_message = build_error_msg(exc=exc) - response = getattr(exc, "response", None) - raise APIConnectionError(error_message, response=response) + err_msg = "Failed to connect to qBittorrent. " + { + requests_exceptions.SSLError: "This is likely due to using an untrusted certificate " + "(likely self-signed) for HTTPS qBittorrent WebUI. To suppress this error (and skip " + "certificate verification consequently exposing the HTTPS connection to man-in-the-middle " + "attacks), set VERIFY_WEBUI_CERTIFICATE=False when instantiating Client or set " + "environment variable PYTHON_QBITTORRENTAPI_DO_NOT_VERIFY_WEBUI_CERTIFICATE " + f"to a non-null value. SSL Error: {repr(exc)}", + requests_exceptions.HTTPError: f"Invalid HTTP Response: {repr(exc)}", # type: ignore + requests_exceptions.TooManyRedirects: f"Too many redirects: {repr(exc)}", + requests_exceptions.ConnectionError: f"Connection Error: {repr(exc)}", + requests_exceptions.Timeout: f"Timeout Error: {repr(exc)}", + requests_exceptions.RequestException: f"Requests Error: {repr(exc)}", + }.get( + type(exc), f"Unknown Error: {repr(exc)}" # type: ignore + ) + logger.debug(err_msg) + response: Response | None = getattr(exc, "response", None) + raise APIConnectionError(err_msg, response=response) + + if retry > 0: + backoff_time = _retry_backoff_factor * (2 ** ((retry + 1) - 1)) + sleep(backoff_time if backoff_time <= 10 else 10) + logger.debug("Retry attempt %d", retry + 1) - retry_backoff(retry_count=retry) self._initialize_context() def _request( self, - http_method, - api_namespace, - api_method, - requests_args=None, - requests_params=None, - headers=None, - params=None, - data=None, - files=None, - response_class=None, - **kwargs, - ): + http_method: str, + api_namespace: APINames | str, + api_method: str, + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + response_class: type = Response, + **kwargs: APIKwargsT, + ) -> Any: """ Meat and potatoes of sending requests to qBittorrent. @@ -541,7 +696,9 @@ def _request( return self._cast(response, response_class, **response_kwargs) @staticmethod - def _get_response_kwargs(kwargs): + def _get_response_kwargs( + kwargs: dict[str, Any] + ) -> tuple[dict[str, Any], dict[str, Any]]: """ Determine the kwargs for managing the response to return. @@ -556,7 +713,11 @@ def _get_response_kwargs(kwargs): } return response_kwargs, kwargs - def _get_requests_kwargs(self, requests_args=None, requests_params=None): + def _get_requests_kwargs( + self, + requests_args: Mapping[str, Any] | None = None, + requests_params: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: """ Determine the requests_kwargs for the call to Requests. The global configuration in ``self._REQUESTS_ARGS`` is updated by any arguments provided for a specific @@ -566,12 +727,16 @@ def _get_requests_kwargs(self, requests_args=None, requests_params=None): :param requests_params: alternative location to expect Requests ``requests_kwargs`` :return: final dictionary of Requests ``requests_kwargs`` """ - requests_kwargs = deepcopy(self._REQUESTS_ARGS) + requests_kwargs = {} + requests_kwargs.update(self._REQUESTS_ARGS) requests_kwargs.update(requests_args or requests_params or {}) return requests_kwargs @staticmethod - def _get_headers(headers=None, more_headers=None): + def _get_headers( + headers: Mapping[str, str] | None = None, + more_headers: Mapping[str, str] | None = None, + ) -> dict[str, str]: """ Determine headers specific to this request. Request headers can be specified explicitly or with the requests kwargs. Headers specified in @@ -581,12 +746,19 @@ def _get_headers(headers=None, more_headers=None): :param more_headers: headers from requests_kwargs arguments :return: final dictionary of headers for this specific request """ - user_headers = more_headers or {} + user_headers: dict[str, str] = {} + user_headers.update(more_headers or {}) user_headers.update(headers or {}) return user_headers @staticmethod - def _get_data(http_method, params=None, data=None, files=None, **kwargs): + def _get_data( + http_method: str, + params: Mapping[str, Any] | None = None, + data: Mapping[str, Any] | None = None, + files: FilesToSendT | None = None, + **kwargs: APIKwargsT, + ) -> tuple[dict[str, Any], dict[str, Any], FilesToSendT]: """ Determine ``data``, ``params``, and ``files`` for the Requests call. @@ -596,9 +768,9 @@ def _get_data(http_method, params=None, data=None, files=None, **kwargs): :param files: dictionary of files to send with request :return: final dictionaries of data to send to qBittorrent """ - params = params or {} - data = data or {} - files = files or {} + params = dict(params) if params is not None else {} + data = dict(data) if data is not None else {} + files = dict(files) if files is not None else {} # any other keyword arguments are sent to qBittorrent as part of the request. # These are user-defined since this Client will put everything in data/params/files @@ -611,7 +783,12 @@ def _get_data(http_method, params=None, data=None, files=None, **kwargs): return params, data, files - def _cast(self, response, response_class, **response_kwargs): + def _cast( + self, + response: Response, + response_class: type, + **response_kwargs: APIKwargsT, + ) -> Any: """ Returns the API response casted to the requested class. @@ -621,22 +798,24 @@ def _cast(self, response, response_class, **response_kwargs): :return: API response as type of ``response_class`` """ try: - if response_class is None: + if response_class is Response: return response - if response_class in (str, int): - return response_class(response.text) + if response_class is str: + return response.text + if response_class is int: + return int(response.text) if response_class is bytes: return response.content if issubclass(response_class, (Dictionary, List)): try: - result = response.json() + json_response = response.json() except AttributeError: # just in case the requests package is old and doesn't contain json() - result = loads(response.text) + json_response = loads(response.text) if self._SIMPLE_RESPONSES or response_kwargs.get("SIMPLE_RESPONSES"): - return result + return json_response else: - return response_class(result, client=self) + return response_class(json_response, client=self) except Exception as exc: logger.debug("Exception during response parsing.", exc_info=True) raise APIError(f"Exception during response parsing. Error: {exc!r}") @@ -645,35 +824,14 @@ def _cast(self, response, response_class, **response_kwargs): raise APIError(f"No handler defined to cast response to {response_class}") @property - def _session(self): + def _session(self) -> QbittorrentSession: """ Create or return existing HTTP session. :return: Requests :class:`~requests.Session` object """ - class QbittorrentSession(Session): - """ - Wrapper to augment Requests Session. - - Requests doesn't allow Session to default certain configuration globally. - This gets around that by setting defaults for each request. - """ - - def request(self, method, url, **kwargs): - kwargs.setdefault("timeout", 15.1) - kwargs.setdefault("allow_redirects", True) - - # send Content-Length as 0 for empty POSTs...Requests will not send Content-Length - # if data is empty but qBittorrent will complain otherwise - data = kwargs.get("data") or {} - is_data = any(x is not None for x in data.values()) - if method.lower() == "post" and not is_data: - kwargs.setdefault("headers", {}).update({"Content-Length": "0"}) - - return super().request(method, url, **kwargs) - - if self._http_session: + if self._http_session is not None: return self._http_session self._http_session = QbittorrentSession() @@ -710,7 +868,7 @@ def request(self, method, url, **kwargs): return self._http_session - def __del__(self): + def __del__(self) -> None: """ Close HTTP Session before destruction. @@ -720,40 +878,52 @@ def __del__(self): """ self._trigger_session_initialization() - def _trigger_session_initialization(self): + def _trigger_session_initialization(self) -> None: """ Effectively resets the HTTP session by removing the reference to it. During the next request, a new session will be created. """ - with suppress(Exception): + if self._http_session is not None: self._http_session.close() self._http_session = None @staticmethod - def _handle_error_responses(data, params, response): + def _handle_error_responses( + data: Mapping[str, Any], + params: Mapping[str, Any], + response: Response, + ) -> None: """Raise proper exception if qBittorrent returns Error HTTP Status.""" if response.status_code < 400: # short circuit for non-error statuses return + request = response.request + if response.status_code == 400: # Returned for malformed requests such as missing or invalid parameters. # If an error_message isn't returned, qBittorrent didn't receive all required parameters. # APIErrorType::BadParams # the name of the HTTP error (i.e. Bad Request) started being returned in v4.3.0 if response.text in ("", "Bad Request"): - raise MissingRequiredParameters400Error() - raise InvalidRequest400Error(response.text) + raise MissingRequiredParameters400Error( + request=request, response=response + ) + raise InvalidRequest400Error( + response.text, request=request, response=response + ) if response.status_code == 401: # Primarily reserved for XSS and host header issues. - raise Unauthorized401Error(response.text) + raise Unauthorized401Error( + response.text, request=request, response=response + ) if response.status_code == 403: # Not logged in or calling an API method that isn't public # APIErrorType::AccessDenied - raise Forbidden403Error(response.text) + raise Forbidden403Error(response.text, request=request, response=response) if response.status_code == 404: # API method doesn't exist or more likely, torrent not found @@ -764,34 +934,46 @@ def _handle_error_responses(data, params, response): error_hash = hash_source.get("hashes", hash_source.get("hash", "")) if error_hash: error_message = "Torrent hash(es): %s" % error_hash - raise NotFound404Error(error_message) + raise NotFound404Error(error_message, request=request, response=response) if response.status_code == 405: # HTTP method not allowed for the API endpoint. # This should only be raised if qBittorrent changes the requirement for an endpoint... - raise MethodNotAllowed405Error(response.text) + raise MethodNotAllowed405Error( + response.text, request=request, response=response + ) if response.status_code == 409: # APIErrorType::Conflict - raise Conflict409Error(response.text) + raise Conflict409Error(response.text, request=request, response=response) if response.status_code == 415: # APIErrorType::BadData - raise UnsupportedMediaType415Error(response.text) + raise UnsupportedMediaType415Error( + response.text, request=request, response=response + ) if response.status_code >= 500: - http_error = InternalServerError500Error(response.text) - http_error.http_status_code = response.status_code - raise http_error + http500_error = InternalServerError500Error( + response.text, request=request, response=response + ) + http500_error.http_status_code = response.status_code + raise http500_error # Unaccounted for API errors - http_error = HTTPError(response.text) + http_error = HTTPError(response.text, request=request, response=response) http_error.http_status_code = response.status_code raise http_error def _verbose_logging( - self, http_method, url, data, params, requests_kwargs, response - ): + self, + http_method: str, + url: str, + data: Mapping[str, Any], + params: Mapping[str, Any], + requests_kwargs: Mapping[str, Any], + response: Response, + ) -> None: """ Log verbose information about request. @@ -808,10 +990,7 @@ def _verbose_logging( resp_logger("Request Headers: %s", response.request.headers) resp_logger("Request HTTP Data: %s", {"data": data, "params": params}) resp_logger("Requests Config: %s", requests_kwargs) - if ( - str(response.request.body) not in ("None", "") - and "auth/login" not in url - ): + if isinstance(response.request.body, str) and "auth/login" not in url: body_len = ( max_text_length_to_log if len(response.request.body) > max_text_length_to_log diff --git a/src/qbittorrentapi/request.pyi b/src/qbittorrentapi/request.pyi deleted file mode 100644 index 52665cca..00000000 --- a/src/qbittorrentapi/request.pyi +++ /dev/null @@ -1,238 +0,0 @@ -from logging import Logger -from typing import Any -from typing import Iterable -from typing import Mapping -from typing import MutableMapping -from typing import Optional -from typing import Text -from typing import Tuple -from typing import Type -from typing import TypeVar -from typing import Union -from urllib.parse import ParseResult - -import six -from requests import Response -from requests import Session - -from qbittorrentapi._types import FilesToSendT -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import KwargsT -from qbittorrentapi.app import Application -from qbittorrentapi.auth import Authorization -from qbittorrentapi.definitions import APINames -from qbittorrentapi.definitions import List -from qbittorrentapi.log import Log -from qbittorrentapi.rss import RSS -from qbittorrentapi.search import Search -from qbittorrentapi.sync import Sync -from qbittorrentapi.torrents import TorrentCategories -from qbittorrentapi.torrents import Torrents -from qbittorrentapi.torrents import TorrentTags -from qbittorrentapi.transfer import Transfer - -logger: Logger - -FinalResponseT = TypeVar( - "FinalResponseT", - bound=Union[ - int, - bytes, - six.text_type, - JsonDictionaryT, - List[JsonDictionaryT], - ], -) - -class URL(object): - client: Request - def __init__(self, client: Request) -> None: ... - def build_url( - self, - api_namespace: APINames | Text, - api_method: Text, - headers: Mapping[Text, Text], - requests_kwargs: Mapping[Text, Any], - ) -> str: ... - def build_base_url( - self, - headers: Mapping[Text, Text], - requests_kwargs: Optional[Mapping[Text, Any]] = None, - ) -> str: ... - def detect_scheme( - self, - base_url: ParseResult, - default_scheme: Text, - alt_scheme: Text, - headers: Mapping[Text, Text], - requests_kwargs: Mapping[Text, Any], - ) -> str: ... - def build_url_path( - self, api_namespace: APINames | Text, api_method: Text - ) -> str: ... - -class Request(object): - host: Text - port: Text | int - username: Text - _password: Text - _url: URL - _http_session: Session | None - _application: Application | None - _authorization: Authorization | None - _transfer: Transfer | None - _torrents: Torrents | None - _torrent_categories: TorrentCategories | None - _torrent_tags: TorrentTags | None - _log: Log | None - _sync: Sync | None - _rss: RSS | None - _search: Search | None - - _API_BASE_URL: Text | None - _API_BASE_PATH: Text | None - - _EXTRA_HEADERS: Mapping[Text, Text] | None - _REQUESTS_ARGS: MutableMapping[Text, Any] | None - _VERIFY_WEBUI_CERTIFICATE: bool | None - _FORCE_SCHEME_FROM_HOST: bool | None - _RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool | None - _RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool | None - _RAISE_UNSUPPORTEDVERSIONERROR: bool | None - _VERBOSE_RESPONSE_LOGGING: bool | None - _PRINT_STACK_FOR_EACH_REQUEST: bool | None - _SIMPLE_RESPONSES: bool | None - _DISABLE_LOGGING_DEBUG_OUTPUT: bool | None - _MOCK_WEB_API_VERSION: Text | None - def __init__( - self, - host: Optional[Text] = "", - port: Optional[Text | int] = None, - username: Optional[Text] = None, - password: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - def _initialize_context(self) -> None: ... - def _initialize_lesser( - self, - EXTRA_HEADERS: Optional[Mapping[Text, Text]] = None, - REQUESTS_ARGS: Optional[Mapping[Text, Any]] = None, - VERIFY_WEBUI_CERTIFICATE: bool = True, - FORCE_SCHEME_FROM_HOST: bool = False, - RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool = False, - RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: bool = False, - RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: bool = False, - VERBOSE_RESPONSE_LOGGING: bool = False, - PRINT_STACK_FOR_EACH_REQUEST: bool = False, - SIMPLE_RESPONSES: bool = False, - DISABLE_LOGGING_DEBUG_OUTPUT: bool = False, - MOCK_WEB_API_VERSION: Optional[Text] = None, - ) -> None: ... - @classmethod - def _list2string( - cls, - input_list: Optional[Iterable[Any]] = None, - delimiter: Text = "|", - ) -> Text: ... - def _trigger_session_initialization(self) -> None: ... - def _get( - self, - _name: APINames | Text = APINames.EMPTY, - _method: Text = "", - requests_args: Optional[Mapping[Text, Any]] = None, - requests_params: Optional[Mapping[Text, Any]] = None, - headers: Optional[Mapping[Text, Text]] = None, - params: Optional[Mapping[Text, Any]] = None, - data: Optional[Mapping[Text, Any]] = None, - files: Optional[FilesToSendT] = None, - response_class: Optional[Type[FinalResponseT]] = None, - **kwargs: KwargsT, - ) -> FinalResponseT: ... - def _post( - self, - _name: APINames | Text = APINames.EMPTY, - _method: Text = "", - requests_args: Optional[Mapping[Text, Any]] = None, - requests_params: Optional[Mapping[Text, Any]] = None, - headers: Optional[Mapping[Text, Text]] = None, - params: Optional[Mapping[Text, Any]] = None, - data: Optional[Mapping[Text, Any]] = None, - files: Optional[FilesToSendT] = None, - response_class: Optional[Type[FinalResponseT]] = None, - **kwargs: KwargsT, - ) -> FinalResponseT: ... - def _request_manager( - self, - http_method: Text, - api_namespace: APINames | Text, - api_method: Text, - _retries: int = 1, - _retry_backoff_factor: float = 0.3, - requests_args: Optional[Mapping[Text, Any]] = None, - requests_params: Optional[Mapping[Text, Any]] = None, - headers: Optional[Mapping[Text, Text]] = None, - params: Optional[Mapping[Text, Any]] = None, - data: Optional[Mapping[Text, Any]] = None, - files: Optional[FilesToSendT] = None, - response_class: Optional[Type[FinalResponseT]] = None, - **kwargs: KwargsT, - ) -> FinalResponseT: ... - def _request( - self, - http_method: Text, - api_namespace: APINames | Text, - api_method: Text, - requests_args: Optional[Mapping[Text, Any]] = None, - requests_params: Optional[Mapping[Text, Any]] = None, - headers: Optional[Mapping[Text, Text]] = None, - params: Optional[Mapping[Text, Any]] = None, - data: Optional[Mapping[Text, Any]] = None, - files: Optional[FilesToSendT] = None, - response_class: Optional[Type[FinalResponseT]] = None, - **kwargs: KwargsT, - ) -> FinalResponseT: ... - @staticmethod - def _get_response_kwargs( - kwargs: MutableMapping[Text, Any] - ) -> Tuple[dict[str, Any], dict[str, Any]]: ... - def _get_requests_kwargs( - self, - requests_args: Optional[Mapping[Text, Any]] = None, - requests_params: Optional[Mapping[Text, Any]] = None, - ) -> dict[Text, Any]: ... - @staticmethod - def _get_headers( - headers: Optional[Mapping[Text, Text]] = None, - more_headers: Optional[Mapping[Text, Text]] = None, - ) -> dict[Text, Text]: ... - @staticmethod - def _get_data( - http_method: Text, - params: Optional[Mapping[Text, Any]] = None, - data: Optional[Mapping[Text, Any]] = None, - files: Optional[FilesToSendT] = None, - **kwargs: KwargsT, - ) -> Tuple[dict[Text, Any], dict[Text, Any], FilesToSendT]: ... - def _cast( - self, - response: Response, - response_class: Type[FinalResponseT], - **response_kwargs: KwargsT, - ) -> FinalResponseT: ... - @property - def _session(self) -> Session: ... - @staticmethod - def _handle_error_responses( - data: Mapping[Text, Any], - params: Mapping[Text, Any], - response: Response, - ) -> None: ... - def _verbose_logging( - self, - http_method: Text, - url: Text, - data: Mapping[Text, Any], - params: Mapping[Text, Any], - requests_kwargs: Mapping[Text, Any], - response: Response, - ) -> None: ... diff --git a/src/qbittorrentapi/rss.py b/src/qbittorrentapi/rss.py index 85b18aa9..74729e85 100644 --- a/src/qbittorrentapi/rss.py +++ b/src/qbittorrentapi/rss.py @@ -1,25 +1,30 @@ +from __future__ import annotations + from json import dumps +from typing import Mapping from qbittorrentapi.app import AppAPIMixIn from qbittorrentapi.decorators import alias from qbittorrentapi.decorators import aliased from qbittorrentapi.decorators import endpoint_introduced from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import JsonValueT -class RSSitemsDictionary(Dictionary): +class RSSitemsDictionary(Dictionary[str, JsonValueT]): """Response for :meth:`~RSSAPIMixIn.rss_items`""" -class RSSRulesDictionary(Dictionary): +class RSSRulesDictionary(Dictionary[str, JsonValueT]): """Response for :meth:`~RSSAPIMixIn.rss_rules`""" @aliased -class RSS(ClientCache): +class RSS(ClientCache["RSSAPIMixIn"]): """ Allows interaction with ``RSS`` API endpoints. @@ -37,32 +42,51 @@ class RSS(ClientCache): >>> items_no_data = client.rss.items.without_data """ - def __init__(self, client): + def __init__(self, client: RSSAPIMixIn): super().__init__(client=client) self.items = RSS._Items(client=client) @alias("addFolder") - def add_folder(self, folder_path=None, **kwargs): + def add_folder( + self, + folder_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_add_folder`""" return self._client.rss_add_folder(folder_path=folder_path, **kwargs) @alias("addFeed") - def add_feed(self, url=None, item_path=None, **kwargs): + def add_feed( + self, + url: str | None = None, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_add_feed`""" return self._client.rss_add_feed(url=url, item_path=item_path, **kwargs) @alias("setFeedURL") - def set_feed_url(self, url=None, item_path=None, **kwargs): + def set_feed_url( + self, + url: str | None = None, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_set_feed_url`""" return self._client.rss_set_feed_url(url=url, item_path=item_path, **kwargs) @alias("removeItem") - def remove_item(self, item_path=None, **kwargs): + def remove_item(self, item_path: str | None = None, **kwargs: APIKwargsT) -> None: """Implements :meth:`~RSSAPIMixIn.rss_remove_item`""" return self._client.rss_remove_item(item_path=item_path, **kwargs) @alias("moveItem") - def move_item(self, orig_item_path=None, new_item_path=None, **kwargs): + def move_item( + self, + orig_item_path: str | None = None, + new_item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_move_item`""" return self._client.rss_move_item( orig_item_path=orig_item_path, @@ -71,12 +95,17 @@ def move_item(self, orig_item_path=None, new_item_path=None, **kwargs): ) @alias("refreshItem") - def refresh_item(self, item_path=None): + def refresh_item(self, item_path: str | None = None) -> None: """Implements :meth:`~RSSAPIMixIn.rss_refresh_item`""" return self._client.rss_refresh_item(item_path=item_path) @alias("markAsRead") - def mark_as_read(self, item_path=None, article_id=None, **kwargs): + def mark_as_read( + self, + item_path: str | None = None, + article_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_mark_as_read`""" return self._client.rss_mark_as_read( item_path=item_path, @@ -85,7 +114,12 @@ def mark_as_read(self, item_path=None, article_id=None, **kwargs): ) @alias("setRule") - def set_rule(self, rule_name=None, rule_def=None, **kwargs): + def set_rule( + self, + rule_name: str | None = None, + rule_def: Mapping[str, JsonValueT] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_set_rule`""" return self._client.rss_set_rule( rule_name=rule_name, @@ -94,7 +128,12 @@ def set_rule(self, rule_name=None, rule_def=None, **kwargs): ) @alias("renameRule") - def rename_rule(self, orig_rule_name=None, new_rule_name=None, **kwargs): + def rename_rule( + self, + orig_rule_name: str | None = None, + new_rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_rename_rule`""" return self._client.rss_rename_rule( orig_rule_name=orig_rule_name, @@ -103,30 +142,42 @@ def rename_rule(self, orig_rule_name=None, new_rule_name=None, **kwargs): ) @alias("removeRule") - def remove_rule(self, rule_name=None, **kwargs): + def remove_rule( + self, + rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~RSSAPIMixIn.rss_remove_rule`""" return self._client.rss_remove_rule(rule_name=rule_name, **kwargs) @property - def rules(self): + def rules(self) -> RSSRulesDictionary: """Implements :meth:`~RSSAPIMixIn.rss_rules`""" return self._client.rss_rules() @alias("matchingArticles") - def matching_articles(self, rule_name=None, **kwargs): + def matching_articles( + self, + rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> RSSitemsDictionary: """Implements :meth:`~RSSAPIMixIn.rss_matching_articles`""" return self._client.rss_matching_articles(rule_name=rule_name, **kwargs) - class _Items(ClientCache): - def __call__(self, include_feed_data=None, **kwargs): + class _Items(ClientCache["RSSAPIMixIn"]): + def __call__( + self, + include_feed_data: bool | None = None, + **kwargs: APIKwargsT, + ) -> RSSitemsDictionary: return self._client.rss_items(include_feed_data=include_feed_data, **kwargs) @property - def without_data(self): + def without_data(self) -> RSSitemsDictionary: return self._client.rss_items(include_feed_data=False) @property - def with_data(self): + def with_data(self) -> RSSitemsDictionary: return self._client.rss_items(include_feed_data=True) @@ -143,7 +194,7 @@ class RSSAPIMixIn(AppAPIMixIn): """ @property - def rss(self): + def rss(self) -> RSS: """ Allows for transparent interaction with RSS endpoints. @@ -156,7 +207,11 @@ def rss(self): @alias("rss_addFolder") @login_required - def rss_add_folder(self, folder_path=None, **kwargs): + def rss_add_folder( + self, + folder_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Add an RSS folder. Any intermediate folders in path must already exist. @@ -170,7 +225,12 @@ def rss_add_folder(self, folder_path=None, **kwargs): @alias("rss_addFeed") @login_required - def rss_add_feed(self, url=None, item_path=None, **kwargs): + def rss_add_feed( + self, + url: str | None = None, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Add new RSS feed. Folders in path must already exist. @@ -186,7 +246,12 @@ def rss_add_feed(self, url=None, item_path=None, **kwargs): @endpoint_introduced("2.9.1", "rss/setFeedURL") @alias("rss_setFeedURL") @login_required - def rss_set_feed_url(self, url=None, item_path=None, **kwargs): + def rss_set_feed_url( + self, + url: str | None = None, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Update the URL for an existing RSS feed. @@ -201,7 +266,11 @@ def rss_set_feed_url(self, url=None, item_path=None, **kwargs): @alias("rss_removeItem") @login_required - def rss_remove_item(self, item_path=None, **kwargs): + def rss_remove_item( + self, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Remove an RSS item (folder, feed, etc). @@ -217,7 +286,12 @@ def rss_remove_item(self, item_path=None, **kwargs): @alias("rss_moveItem") @login_required - def rss_move_item(self, orig_item_path=None, new_item_path=None, **kwargs): + def rss_move_item( + self, + orig_item_path: str | None = None, + new_item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Move/rename an RSS item (folder, feed, etc.). @@ -231,7 +305,11 @@ def rss_move_item(self, orig_item_path=None, new_item_path=None, **kwargs): self._post(_name=APINames.RSS, _method="moveItem", data=data, **kwargs) @login_required - def rss_items(self, include_feed_data=None, **kwargs): + def rss_items( + self, + include_feed_data: bool | None = None, + **kwargs: APIKwargsT, + ) -> RSSitemsDictionary: """ Retrieve RSS items and optionally feed data. @@ -241,7 +319,7 @@ def rss_items(self, include_feed_data=None, **kwargs): params = { "withData": None if include_feed_data is None else bool(include_feed_data) } - return self._get( + return self._get_cast( _name=APINames.RSS, _method="items", params=params, @@ -252,12 +330,16 @@ def rss_items(self, include_feed_data=None, **kwargs): @endpoint_introduced("2.2", "rss/refreshItem") @alias("rss_refreshItem") @login_required - def rss_refresh_item(self, item_path=None, **kwargs): + def rss_refresh_item( + self, + item_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Trigger a refresh for an RSS item. - Note: qBittorrent v4.1.5 thru v4.1.8 all use Web API 2.2. However, this endpoint was - introduced with v4.1.8; so, behavior may be undefined for these versions. + Note: qBittorrent v4.1.5 through v4.1.8 all use Web API 2.2 but this endpoint + was introduced with v4.1.8; so, behavior may be undefined for these versions. :param item_path: path to item to be refreshed (e.g. ``Folder\\Subfolder\\ItemName``) :return: None @@ -268,7 +350,12 @@ def rss_refresh_item(self, item_path=None, **kwargs): @endpoint_introduced("2.5.1", "rss/markAsRead") @alias("rss_markAsRead") @login_required - def rss_mark_as_read(self, item_path=None, article_id=None, **kwargs): + def rss_mark_as_read( + self, + item_path: str | None = None, + article_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Mark RSS article as read. If article ID is not provider, the entire feed is marked as read. @@ -284,7 +371,12 @@ def rss_mark_as_read(self, item_path=None, article_id=None, **kwargs): @alias("rss_setRule") @login_required - def rss_set_rule(self, rule_name=None, rule_def=None, **kwargs): + def rss_set_rule( + self, + rule_name: str | None = None, + rule_def: Mapping[str, JsonValueT] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Create a new RSS auto-downloading rule. @@ -297,7 +389,12 @@ def rss_set_rule(self, rule_name=None, rule_def=None, **kwargs): @alias("rss_renameRule") @login_required - def rss_rename_rule(self, orig_rule_name=None, new_rule_name=None, **kwargs): + def rss_rename_rule( + self, + orig_rule_name: str | None = None, + new_rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Rename an RSS auto-download rule. @@ -312,7 +409,11 @@ def rss_rename_rule(self, orig_rule_name=None, new_rule_name=None, **kwargs): @alias("rss_removeRule") @login_required - def rss_remove_rule(self, rule_name=None, **kwargs): + def rss_remove_rule( + self, + rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Delete a RSS auto-downloading rule. @@ -323,13 +424,13 @@ def rss_remove_rule(self, rule_name=None, **kwargs): self._post(_name=APINames.RSS, _method="removeRule", data=data, **kwargs) @login_required - def rss_rules(self, **kwargs): + def rss_rules(self, **kwargs: APIKwargsT) -> RSSRulesDictionary: """ Retrieve RSS auto-download rule definitions. :return: :class:`RSSRulesDictionary` """ - return self._get( + return self._get_cast( _name=APINames.RSS, _method="rules", response_class=RSSRulesDictionary, @@ -339,7 +440,11 @@ def rss_rules(self, **kwargs): @endpoint_introduced("2.5.1", "rss/matchingArticles") @alias("rss_matchingArticles") @login_required - def rss_matching_articles(self, rule_name=None, **kwargs): + def rss_matching_articles( + self, + rule_name: str | None = None, + **kwargs: APIKwargsT, + ) -> RSSitemsDictionary: """ Fetch all articles matching a rule. @@ -347,7 +452,7 @@ def rss_matching_articles(self, rule_name=None, **kwargs): :return: :class:`RSSitemsDictionary` """ data = {"ruleName": rule_name} - return self._post( + return self._post_cast( _name=APINames.RSS, _method="matchingArticles", data=data, diff --git a/src/qbittorrentapi/rss.pyi b/src/qbittorrentapi/rss.pyi deleted file mode 100644 index b5ef2b2b..00000000 --- a/src/qbittorrentapi/rss.pyi +++ /dev/null @@ -1,179 +0,0 @@ -from typing import Mapping -from typing import Optional -from typing import Text - -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import JsonValueT -from qbittorrentapi._types import KwargsT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache - -class RSSitemsDictionary(JsonDictionaryT): ... -class RSSRulesDictionary(JsonDictionaryT): ... - -class RSS(ClientCache): - items: _Items - def __init__(self, client: RSSAPIMixIn) -> None: ... - def add_folder( - self, - folder_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - addFolder = add_folder - def add_feed( - self, - url: Optional[Text] = None, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - addFeed = add_feed - def set_feed_url( - self, - url: Optional[Text] = None, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - setFeedURL = set_feed_url - def remove_item( - self, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - removeItem = remove_item - def move_item( - self, - orig_item_path: Optional[Text] = None, - new_item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - moveItem = move_item - def refresh_item(self, item_path: Optional[Text] = None) -> None: ... - refreshItem = refresh_item - def mark_as_read( - self, - item_path: Optional[Text] = None, - article_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - markAsRead = mark_as_read - def set_rule( - self, - rule_name: Optional[Text] = None, - rule_def: Optional[Mapping[Text, JsonValueT]] = None, - **kwargs: KwargsT, - ) -> None: ... - setRule = set_rule - def rename_rule( - self, - orig_rule_name: Optional[Text] = None, - new_rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - renameRule = rename_rule - def remove_rule( - self, - rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - removeRule = remove_rule - @property - def rules(self) -> RSSRulesDictionary: ... - def matching_articles( - self, - rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - matchingArticles = matching_articles - - class _Items(ClientCache): - def __call__( - self, - include_feed_data: Optional[bool] = None, - **kwargs: KwargsT, - ) -> RSSitemsDictionary: ... - @property - def without_data(self) -> RSSitemsDictionary: ... - @property - def with_data(self) -> RSSitemsDictionary: ... - -class RSSAPIMixIn(AppAPIMixIn): - @property - def rss(self) -> RSS: ... - def rss_add_folder( - self, - folder_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_addFolder = rss_add_folder - def rss_add_feed( - self, - url: Optional[Text] = None, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_addFeed = rss_add_feed - def rss_set_feed_url( - self, - url: Optional[Text] = None, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_setFeedURL = rss_set_feed_url - def rss_remove_item( - self, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_removeItem = rss_remove_item - def rss_move_item( - self, - orig_item_path: Optional[Text] = None, - new_item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_moveItem = rss_move_item - def rss_items( - self, - include_feed_data: Optional[bool] = None, - **kwargs: KwargsT, - ) -> RSSitemsDictionary: ... - def rss_refresh_item( - self, - item_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_refreshItem = rss_refresh_item - def rss_mark_as_read( - self, - item_path: Optional[Text] = None, - article_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_markAsRead = rss_mark_as_read - def rss_set_rule( - self, - rule_name: Optional[Text] = None, - rule_def: Optional[Mapping[Text, JsonValueT]] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_setRule = rss_set_rule - def rss_rename_rule( - self, - orig_rule_name: Optional[Text] = None, - new_rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_renameRule = rss_rename_rule - def rss_remove_rule( - self, - rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - rss_removeRule = rss_remove_rule - def rss_rules(self, **kwargs: KwargsT) -> RSSRulesDictionary: ... - def rss_matching_articles( - self, - rule_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> RSSitemsDictionary: ... - rss_matchingArticles = rss_matching_articles diff --git a/src/qbittorrentapi/search.py b/src/qbittorrentapi/search.py index 02893b67..d4f653ac 100644 --- a/src/qbittorrentapi/search.py +++ b/src/qbittorrentapi/search.py @@ -1,81 +1,98 @@ +from __future__ import annotations + +from typing import Iterable +from typing import Mapping +from typing import cast + from qbittorrentapi.app import AppAPIMixIn from qbittorrentapi.decorators import alias from qbittorrentapi.decorators import aliased from qbittorrentapi.decorators import endpoint_introduced from qbittorrentapi.decorators import login_required from qbittorrentapi.decorators import version_removed +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import JsonValueT from qbittorrentapi.definitions import List from qbittorrentapi.definitions import ListEntry +from qbittorrentapi.definitions import ListInputT -class SearchJobDictionary(Dictionary): +class SearchJobDictionary(ClientCache["SearchAPIMixIn"], Dictionary[str, JsonValueT]): """Response for :meth:`~SearchAPIMixIn.search_start`""" - def __init__(self, data, client): - self._search_job_id = data.get("id", None) + def __init__(self, data: Mapping[str, JsonValueT], client: SearchAPIMixIn): + self._search_job_id: int | None = cast(int, data.get("id", None)) super().__init__(data=data, client=client) - def stop(self, **kwargs): + def stop(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~SearchAPIMixIn.search_stop`""" return self._client.search_stop(search_id=self._search_job_id, **kwargs) - def status(self, **kwargs): + def status(self, **kwargs: APIKwargsT) -> SearchStatusesList: """Implements :meth:`~SearchAPIMixIn.search_status`""" return self._client.search_status(search_id=self._search_job_id, **kwargs) - def results(self, limit=None, offset=None, **kwargs): + def results( + self, + limit: str | int | None = None, + offset: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SearchResultsDictionary: """Implements :meth:`~SearchAPIMixIn.search_results`""" return self._client.search_results( - limit=limit, offset=offset, search_id=self._search_job_id, **kwargs + limit=limit, + offset=offset, + search_id=self._search_job_id, + **kwargs, ) - def delete(self, **kwargs): + def delete(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~SearchAPIMixIn.search_delete`""" return self._client.search_delete(search_id=self._search_job_id, **kwargs) -class SearchResultsDictionary(Dictionary): +class SearchResultsDictionary(Dictionary[str, JsonValueT]): """Response for :meth:`~SearchAPIMixIn.search_results`""" -class SearchStatusesList(List): +class SearchStatus(ListEntry): + """Item in :class:`SearchStatusesList`""" + + +class SearchStatusesList(List[SearchStatus]): """Response for :meth:`~SearchAPIMixIn.search_status`""" - def __init__(self, list_entries, client=None): + def __init__(self, list_entries: ListInputT, client: SearchAPIMixIn | None = None): super().__init__(list_entries, entry_class=SearchStatus, client=client) -class SearchStatus(ListEntry): - """Item in :class:`SearchStatusesList`""" +class SearchCategory(ListEntry): + """Item in :class:`SearchCategoriesList`""" -class SearchCategoriesList(List): +class SearchCategoriesList(List[SearchCategory]): """Response for :meth:`~SearchAPIMixIn.search_categories`""" - def __init__(self, list_entries, client=None): + def __init__(self, list_entries: ListInputT, client: SearchAPIMixIn | None = None): super().__init__(list_entries, entry_class=SearchCategory, client=client) -class SearchCategory(ListEntry): - """Item in :class:`SearchCategoriesList`""" +class SearchPlugin(ListEntry): + """Item in :class:`SearchPluginsList`""" -class SearchPluginsList(List): +class SearchPluginsList(List[SearchPlugin]): """Response for :meth:`~SearchAPIMixIn.search_plugins`""" - def __init__(self, list_entries, client=None): + def __init__(self, list_entries: ListInputT, client: SearchAPIMixIn | None = None): super().__init__(list_entries, entry_class=SearchPlugin, client=client) -class SearchPlugin(ListEntry): - """Item in :class:`SearchPluginsList`""" - - @aliased -class Search(ClientCache): +class Search(ClientCache["SearchAPIMixIn"]): """ Allows interaction with ``Search`` API endpoints. @@ -96,7 +113,13 @@ class Search(ClientCache): >>> client.search.update_plugins() """ - def start(self, pattern=None, plugins=None, category=None, **kwargs): + def start( + self, + pattern: str | None = None, + plugins: Iterable[str] | None = None, + category: str | None = None, + **kwargs: APIKwargsT, + ) -> SearchJobDictionary: """Implements :meth:`~SearchAPIMixIn.search_start`""" return self._client.search_start( pattern=pattern, @@ -105,15 +128,29 @@ def start(self, pattern=None, plugins=None, category=None, **kwargs): **kwargs, ) - def stop(self, search_id=None, **kwargs): + def stop( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~SearchAPIMixIn.search_stop`""" return self._client.search_stop(search_id=search_id, **kwargs) - def status(self, search_id=None, **kwargs): + def status( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SearchStatusesList: """Implements :meth:`~SearchAPIMixIn.search_status`""" return self._client.search_status(search_id=search_id, **kwargs) - def results(self, search_id=None, limit=None, offset=None, **kwargs): + def results( + self, + search_id: str | int | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SearchResultsDictionary: """Implements :meth:`~SearchAPIMixIn.search_results`""" return self._client.search_results( search_id=search_id, @@ -122,31 +159,52 @@ def results(self, search_id=None, limit=None, offset=None, **kwargs): **kwargs, ) - def delete(self, search_id=None, **kwargs): + def delete( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~SearchAPIMixIn.search_delete`""" return self._client.search_delete(search_id=search_id, **kwargs) - def categories(self, plugin_name=None, **kwargs): + def categories( + self, + plugin_name: str | None = None, + **kwargs: APIKwargsT, + ) -> SearchCategoriesList: """Implements :meth:`~SearchAPIMixIn.search_categories`""" return self._client.search_categories(plugin_name=plugin_name, **kwargs) @property - def plugins(self): + def plugins(self) -> SearchPluginsList: """Implements :meth:`~SearchAPIMixIn.search_plugins`""" return self._client.search_plugins() @alias("installPlugin") - def install_plugin(self, sources=None, **kwargs): + def install_plugin( + self, + sources: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~SearchAPIMixIn.search_install_plugin`""" return self._client.search_install_plugin(sources=sources, **kwargs) @alias("uninstallPlugin") - def uninstall_plugin(self, sources=None, **kwargs): + def uninstall_plugin( + self, + sources: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~SearchAPIMixIn.search_uninstall_plugin`""" return self._client.search_uninstall_plugin(sources=sources, **kwargs) @alias("enablePlugin") - def enable_plugin(self, plugins=None, enable=None, **kwargs): + def enable_plugin( + self, + plugins: Iterable[str] | None = None, + enable: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~SearchAPIMixIn.search_enable_plugin`""" return self._client.search_enable_plugin( plugins=plugins, @@ -155,7 +213,7 @@ def enable_plugin(self, plugins=None, enable=None, **kwargs): ) @alias("updatePlugins") - def update_plugins(self, **kwargs): + def update_plugins(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~SearchAPIMixIn.search_update_plugins`""" return self._client.search_update_plugins(**kwargs) @@ -176,7 +234,7 @@ class SearchAPIMixIn(AppAPIMixIn): """ @property - def search(self): + def search(self) -> Search: """ Allows for transparent interaction with ``Search`` endpoints. @@ -189,7 +247,13 @@ def search(self): @endpoint_introduced("2.1.1", "search/start") @login_required - def search_start(self, pattern=None, plugins=None, category=None, **kwargs): + def search_start( + self, + pattern: str | None = None, + plugins: Iterable[str] | None = None, + category: str | None = None, + **kwargs: APIKwargsT, + ) -> SearchJobDictionary: """ Start a search. Python must be installed. Host may limit number of concurrent searches. @@ -206,7 +270,7 @@ def search_start(self, pattern=None, plugins=None, category=None, **kwargs): "plugins": self._list2string(plugins, "|"), "category": category, } - return self._post( + return self._post_cast( _name=APINames.Search, _method="start", data=data, @@ -216,7 +280,11 @@ def search_start(self, pattern=None, plugins=None, category=None, **kwargs): @endpoint_introduced("2.1.1", "search/stop") @login_required - def search_stop(self, search_id=None, **kwargs): + def search_stop( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Stop a running search. @@ -229,7 +297,11 @@ def search_stop(self, search_id=None, **kwargs): @endpoint_introduced("2.1.1", "search/status") @login_required - def search_status(self, search_id=None, **kwargs): + def search_status( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SearchStatusesList: """ Retrieve status of one or all searches. @@ -239,7 +311,7 @@ def search_status(self, search_id=None, **kwargs): :return: :class:`SearchStatusesList` - ``_ """ # noqa: E501 params = {"id": search_id} - return self._get( + return self._get_cast( _name=APINames.Search, _method="status", params=params, @@ -249,7 +321,13 @@ def search_status(self, search_id=None, **kwargs): @endpoint_introduced("2.1.1", "search/results") @login_required - def search_results(self, search_id=None, limit=None, offset=None, **kwargs): + def search_results( + self, + search_id: str | int | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SearchResultsDictionary: """ Retrieve the results for the search. @@ -262,7 +340,7 @@ def search_results(self, search_id=None, limit=None, offset=None, **kwargs): :return: :class:`SearchResultsDictionary` - ``_ """ # noqa: E501 data = {"id": search_id, "limit": limit, "offset": offset} - return self._post( + return self._post_cast( _name=APINames.Search, _method="results", data=data, @@ -272,7 +350,11 @@ def search_results(self, search_id=None, limit=None, offset=None, **kwargs): @endpoint_introduced("2.1.1", "search/delete") @login_required - def search_delete(self, search_id=None, **kwargs): + def search_delete( + self, + search_id: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Delete a search job. @@ -286,7 +368,11 @@ def search_delete(self, search_id=None, **kwargs): @endpoint_introduced("2.1.1", "search/categories") @version_removed("2.6", "search/categories") @login_required - def search_categories(self, plugin_name=None, **kwargs): + def search_categories( + self, + plugin_name: str | None = None, + **kwargs: APIKwargsT, + ) -> SearchCategoriesList: """ Retrieve categories for search. @@ -296,7 +382,7 @@ def search_categories(self, plugin_name=None, **kwargs): :return: :class:`SearchCategoriesList` """ data = {"pluginName": plugin_name} - return self._post( + return self._post_cast( _name=APINames.Search, _method="categories", data=data, @@ -306,13 +392,13 @@ def search_categories(self, plugin_name=None, **kwargs): @endpoint_introduced("2.1.1", "search/plugins") @login_required - def search_plugins(self, **kwargs): + def search_plugins(self, **kwargs: APIKwargsT) -> SearchPluginsList: """ Retrieve details of search plugins. :return: :class:`SearchPluginsList` - ``_ """ # noqa: E501 - return self._get( + return self._get_cast( _name=APINames.Search, _method="plugins", response_class=SearchPluginsList, @@ -322,7 +408,11 @@ def search_plugins(self, **kwargs): @endpoint_introduced("2.1.1", "search/installPlugin") @alias("search_installPlugin") @login_required - def search_install_plugin(self, sources=None, **kwargs): + def search_install_plugin( + self, + sources: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Install search plugins from either URL or file. @@ -335,7 +425,11 @@ def search_install_plugin(self, sources=None, **kwargs): @endpoint_introduced("2.1.1", "search/uninstallPlugin") @alias("search_uninstallPlugin") @login_required - def search_uninstall_plugin(self, names=None, **kwargs): + def search_uninstall_plugin( + self, + names: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Uninstall search plugins. @@ -353,7 +447,12 @@ def search_uninstall_plugin(self, names=None, **kwargs): @endpoint_introduced("2.1.1", "search/enablePlugin") @alias("search_enablePlugin") @login_required - def search_enable_plugin(self, plugins=None, enable=None, **kwargs): + def search_enable_plugin( + self, + plugins: Iterable[str] | None = None, + enable: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Enable or disable search plugin(s). @@ -370,7 +469,7 @@ def search_enable_plugin(self, plugins=None, enable=None, **kwargs): @endpoint_introduced("2.1.1", "search/updatePlugin") @alias("search_updatePlugins") @login_required - def search_update_plugins(self, **kwargs): + def search_update_plugins(self, **kwargs: APIKwargsT) -> None: """ Auto update search plugins. diff --git a/src/qbittorrentapi/search.pyi b/src/qbittorrentapi/search.pyi deleted file mode 100644 index bf6eb614..00000000 --- a/src/qbittorrentapi/search.pyi +++ /dev/null @@ -1,175 +0,0 @@ -from typing import Iterable -from typing import Optional -from typing import Text - -from qbittorrentapi._types import DictInputT -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import KwargsT -from qbittorrentapi._types import ListInputT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import List -from qbittorrentapi.definitions import ListEntry - -class SearchJobDictionary(JsonDictionaryT): - def __init__( - self, - data: DictInputT, - client: SearchAPIMixIn, - ) -> None: ... - def stop(self, **kwargs: KwargsT) -> None: ... - def status(self, **kwargs: KwargsT) -> SearchStatusesList: ... - def results( - self, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SearchResultsDictionary: ... - def delete(self, **kwargs: KwargsT) -> None: ... - -class SearchResultsDictionary(JsonDictionaryT): ... -class SearchStatus(ListEntry): ... - -class SearchStatusesList(List[SearchStatus]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[SearchAPIMixIn] = None, - ) -> None: ... - -class SearchCategory(ListEntry): ... - -class SearchCategoriesList(List[SearchCategory]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[SearchAPIMixIn] = None, - ) -> None: ... - -class SearchPlugin(ListEntry): ... - -class SearchPluginsList(List[SearchPlugin]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[SearchAPIMixIn] = None, - ) -> None: ... - -class Search(ClientCache): - def start( - self, - pattern: Optional[Text] = None, - plugins: Optional[Iterable[Text]] = None, - category: Optional[Text] = None, - **kwargs: KwargsT, - ) -> SearchJobDictionary: ... - def stop( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def status( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SearchStatusesList: ... - def results( - self, - search_id: Optional[Text | int] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SearchResultsDictionary: ... - def delete( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def categories( - self, - plugin_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> SearchCategoriesList: ... - @property - def plugins(self) -> SearchPluginsList: ... - def install_plugin( - self, - sources: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - installPlugin = install_plugin - def uninstall_plugin( - self, - sources: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - uninstallPlugin = uninstall_plugin - def enable_plugin( - self, - plugins: Optional[Iterable[Text]] = None, - enable: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - enablePlugin = enable_plugin - def update_plugins(self, **kwargs: KwargsT) -> None: ... - updatePlugins = update_plugins - -class SearchAPIMixIn(AppAPIMixIn): - @property - def search(self) -> Search: ... - def search_start( - self, - pattern: Optional[Text] = None, - plugins: Optional[Iterable[Text]] = None, - category: Optional[Text] = None, - **kwargs: KwargsT, - ) -> SearchJobDictionary: ... - def search_stop( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def search_status( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SearchStatusesList: ... - def search_results( - self, - search_id: Optional[Text | int] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SearchResultsDictionary: ... - def search_delete( - self, - search_id: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def search_categories( - self, - plugin_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> SearchCategoriesList: ... - def search_plugins(self, **kwargs: KwargsT) -> SearchPluginsList: ... - def search_install_plugin( - self, - sources: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - search_installPlugin = search_install_plugin - def search_uninstall_plugin( - self, - names: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - search_uninstallPlugin = search_uninstall_plugin - def search_enable_plugin( - self, - plugins: Optional[Iterable[Text]] = None, - enable: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - search_enablePlugin = search_enable_plugin - def search_update_plugins(self, **kwargs: KwargsT) -> None: ... - search_updatePlugins = search_update_plugins diff --git a/src/qbittorrentapi/sync.py b/src/qbittorrentapi/sync.py index c290af70..87afc07e 100644 --- a/src/qbittorrentapi/sync.py +++ b/src/qbittorrentapi/sync.py @@ -1,22 +1,28 @@ +from __future__ import annotations + +from typing import cast + from qbittorrentapi.app import AppAPIMixIn from qbittorrentapi.decorators import alias from qbittorrentapi.decorators import aliased from qbittorrentapi.decorators import handle_hashes from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import JsonValueT -class SyncMainDataDictionary(Dictionary): +class SyncMainDataDictionary(Dictionary[str, JsonValueT]): """Response for :meth:`~SyncAPIMixIn.sync_maindata`""" -class SyncTorrentPeersDictionary(Dictionary): +class SyncTorrentPeersDictionary(Dictionary[str, JsonValueT]): """Response for :meth:`~SyncAPIMixIn.sync_torrent_peers`""" -class Sync(ClientCache): +class Sync(ClientCache["SyncAPIMixIn"]): """ Allows interaction with the ``Sync`` API endpoints. @@ -34,46 +40,59 @@ class Sync(ClientCache): >>> torrent_peers = client.sync.torrent_peers(hash="...'", rid='...') """ - def __init__(self, client): + def __init__(self, client: SyncAPIMixIn) -> None: super().__init__(client=client) self.maindata = self._MainData(client=client) self.torrent_peers = self._TorrentPeers(client=client) self.torrentPeers = self.torrent_peers - class _MainData(ClientCache): - def __init__(self, client): + class _MainData(ClientCache["SyncAPIMixIn"]): + def __init__(self, client: SyncAPIMixIn) -> None: super().__init__(client=client) - self._rid = 0 + self._rid: int = 0 - def __call__(self, rid=None, **kwargs): + def __call__( + self, + rid: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SyncMainDataDictionary: return self._client.sync_maindata(rid=rid, **kwargs) - def delta(self, **kwargs): + def delta(self, **kwargs: APIKwargsT) -> SyncMainDataDictionary: md = self._client.sync_maindata(rid=self._rid, **kwargs) - self._rid = md.get("rid", 0) + self._rid = cast(int, md.get("rid", 0)) return md - def reset_rid(self): + def reset_rid(self) -> None: self._rid = 0 - class _TorrentPeers(ClientCache): - def __init__(self, client): + class _TorrentPeers(ClientCache["SyncAPIMixIn"]): + def __init__(self, client: SyncAPIMixIn) -> None: super().__init__(client=client) - self._rid = None - - def __call__(self, torrent_hash=None, rid=None, **kwargs): + self._rid: int | None = None + + def __call__( + self, + torrent_hash: str | None = None, + rid: str | int | None = None, + **kwargs: APIKwargsT, + ) -> SyncTorrentPeersDictionary: return self._client.sync_torrent_peers( torrent_hash=torrent_hash, rid=rid, **kwargs ) - def delta(self, torrent_hash=None, **kwargs): - tp = self._client.sync_torrent_peers( + def delta( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> SyncTorrentPeersDictionary: + torrent_peers = self._client.sync_torrent_peers( torrent_hash=torrent_hash, rid=self._rid, **kwargs ) - self._rid = tp.get("rid", 0) - return tp + self._rid = cast(int, torrent_peers.get("rid", 0)) + return torrent_peers - def reset_rid(self): + def reset_rid(self) -> None: self._rid = 0 @@ -90,7 +109,7 @@ class SyncAPIMixIn(AppAPIMixIn): """ @property - def sync(self): + def sync(self) -> Sync: """ Allows for transparent interaction with ``Sync`` endpoints. @@ -102,7 +121,11 @@ def sync(self): return self._sync @login_required - def sync_maindata(self, rid=0, **kwargs): + def sync_maindata( + self, + rid: str | int = 0, + **kwargs: APIKwargsT, + ) -> SyncMainDataDictionary: """ Retrieves sync data. @@ -110,7 +133,7 @@ def sync_maindata(self, rid=0, **kwargs): :return: :class:`SyncMainDataDictionary` - ``_ """ # noqa: E501 data = {"rid": rid} - return self._post( + return self._post_cast( _name=APINames.Sync, _method="maindata", data=data, @@ -121,7 +144,12 @@ def sync_maindata(self, rid=0, **kwargs): @alias("sync_torrentPeers") @handle_hashes @login_required - def sync_torrent_peers(self, torrent_hash=None, rid=0, **kwargs): + def sync_torrent_peers( + self, + torrent_hash: str | None = None, + rid: str | int = 0, + **kwargs: APIKwargsT, + ) -> SyncTorrentPeersDictionary: """ Retrieves torrent sync data. @@ -132,7 +160,7 @@ def sync_torrent_peers(self, torrent_hash=None, rid=0, **kwargs): :return: :class:`SyncTorrentPeersDictionary` - ``_ """ # noqa: E501 data = {"hash": torrent_hash, "rid": rid} - return self._post( + return self._post_cast( _name=APINames.Sync, _method="torrentPeers", data=data, diff --git a/src/qbittorrentapi/sync.pyi b/src/qbittorrentapi/sync.pyi deleted file mode 100644 index 388db966..00000000 --- a/src/qbittorrentapi/sync.pyi +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Optional -from typing import Text - -from qbittorrentapi._types import JsonValueT -from qbittorrentapi._types import KwargsT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import Dictionary - -# mypy crashes when this is imported from _types... -JsonDictionaryT = Dictionary[Text, JsonValueT] - -class SyncMainDataDictionary(JsonDictionaryT): ... -class SyncTorrentPeersDictionary(JsonDictionaryT): ... - -class Sync(ClientCache): - maindata: _MainData - torrent_peers: _TorrentPeers - torrentPeers: _TorrentPeers - def __init__(self, client: SyncAPIMixIn) -> None: ... - - class _MainData(ClientCache): - _rid: int | None - def __init__(self, client: SyncAPIMixIn) -> None: ... - def __call__( - self, - rid: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SyncMainDataDictionary: ... - def delta(self, **kwargs: KwargsT) -> SyncMainDataDictionary: ... - def reset_rid(self) -> None: ... - - class _TorrentPeers(ClientCache): - _rid: int | None - def __init__(self, client: SyncAPIMixIn) -> None: ... - def __call__( - self, - torrent_hash: Optional[Text] = None, - rid: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> SyncTorrentPeersDictionary: ... - def delta( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> SyncTorrentPeersDictionary: ... - def reset_rid(self) -> None: ... - -class SyncAPIMixIn(AppAPIMixIn): - @property - def sync(self) -> Sync: ... - def sync_maindata( - self, - rid: Text | int = 0, - **kwargs: KwargsT, - ) -> SyncMainDataDictionary: ... - def sync_torrent_peers( - self, - torrent_hash: Optional[Text] = None, - rid: Text | int = 0, - **kwargs: KwargsT, - ) -> SyncTorrentPeersDictionary: ... - sync_torrentPeers = sync_torrent_peers diff --git a/src/qbittorrentapi/torrents.py b/src/qbittorrentapi/torrents.py index 1e430e8c..c9e75bc3 100644 --- a/src/qbittorrentapi/torrents.py +++ b/src/qbittorrentapi/torrents.py @@ -1,9 +1,19 @@ +from __future__ import annotations + import errno -from collections.abc import Iterable -from collections.abc import Mapping +from logging import Logger from logging import getLogger from os import path from os import strerror as os_strerror +from typing import IO +from typing import Any +from typing import Callable +from typing import Iterable +from typing import Literal +from typing import Mapping +from typing import TypeVar +from typing import Union +from typing import cast from qbittorrentapi._version_support import v from qbittorrentapi.app import AppAPIMixIn @@ -13,21 +23,52 @@ from qbittorrentapi.decorators import endpoint_introduced from qbittorrentapi.decorators import handle_hashes from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import DictMutableInputT +from qbittorrentapi.definitions import JsonValueT from qbittorrentapi.definitions import List from qbittorrentapi.definitions import ListEntry +from qbittorrentapi.definitions import ListInputT from qbittorrentapi.definitions import TorrentState from qbittorrentapi.exceptions import TorrentFileError from qbittorrentapi.exceptions import TorrentFileNotFoundError from qbittorrentapi.exceptions import TorrentFilePermissionError - -logger = getLogger(__name__) +from qbittorrentapi.request import FilesToSendT + +TorrentStatusesT = Literal[ + "all", + "downloading", + "seeding", + "completed", + "paused", + "active", + "inactive", + "resumed", + "stalled", + "stalled_uploading", + "stalled_downloading", + "checking", + "moving", + "errored", +] + +TorrentFilesT = TypeVar( + "TorrentFilesT", + bytes, + str, + IO[bytes], + Mapping[str, Union[bytes, str, IO[bytes]]], + Iterable[Union[bytes, str, IO[bytes]]], +) + +logger: Logger = getLogger(__name__) @aliased -class TorrentDictionary(Dictionary): +class TorrentDictionary(ClientCache["TorrentsAPIMixIn"], ListEntry): """ Item in :class:`TorrentInfoList`. Allows interaction with individual torrents via the ``Torrents`` API endpoints. @@ -55,21 +96,21 @@ class TorrentDictionary(Dictionary): >>> torrent.setCategory(category='video') """ - def __init__(self, data, client): - self._torrent_hash = data.get("hash", None) - # The countdown to the next announce was added in v5.0.0. + def __init__(self, data: DictMutableInputT, client: TorrentsAPIMixIn) -> None: + self._torrent_hash: str | None = cast(str, data.get("hash", None)) + # The attribute for "# of secs til the next announce" was added in v5.0.0. # To avoid clashing with `reannounce()`, rename to `reannounce_in`. if "reannounce" in data: data["reannounce_in"] = data.pop("reannounce") super().__init__(client=client, data=data) - def sync_local(self): + def sync_local(self) -> None: """Update local cache of torrent info.""" for name, value in self.info.items(): setattr(self, name, value) @property - def state_enum(self): + def state_enum(self) -> TorrentState: """Returns the state of a :class:`~qbittorrentapi.definitions.TorrentState`.""" try: return TorrentState(self.state) @@ -77,43 +118,47 @@ def state_enum(self): return TorrentState.UNKNOWN @property - def info(self): + def info(self) -> TorrentDictionary: """Implements :meth:`~TorrentsAPIMixIn.torrents_info`""" info = self._client.torrents_info(torrent_hashes=self._torrent_hash) if len(info) == 1 and info[0].hash == self._torrent_hash: - return info[0] + return info[0] # type: ignore[no-any-return] # qBittorrent v4.1.0 didn't support torrent hash parameter - info = [t for t in self._client.torrents_info() if t.hash == self._torrent_hash] + info = [t for t in self._client.torrents_info() if t.hash == self._torrent_hash] # type: ignore[assignment] if len(info) == 1: - return info[0] + return info[0] # type: ignore[no-any-return] return TorrentDictionary(data={}, client=self._client) - def resume(self, **kwargs): + def resume(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_resume`""" self._client.torrents_resume(torrent_hashes=self._torrent_hash, **kwargs) - def pause(self, **kwargs): + def pause(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_pause`""" self._client.torrents_pause(torrent_hashes=self._torrent_hash, **kwargs) - def delete(self, delete_files=None, **kwargs): + def delete( + self, + delete_files: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_delete`""" self._client.torrents_delete( delete_files=delete_files, torrent_hashes=self._torrent_hash, **kwargs ) - def recheck(self, **kwargs): + def recheck(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_recheck`""" self._client.torrents_recheck(torrent_hashes=self._torrent_hash, **kwargs) - def reannounce(self, **kwargs): + def reannounce(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_reannounce`""" self._client.torrents_reannounce(torrent_hashes=self._torrent_hash, **kwargs) @alias("increasePrio") - def increase_priority(self, **kwargs): + def increase_priority(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_increase_priority`""" self._client.torrents_increase_priority( torrent_hashes=self._torrent_hash, @@ -121,7 +166,7 @@ def increase_priority(self, **kwargs): ) @alias("decreasePrio") - def decrease_priority(self, **kwargs): + def decrease_priority(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_decrease_priority`""" self._client.torrents_decrease_priority( torrent_hashes=self._torrent_hash, @@ -129,12 +174,12 @@ def decrease_priority(self, **kwargs): ) @alias("topPrio") - def top_priority(self, **kwargs): + def top_priority(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_top_priority`""" self._client.torrents_top_priority(torrent_hashes=self._torrent_hash, **kwargs) @alias("bottomPrio") - def bottom_priority(self, **kwargs): + def bottom_priority(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_bottom_priority`""" self._client.torrents_bottom_priority( torrent_hashes=self._torrent_hash, @@ -144,11 +189,11 @@ def bottom_priority(self, **kwargs): @alias("setShareLimits") def set_share_limits( self, - ratio_limit=None, - seeding_time_limit=None, - inactive_seeding_time_limit=None, - **kwargs, - ): + ratio_limit: str | int | None = None, + seeding_time_limit: str | int | None = None, + inactive_seeding_time_limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_share_limits`""" self._client.torrents_set_share_limits( ratio_limit=ratio_limit, @@ -159,26 +204,41 @@ def set_share_limits( ) @property - def download_limit(self): + def download_limit(self) -> int: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_limit`""" - return self._client.torrents_download_limit( - torrent_hashes=self._torrent_hash - ).get(self._torrent_hash) + return cast( + int, + self._client.torrents_download_limit(torrent_hashes=self._torrent_hash).get( + self._torrent_hash or "" + ), + ) @download_limit.setter - def download_limit(self, v): + def download_limit(self, val: str | int) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_limit`""" - self.set_download_limit(limit=v) + self.set_download_limit(limit=val) - downloadLimit = download_limit + @property + def downloadLimit(self) -> int: + """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_limit`""" + return cast( + int, + self._client.torrents_download_limit(torrent_hashes=self._torrent_hash).get( + self._torrent_hash or "" + ), + ) @downloadLimit.setter - def downloadLimit(self, v): + def downloadLimit(self, val: str | int) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_limit`""" - self.download_limit = v + self.set_download_limit(limit=val) @alias("setDownloadLimit") - def set_download_limit(self, limit=None, **kwargs): + def set_download_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_limit`""" self._client.torrents_set_download_limit( limit=limit, @@ -187,26 +247,41 @@ def set_download_limit(self, limit=None, **kwargs): ) @property - def upload_limit(self): + def upload_limit(self) -> int: """Implements :meth:`~TorrentsAPIMixIn.torrents_upload_limit`""" - return self._client.torrents_upload_limit( - torrent_hashes=self._torrent_hash - ).get(self._torrent_hash) + return cast( + int, + self._client.torrents_upload_limit(torrent_hashes=self._torrent_hash).get( + self._torrent_hash or "" + ), + ) @upload_limit.setter - def upload_limit(self, v): + def upload_limit(self, val: str | int) -> None: """Implements :meth:`~TorrentsAPIMixIn.set_upload_limit`""" - self.set_upload_limit(limit=v) + self.set_upload_limit(limit=val) - uploadLimit = upload_limit + @property + def uploadLimit(self) -> int: + """Implements :meth:`~TorrentsAPIMixIn.torrents_upload_limit`""" + return cast( + int, + self._client.torrents_upload_limit(torrent_hashes=self._torrent_hash).get( + self._torrent_hash or "" + ), + ) @uploadLimit.setter - def uploadLimit(self, v): + def uploadLimit(self, val: str | int) -> None: """Implements :meth:`~TorrentsAPIMixIn.set_upload_limit`""" - self.upload_limit = v + self.set_upload_limit(limit=val) @alias("setUploadLimit") - def set_upload_limit(self, limit=None, **kwargs): + def set_upload_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_upload_limit`""" self._client.torrents_set_upload_limit( limit=limit, @@ -215,7 +290,11 @@ def set_upload_limit(self, limit=None, **kwargs): ) @alias("setLocation") - def set_location(self, location=None, **kwargs): + def set_location( + self, + location: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_location`""" self._client.torrents_set_location( location=location, @@ -224,7 +303,11 @@ def set_location(self, location=None, **kwargs): ) @alias("setSavePath") - def set_save_path(self, save_path=None, **kwargs): + def set_save_path( + self, + save_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_save_path`""" self._client.torrents_set_save_path( save_path=save_path, @@ -233,7 +316,11 @@ def set_save_path(self, save_path=None, **kwargs): ) @alias("setDownloadPath") - def set_download_path(self, download_path=None, **kwargs): + def set_download_path( + self, + download_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_download_path`""" self._client.torrents_set_download_path( download_path=download_path, @@ -242,7 +329,11 @@ def set_download_path(self, download_path=None, **kwargs): ) @alias("setCategory") - def set_category(self, category=None, **kwargs): + def set_category( + self, + category: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_category`""" self._client.torrents_set_category( category=category, @@ -251,7 +342,11 @@ def set_category(self, category=None, **kwargs): ) @alias("setAutoManagement") - def set_auto_management(self, enable=None, **kwargs): + def set_auto_management( + self, + enable: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_auto_management`""" self._client.torrents_set_auto_management( enable=enable, @@ -260,14 +355,14 @@ def set_auto_management(self, enable=None, **kwargs): ) @alias("toggleSequentialDownload") - def toggle_sequential_download(self, **kwargs): + def toggle_sequential_download(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_toggle_sequential_download`""" self._client.torrents_toggle_sequential_download( torrent_hashes=self._torrent_hash, **kwargs ) @alias("toggleFirstLastPiecePrio") - def toggle_first_last_piece_priority(self, **kwargs): + def toggle_first_last_piece_priority(self, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_toggle_first_last_piece_priority`""" self._client.torrents_toggle_first_last_piece_priority( @@ -276,7 +371,11 @@ def toggle_first_last_piece_priority(self, **kwargs): ) @alias("setForceStart") - def set_force_start(self, enable=None, **kwargs): + def set_force_start( + self, + enable: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_force_start`""" self._client.torrents_set_force_start( enable=enable, @@ -285,7 +384,11 @@ def set_force_start(self, enable=None, **kwargs): ) @alias("setSuperSeeding") - def set_super_seeding(self, enable=None, **kwargs): + def set_super_seeding( + self, + enable: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_set_super_seeding`""" self._client.torrents_set_super_seeding( enable=enable, @@ -294,39 +397,39 @@ def set_super_seeding(self, enable=None, **kwargs): ) @property - def properties(self): + def properties(self) -> TorrentPropertiesDictionary: """Implements :meth:`~TorrentsAPIMixIn.torrents_properties`""" return self._client.torrents_properties(torrent_hash=self._torrent_hash) @property - def trackers(self): + def trackers(self) -> TrackersList: """Implements :meth:`~TorrentsAPIMixIn.torrents_trackers`""" return self._client.torrents_trackers(torrent_hash=self._torrent_hash) @trackers.setter - def trackers(self, v): + def trackers(self, val: Iterable[str]) -> None: """Implements :meth:`~TorrentsAPIMixIn.add_trackers`""" - self.add_trackers(urls=v) + self.add_trackers(urls=val) @property - def webseeds(self): + def webseeds(self) -> WebSeedsList: """Implements :meth:`~TorrentsAPIMixIn.torrents_webseeds`""" return self._client.torrents_webseeds(torrent_hash=self._torrent_hash) @property - def files(self): + def files(self) -> TorrentFilesList: """Implements :meth:`~TorrentsAPIMixIn.torrents_files`""" return self._client.torrents_files(torrent_hash=self._torrent_hash) @alias("renameFile") def rename_file( self, - file_id=None, - new_file_name=None, - old_path=None, - new_path=None, - **kwargs, - ): + file_id: str | int | None = None, + new_file_name: str | None = None, + old_path: str | None = None, + new_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_rename_file`""" self._client.torrents_rename_file( torrent_hash=self._torrent_hash, @@ -338,7 +441,12 @@ def rename_file( ) @alias("renameFolder") - def rename_folder(self, old_path=None, new_path=None, **kwargs): + def rename_folder( + self, + old_path: str | None = None, + new_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_rename_folder`""" self._client.torrents_rename_folder( torrent_hash=self._torrent_hash, @@ -348,28 +456,37 @@ def rename_folder(self, old_path=None, new_path=None, **kwargs): ) @property - def piece_states(self): + def piece_states(self) -> TorrentPieceInfoList: """Implements :meth:`~TorrentsAPIMixIn.torrents_piece_states`""" return self._client.torrents_piece_states(torrent_hash=self._torrent_hash) pieceStates = piece_states @property - def piece_hashes(self): + def piece_hashes(self) -> TorrentPieceInfoList: """Implements :meth:`~TorrentsAPIMixIn.torrents_piece_hashes`""" return self._client.torrents_piece_hashes(torrent_hash=self._torrent_hash) pieceHashes = piece_hashes @alias("addTrackers") - def add_trackers(self, urls=None, **kwargs): + def add_trackers( + self, + urls: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_add_trackers`""" self._client.torrents_add_trackers( torrent_hash=self._torrent_hash, urls=urls, **kwargs ) @alias("editTracker") - def edit_tracker(self, orig_url=None, new_url=None, **kwargs): + def edit_tracker( + self, + orig_url: str | None = None, + new_url: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_edit_tracker`""" self._client.torrents_edit_tracker( torrent_hash=self._torrent_hash, @@ -379,7 +496,11 @@ def edit_tracker(self, orig_url=None, new_url=None, **kwargs): ) @alias("removeTrackers") - def remove_trackers(self, urls=None, **kwargs): + def remove_trackers( + self, + urls: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_remove_trackers`""" self._client.torrents_remove_trackers( torrent_hash=self._torrent_hash, @@ -388,7 +509,12 @@ def remove_trackers(self, urls=None, **kwargs): ) @alias("filePriority") - def file_priority(self, file_ids=None, priority=None, **kwargs): + def file_priority( + self, + file_ids: int | Iterable[str | int] | None = None, + priority: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_file_priority`""" self._client.torrents_file_priority( torrent_hash=self._torrent_hash, @@ -397,14 +523,18 @@ def file_priority(self, file_ids=None, priority=None, **kwargs): **kwargs, ) - def rename(self, new_name=None, **kwargs): + def rename(self, new_name: str | None = None, **kwargs: APIKwargsT) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_rename`""" self._client.torrents_rename( torrent_hash=self._torrent_hash, new_torrent_name=new_name, **kwargs ), @alias("addTags") - def add_tags(self, tags=None, **kwargs): + def add_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_add_tags`""" self._client.torrents_add_tags( torrent_hashes=self._torrent_hash, @@ -413,7 +543,11 @@ def add_tags(self, tags=None, **kwargs): ) @alias("removeTags") - def remove_tags(self, tags=None, **kwargs): + def remove_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_remove_tags`""" self._client.torrents_remove_tags( torrent_hashes=self._torrent_hash, @@ -421,31 +555,39 @@ def remove_tags(self, tags=None, **kwargs): **kwargs, ) - def export(self, **kwargs): + def export(self, **kwargs: APIKwargsT) -> bytes: """Implements :meth:`~TorrentsAPIMixIn.torrents_export`""" return self._client.torrents_export(torrent_hash=self._torrent_hash, **kwargs) -class TorrentPropertiesDictionary(Dictionary): +class TorrentPropertiesDictionary(Dictionary[str, JsonValueT]): """Response to :meth:`~TorrentsAPIMixIn.torrents_properties`""" -class TorrentLimitsDictionary(Dictionary): +class TorrentLimitsDictionary(Dictionary[str, JsonValueT]): """Response to :meth:`~TorrentsAPIMixIn.torrents_download_limit`""" -class TorrentCategoriesDictionary(Dictionary): +class TorrentCategoriesDictionary(Dictionary[str, JsonValueT]): """Response to :meth:`~TorrentsAPIMixIn.torrents_categories`""" -class TorrentsAddPeersDictionary(Dictionary): +class TorrentsAddPeersDictionary(Dictionary[str, JsonValueT]): """Response to :meth:`~TorrentsAPIMixIn.torrents_add_peers`""" -class TorrentFilesList(List): +class TorrentFile(ListEntry): + """Item in :class:`TorrentFilesList`""" + + +class TorrentFilesList(List[TorrentFile]): """Response to :meth:`~TorrentsAPIMixIn.torrents_files`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=TorrentFile, client=client) # until v4.3.5, the index key wasn't returned...default it to ID for older versions. # when index is returned, maintain backwards compatibility and populate id with index value. @@ -453,63 +595,79 @@ def __init__(self, list_entries, client=None): entry.update({"id": entry.setdefault("index", i)}) -class TorrentFile(ListEntry): - """Item in :class:`TorrentFilesList`""" +class WebSeed(ListEntry): + """Item in :class:`WebSeedsList`""" -class WebSeedsList(List): +class WebSeedsList(List[WebSeed]): """Response to :meth:`~TorrentsAPIMixIn.torrents_webseeds`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=WebSeed, client=client) -class WebSeed(ListEntry): - """Item in :class:`WebSeedsList`""" +class Tracker(ListEntry): + """Item in :class:`TrackersList`""" -class TrackersList(List): +class TrackersList(List[Tracker]): """Response to :meth:`~TorrentsAPIMixIn.torrents_trackers`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=Tracker, client=client) -class Tracker(ListEntry): - """Item in :class:`TrackersList`""" - - -class TorrentInfoList(List): +class TorrentInfoList(List[TorrentDictionary]): """Response to :meth:`~TorrentsAPIMixIn.torrents_info`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=TorrentDictionary, client=client) -class TorrentPieceInfoList(List): +class TorrentPieceData(ListEntry): + """Item in :class:`TorrentPieceInfoList`""" + + +class TorrentPieceInfoList(List[TorrentPieceData]): """Response to :meth:`~TorrentsAPIMixIn.torrents_piece_states` and :meth:`~TorrentsAPIMixIn.torrents_piece_hashes`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=TorrentPieceData, client=client) -class TorrentPieceData(ListEntry): - """Item in :class:`TorrentPieceInfoList`""" +class Tag(ListEntry): + """Item in :class:`TagList`""" -class TagList(List): +class TagList(List[Tag]): """Response to :meth:`~TorrentsAPIMixIn.torrents_tags`""" - def __init__(self, list_entries, client=None): + def __init__( + self, + list_entries: ListInputT, + client: TorrentsAPIMixIn | None = None, + ): super().__init__(list_entries, entry_class=Tag, client=client) -class Tag(ListEntry): - """Item in :class:`TagList`""" - - -class Torrents(ClientCache): +class Torrents(ClientCache["TorrentsAPIMixIn"]): """ Allows interaction with the ``Torrents`` API endpoints. @@ -531,7 +689,7 @@ class Torrents(ClientCache): >>> client.torrents.downloadLimit(torrent_hashes=['...', '...']) """ - def __init__(self, client): + def __init__(self, client: TorrentsAPIMixIn) -> None: super().__init__(client=client) self.info = self._Info(client=client) self.resume = self._ActionForAllTorrents( @@ -628,29 +786,29 @@ def __init__(self, client): def add( self, - urls=None, - torrent_files=None, - save_path=None, - cookie=None, - category=None, - is_skip_checking=None, - is_paused=None, - is_root_folder=None, - rename=None, - upload_limit=None, - download_limit=None, - use_auto_torrent_management=None, - is_sequential_download=None, - is_first_last_piece_priority=None, - tags=None, - content_layout=None, - ratio_limit=None, - seeding_time_limit=None, - download_path=None, - use_download_path=None, - stop_condition=None, - **kwargs, - ): + urls: Iterable[str] | None = None, + torrent_files: TorrentFilesT | None = None, + save_path: str | None = None, + cookie: str | None = None, + category: str | None = None, + is_skip_checking: bool | None = None, + is_paused: bool | None = None, + is_root_folder: bool | None = None, + rename: str | None = None, + upload_limit: str | int | None = None, + download_limit: str | int | None = None, + use_auto_torrent_management: bool | None = None, + is_sequential_download: bool | None = None, + is_first_last_piece_priority: bool | None = None, + tags: Iterable[str] | None = None, + content_layout: None | (Literal["Original", "Subfolder", "NoSubFolder"]) = None, + ratio_limit: str | float | None = None, + seeding_time_limit: str | int | None = None, + download_path: str | None = None, + use_download_path: bool | None = None, + stop_condition: Literal["MetadataReceived", "FilesChecked"] | None = None, + **kwargs: APIKwargsT, + ) -> str: return self._client.torrents_add( urls=urls, torrent_files=torrent_files, @@ -676,30 +834,38 @@ def add( **kwargs, ) - class _ActionForAllTorrents(ClientCache): - def __init__(self, client, func): + class _ActionForAllTorrents(ClientCache["TorrentsAPIMixIn"]): + def __init__( + self, + client: TorrentsAPIMixIn, + func: Callable[..., Any], + ) -> None: super().__init__(client=client) self.func = func - def __call__(self, torrent_hashes=None, **kwargs): + def __call__( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> Any | None: return self.func(torrent_hashes=torrent_hashes, **kwargs) - def all(self, **kwargs): + def all(self, **kwargs: APIKwargsT) -> Any | None: return self.func(torrent_hashes="all", **kwargs) - class _Info(ClientCache): + class _Info(ClientCache["TorrentsAPIMixIn"]): def __call__( self, - status_filter=None, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + status_filter: TorrentStatusesT | None = None, + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter=status_filter, category=category, @@ -714,15 +880,15 @@ def __call__( def all( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="all", category=category, @@ -737,15 +903,15 @@ def all( def downloading( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="downloading", category=category, @@ -760,15 +926,15 @@ def downloading( def seeding( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="seeding", category=category, @@ -783,15 +949,15 @@ def seeding( def completed( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="completed", category=category, @@ -806,15 +972,15 @@ def completed( def paused( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="paused", category=category, @@ -829,15 +995,15 @@ def paused( def active( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="active", category=category, @@ -852,15 +1018,15 @@ def active( def inactive( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="inactive", category=category, @@ -875,15 +1041,15 @@ def inactive( def resumed( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="resumed", category=category, @@ -898,15 +1064,15 @@ def resumed( def stalled( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="stalled", category=category, @@ -921,15 +1087,15 @@ def stalled( def stalled_uploading( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="stalled_uploading", category=category, @@ -944,15 +1110,15 @@ def stalled_uploading( def stalled_downloading( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="stalled_downloading", category=category, @@ -967,15 +1133,15 @@ def stalled_downloading( def checking( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="checking", category=category, @@ -990,15 +1156,15 @@ def checking( def moving( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="moving", category=category, @@ -1013,15 +1179,15 @@ def moving( def errored( self, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: return self._client.torrents_info( status_filter="errored", category=category, @@ -1036,7 +1202,7 @@ def errored( @aliased -class TorrentCategories(ClientCache): +class TorrentCategories(ClientCache["TorrentsAPIMixIn"]): """ Allows interaction with torrent categories within the ``Torrents`` API endpoints. @@ -1057,28 +1223,28 @@ class TorrentCategories(ClientCache): """ @property - def categories(self): + def categories(self) -> TorrentCategoriesDictionary: """Implements :meth:`~TorrentsAPIMixIn.torrents_categories`""" return self._client.torrents_categories() @categories.setter - def categories(self, v): + def categories(self, val: Mapping[str, str | bool]) -> None: """Implements :meth:`~TorrentsAPIMixIn.edit_category` or :meth:`~TorrentsAPIMixIn.create_category`""" - if v.get("name", "") in self.categories: - self.edit_category(**v) + if val.get("name", "") in self.categories: + self.edit_category(**val) else: - self.create_category(**v) + self.create_category(**val) @alias("createCategory") def create_category( self, - name=None, - save_path=None, - download_path=None, - enable_download_path=None, - **kwargs, - ): + name: str | None = None, + save_path: str | None = None, + download_path: str | None = None, + enable_download_path: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_create_category`""" return self._client.torrents_create_category( name=name, @@ -1091,12 +1257,12 @@ def create_category( @alias("editCategory") def edit_category( self, - name=None, - save_path=None, - download_path=None, - enable_download_path=None, - **kwargs, - ): + name: str | None = None, + save_path: str | None = None, + download_path: str | None = None, + enable_download_path: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_edit_category`""" return self._client.torrents_edit_category( name=name, @@ -1107,13 +1273,17 @@ def edit_category( ) @alias("removeCategories") - def remove_categories(self, categories=None, **kwargs): + def remove_categories( + self, + categories: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_remove_categories`""" return self._client.torrents_remove_categories(categories=categories, **kwargs) @aliased -class TorrentTags(ClientCache): +class TorrentTags(ClientCache["TorrentsAPIMixIn"]): """ Allows interaction with torrent tags within the "Torrent" API endpoints. @@ -1127,17 +1297,22 @@ class TorrentTags(ClientCache): """ @property - def tags(self): + def tags(self) -> TagList: """Implements :meth:`~TorrentsAPIMixIn.torrents_tags`""" return self._client.torrents_tags() @tags.setter - def tags(self, v): + def tags(self, val: Iterable[str] | None = None) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_create_tags`""" - self._client.torrents_create_tags(tags=v) + self._client.torrents_create_tags(tags=val) @alias("addTags") - def add_tags(self, tags=None, torrent_hashes=None, **kwargs): + def add_tags( + self, + tags: Iterable[str] | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_add_tags`""" self._client.torrents_add_tags( tags=tags, @@ -1146,7 +1321,12 @@ def add_tags(self, tags=None, torrent_hashes=None, **kwargs): ) @alias("removeTags") - def remove_tags(self, tags=None, torrent_hashes=None, **kwargs): + def remove_tags( + self, + tags: Iterable[str] | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_remove_tags`""" self._client.torrents_remove_tags( tags=tags, @@ -1155,12 +1335,20 @@ def remove_tags(self, tags=None, torrent_hashes=None, **kwargs): ) @alias("createTags") - def create_tags(self, tags=None, **kwargs): + def create_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_create_tags`""" self._client.torrents_create_tags(tags=tags, **kwargs) @alias("deleteTags") - def delete_tags(self, tags=None, **kwargs): + def delete_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TorrentsAPIMixIn.torrents_delete_tags`""" self._client.torrents_delete_tags(tags=tags, **kwargs) @@ -1178,7 +1366,7 @@ class TorrentsAPIMixIn(AppAPIMixIn): """ @property - def torrents(self): + def torrents(self) -> Torrents: """ Allows for transparent interaction with Torrents endpoints. @@ -1190,7 +1378,7 @@ def torrents(self): return self._torrents @property - def torrent_categories(self): + def torrent_categories(self) -> TorrentCategories: """ Allows for transparent interaction with Torrent Categories endpoints. @@ -1202,7 +1390,7 @@ def torrent_categories(self): return self._torrent_categories @property - def torrent_tags(self): + def torrent_tags(self) -> TorrentTags: """ Allows for transparent interaction with Torrent Tags endpoints. @@ -1216,29 +1404,29 @@ def torrent_tags(self): @login_required def torrents_add( self, - urls=None, - torrent_files=None, - save_path=None, - cookie=None, - category=None, - is_skip_checking=None, - is_paused=None, - is_root_folder=None, - rename=None, - upload_limit=None, - download_limit=None, - use_auto_torrent_management=None, - is_sequential_download=None, - is_first_last_piece_priority=None, - tags=None, - content_layout=None, - ratio_limit=None, - seeding_time_limit=None, - download_path=None, - use_download_path=None, - stop_condition=None, - **kwargs, - ): + urls: Iterable[str] | None = None, + torrent_files: TorrentFilesT | None = None, + save_path: str | None = None, + cookie: str | None = None, + category: str | None = None, + is_skip_checking: bool | None = None, + is_paused: bool | None = None, + is_root_folder: bool | None = None, + rename: str | None = None, + upload_limit: str | int | None = None, + download_limit: str | int | None = None, + use_auto_torrent_management: bool | None = None, + is_sequential_download: bool | None = None, + is_first_last_piece_priority: bool | None = None, + tags: Iterable[str] | None = None, + content_layout: None | (Literal["Original", "Subfolder", "NoSubFolder"]) = None, + ratio_limit: str | float | None = None, + seeding_time_limit: str | int | None = None, + download_path: str | None = None, + use_download_path: bool | None = None, + stop_condition: Literal["MetadataReceived", "FilesChecked"] | None = None, + **kwargs: APIKwargsT, + ) -> str: """ Add one or more torrents by URLs and/or torrent files. @@ -1288,7 +1476,7 @@ def torrents_add( and is_root_folder is not None and v(api_version) >= v("2.7") ): - content_layout = "Original" if is_root_folder else "NoSubfolder" + content_layout = "Original" if is_root_folder else "NoSubfolder" # type: ignore is_root_folder = None elif ( content_layout is not None @@ -1325,7 +1513,7 @@ def torrents_add( "stopCondition": (None, stop_condition), } - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="add", data=data, @@ -1335,7 +1523,9 @@ def torrents_add( ) @staticmethod - def _normalize_torrent_files(user_files): + def _normalize_torrent_files( + user_files: TorrentFilesT | None, + ) -> FilesToSendT | None: """ Normalize the torrent file(s) from the user. @@ -1354,7 +1544,7 @@ def _normalize_torrent_files(user_files): is_string_like = isinstance(user_files, (bytes, str)) is_file_like = hasattr(user_files, "read") if is_string_like or is_file_like or not isinstance(user_files, Iterable): - user_files = [user_files] + user_files = [user_files] # type: ignore # up convert to a dictionary to add fabricated torrent names norm_files = ( @@ -1403,7 +1593,11 @@ def _normalize_torrent_files(user_files): ########################################################################## @handle_hashes @login_required - def torrents_properties(self, torrent_hash=None, **kwargs): + def torrents_properties( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentPropertiesDictionary: """ Retrieve individual torrent's properties. @@ -1413,7 +1607,7 @@ def torrents_properties(self, torrent_hash=None, **kwargs): :return: :class:`TorrentPropertiesDictionary` - ``_ """ # noqa: E501 data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="properties", data=data, @@ -1423,7 +1617,11 @@ def torrents_properties(self, torrent_hash=None, **kwargs): @handle_hashes @login_required - def torrents_trackers(self, torrent_hash=None, **kwargs): + def torrents_trackers( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> TrackersList: """ Retrieve individual torrent's trackers. Tracker status is defined in :class:`~qbittorrentapi.definitions.TrackerStatus`. @@ -1434,7 +1632,7 @@ def torrents_trackers(self, torrent_hash=None, **kwargs): :return: :class:`TrackersList` - ``_ """ # noqa: E501 data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="trackers", data=data, @@ -1444,7 +1642,11 @@ def torrents_trackers(self, torrent_hash=None, **kwargs): @handle_hashes @login_required - def torrents_webseeds(self, torrent_hash=None, **kwargs): + def torrents_webseeds( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> WebSeedsList: """ Retrieve individual torrent's web seeds. @@ -1454,7 +1656,7 @@ def torrents_webseeds(self, torrent_hash=None, **kwargs): :return: :class:`WebSeedsList` - ``_ """ # noqa: E501 data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="webseeds", data=data, @@ -1464,7 +1666,11 @@ def torrents_webseeds(self, torrent_hash=None, **kwargs): @handle_hashes @login_required - def torrents_files(self, torrent_hash=None, **kwargs): + def torrents_files( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentFilesList: """ Retrieve individual torrent's files. @@ -1474,7 +1680,7 @@ def torrents_files(self, torrent_hash=None, **kwargs): :return: :class:`TorrentFilesList` - ``_ """ # noqa: E501 data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="files", data=data, @@ -1485,7 +1691,11 @@ def torrents_files(self, torrent_hash=None, **kwargs): @alias("torrents_pieceStates") @handle_hashes @login_required - def torrents_piece_states(self, torrent_hash=None, **kwargs): + def torrents_piece_states( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentPieceInfoList: """ Retrieve individual torrent's pieces' states. @@ -1495,7 +1705,7 @@ def torrents_piece_states(self, torrent_hash=None, **kwargs): :return: :class:`TorrentPieceInfoList` """ data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="pieceStates", data=data, @@ -1506,7 +1716,11 @@ def torrents_piece_states(self, torrent_hash=None, **kwargs): @alias("torrents_pieceHashes") @handle_hashes @login_required - def torrents_piece_hashes(self, torrent_hash=None, **kwargs): + def torrents_piece_hashes( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentPieceInfoList: """ Retrieve individual torrent's pieces' hashes. @@ -1516,7 +1730,7 @@ def torrents_piece_hashes(self, torrent_hash=None, **kwargs): :return: :class:`TorrentPieceInfoList` """ data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="pieceHashes", data=data, @@ -1527,7 +1741,12 @@ def torrents_piece_hashes(self, torrent_hash=None, **kwargs): @alias("torrents_addTrackers") @handle_hashes @login_required - def torrents_add_trackers(self, torrent_hash=None, urls=None, **kwargs): + def torrents_add_trackers( + self, + torrent_hash: str | None = None, + urls: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Add trackers to a torrent. @@ -1547,8 +1766,12 @@ def torrents_add_trackers(self, torrent_hash=None, urls=None, **kwargs): @handle_hashes @login_required def torrents_edit_tracker( - self, torrent_hash=None, original_url=None, new_url=None, **kwargs - ): + self, + torrent_hash: str | None = None, + original_url: str | None = None, + new_url: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Replace a torrent's tracker with a different one. @@ -1571,7 +1794,12 @@ def torrents_edit_tracker( @endpoint_introduced("2.2.0", "torrents/removeTrackers") @handle_hashes @login_required - def torrents_remove_trackers(self, torrent_hash=None, urls=None, **kwargs): + def torrents_remove_trackers( + self, + torrent_hash: str | None = None, + urls: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Remove trackers from a torrent. @@ -1593,8 +1821,12 @@ def torrents_remove_trackers(self, torrent_hash=None, urls=None, **kwargs): @handle_hashes @login_required def torrents_file_priority( - self, torrent_hash=None, file_ids=None, priority=None, **kwargs - ): + self, + torrent_hash: str | None = None, + file_ids: int | Iterable[str | int] | None = None, + priority: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set priority for one or more files. @@ -1615,7 +1847,12 @@ def torrents_file_priority( @handle_hashes @login_required - def torrents_rename(self, torrent_hash=None, new_torrent_name=None, **kwargs): + def torrents_rename( + self, + torrent_hash: str | None = None, + new_torrent_name: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Rename a torrent. @@ -1633,13 +1870,13 @@ def torrents_rename(self, torrent_hash=None, new_torrent_name=None, **kwargs): @login_required def torrents_rename_file( self, - torrent_hash=None, - file_id=None, - new_file_name=None, - old_path=None, - new_path=None, - **kwargs, - ): + torrent_hash: str | None = None, + file_id: str | int | None = None, + new_file_name: str | None = None, + old_path: str | None = None, + new_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Rename a torrent file. @@ -1662,7 +1899,7 @@ def torrents_rename_file( and v(self.app_version()) >= v("v4.3.3") ): try: - old_path = self.torrents_files(torrent_hash=torrent_hash)[file_id].name + old_path = self.torrents_files(torrent_hash=torrent_hash)[file_id].name # type: ignore[index] except (IndexError, AttributeError, TypeError): logger.debug( "ERROR: File ID '%s' isn't valid...'oldPath' cannot be determined.", @@ -1704,11 +1941,11 @@ def torrents_rename_file( @login_required def torrents_rename_folder( self, - torrent_hash=None, - old_path=None, - new_path=None, - **kwargs, - ): + torrent_hash: str | None = None, + old_path: str | None = None, + new_path: str | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Rename a torrent folder. @@ -1746,7 +1983,11 @@ def torrents_rename_folder( @endpoint_introduced("2.8.14", "torrents/export") @handle_hashes @login_required - def torrents_export(self, torrent_hash=None, **kwargs): + def torrents_export( + self, + torrent_hash: str | None = None, + **kwargs: APIKwargsT, + ) -> bytes: """ Export a .torrent file for the torrent. @@ -1756,7 +1997,7 @@ def torrents_export(self, torrent_hash=None, **kwargs): :return: bytes .torrent file """ data = {"hash": torrent_hash} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="export", data=data, @@ -1771,16 +2012,16 @@ def torrents_export(self, torrent_hash=None, **kwargs): @login_required def torrents_info( self, - status_filter=None, - category=None, - sort=None, - reverse=None, - limit=None, - offset=None, - torrent_hashes=None, - tag=None, - **kwargs, - ): + status_filter: TorrentStatusesT | None = None, + category: str | None = None, + sort: str | None = None, + reverse: bool | None = None, + limit: str | int | None = None, + offset: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + tag: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentInfoList: """ Retrieves list of info for torrents. @@ -1812,7 +2053,7 @@ def torrents_info( "hashes": self._list2string(torrent_hashes, "|"), "tag": tag, } - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="info", data=data, @@ -1822,7 +2063,11 @@ def torrents_info( @handle_hashes @login_required - def torrents_resume(self, torrent_hashes=None, **kwargs): + def torrents_resume( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Resume one or more torrents in qBittorrent. @@ -1834,7 +2079,11 @@ def torrents_resume(self, torrent_hashes=None, **kwargs): @handle_hashes @login_required - def torrents_pause(self, torrent_hashes=None, **kwargs): + def torrents_pause( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Pause one or more torrents in qBittorrent. @@ -1846,7 +2095,12 @@ def torrents_pause(self, torrent_hashes=None, **kwargs): @handle_hashes @login_required - def torrents_delete(self, delete_files=False, torrent_hashes=None, **kwargs): + def torrents_delete( + self, + delete_files: bool | None = False, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Remove a torrent from qBittorrent and optionally delete its files. @@ -1862,7 +2116,11 @@ def torrents_delete(self, delete_files=False, torrent_hashes=None, **kwargs): @handle_hashes @login_required - def torrents_recheck(self, torrent_hashes=None, **kwargs): + def torrents_recheck( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Recheck a torrent in qBittorrent. @@ -1875,7 +2133,11 @@ def torrents_recheck(self, torrent_hashes=None, **kwargs): @endpoint_introduced("2.0.2", "torrents/reannounce") @handle_hashes @login_required - def torrents_reannounce(self, torrent_hashes=None, **kwargs): + def torrents_reannounce( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Reannounce a torrent. @@ -1890,7 +2152,11 @@ def torrents_reannounce(self, torrent_hashes=None, **kwargs): @alias("torrents_increasePrio") @handle_hashes @login_required - def torrents_increase_priority(self, torrent_hashes=None, **kwargs): + def torrents_increase_priority( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Increase the priority of a torrent. Torrent Queuing must be enabled. @@ -1905,7 +2171,11 @@ def torrents_increase_priority(self, torrent_hashes=None, **kwargs): @alias("torrents_decreasePrio") @handle_hashes @login_required - def torrents_decrease_priority(self, torrent_hashes=None, **kwargs): + def torrents_decrease_priority( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Decrease the priority of a torrent. Torrent Queuing must be enabled. @@ -1920,7 +2190,11 @@ def torrents_decrease_priority(self, torrent_hashes=None, **kwargs): @alias("torrents_topPrio") @handle_hashes @login_required - def torrents_top_priority(self, torrent_hashes=None, **kwargs): + def torrents_top_priority( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set torrent as highest priority. Torrent Queuing must be enabled. @@ -1935,7 +2209,11 @@ def torrents_top_priority(self, torrent_hashes=None, **kwargs): @alias("torrents_bottomPrio") @handle_hashes @login_required - def torrents_bottom_priority(self, torrent_hashes=None, **kwargs): + def torrents_bottom_priority( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set torrent as lowest priority. Torrent Queuing must be enabled. @@ -1950,14 +2228,18 @@ def torrents_bottom_priority(self, torrent_hashes=None, **kwargs): @alias("torrents_downloadLimit") @handle_hashes @login_required - def torrents_download_limit(self, torrent_hashes=None, **kwargs): + def torrents_download_limit( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> TorrentLimitsDictionary: """ Retrieve the download limit for one or more torrents. :return: :class:`TorrentLimitsDictionary` - ``{hash: limit}`` (-1 represents no limit) """ data = {"hashes": self._list2string(torrent_hashes, "|")} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="downloadLimit", data=data, @@ -1968,7 +2250,12 @@ def torrents_download_limit(self, torrent_hashes=None, **kwargs): @alias("torrents_setDownloadLimit") @handle_hashes @login_required - def torrents_set_download_limit(self, limit=None, torrent_hashes=None, **kwargs): + def torrents_set_download_limit( + self, + limit: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the download limit for one or more torrents. @@ -1993,12 +2280,12 @@ def torrents_set_download_limit(self, limit=None, torrent_hashes=None, **kwargs) @login_required def torrents_set_share_limits( self, - ratio_limit=None, - seeding_time_limit=None, - inactive_seeding_time_limit=None, - torrent_hashes=None, - **kwargs, - ): + ratio_limit: str | int | None = None, + seeding_time_limit: str | int | None = None, + inactive_seeding_time_limit: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set share limits for one or more torrents. @@ -2025,7 +2312,11 @@ def torrents_set_share_limits( @alias("torrents_uploadLimit") @handle_hashes @login_required - def torrents_upload_limit(self, torrent_hashes=None, **kwargs): + def torrents_upload_limit( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> TorrentLimitsDictionary: """ Retrieve the upload limit for one or more torrents. @@ -2033,7 +2324,7 @@ def torrents_upload_limit(self, torrent_hashes=None, **kwargs): :return: :class:`TorrentLimitsDictionary` """ data = {"hashes": self._list2string(torrent_hashes, "|")} - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="uploadLimit", data=data, @@ -2044,7 +2335,12 @@ def torrents_upload_limit(self, torrent_hashes=None, **kwargs): @alias("torrents_setUploadLimit") @handle_hashes @login_required - def torrents_set_upload_limit(self, limit=None, torrent_hashes=None, **kwargs): + def torrents_set_upload_limit( + self, + limit: str | int | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the upload limit for one or more torrents. @@ -2066,7 +2362,12 @@ def torrents_set_upload_limit(self, limit=None, torrent_hashes=None, **kwargs): @alias("torrents_setLocation") @handle_hashes @login_required - def torrents_set_location(self, location=None, torrent_hashes=None, **kwargs): + def torrents_set_location( + self, + location: str | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set location for torrents' files. @@ -2088,12 +2389,17 @@ def torrents_set_location(self, location=None, torrent_hashes=None, **kwargs): @endpoint_introduced("2.8.4", "torrents/setSavePath") @handle_hashes @login_required - def torrents_set_save_path(self, save_path=None, torrent_hashes=None, **kwargs): + def torrents_set_save_path( + self, + save_path: str | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the Save Path for one or more torrents. :raises Forbidden403Error: cannot write to directory - :raises Conflict409Error: directory cannot be created + :raises Conflict409Error: cannot create directory :param save_path: file path to save torrent contents :param torrent_hashes: single torrent hash or list of torrent hashes. Or ``all`` for all torrents. @@ -2109,13 +2415,16 @@ def torrents_set_save_path(self, save_path=None, torrent_hashes=None, **kwargs): @handle_hashes @login_required def torrents_set_download_path( - self, download_path=None, torrent_hashes=None, **kwargs - ): + self, + download_path: str | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the Download Path for one or more torrents. :raises Forbidden403Error: cannot write to directory - :raises Conflict409Error: directory cannot be created + :raises Conflict409Error: cannot create directory :param download_path: file path to save torrent contents before torrent finishes downloading :param torrent_hashes: single torrent hash or list of torrent hashes. Or ``all`` for all torrents. @@ -2134,7 +2443,12 @@ def torrents_set_download_path( @alias("torrents_setCategory") @handle_hashes @login_required - def torrents_set_category(self, category=None, torrent_hashes=None, **kwargs): + def torrents_set_category( + self, + category: str | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set a category for one or more torrents. @@ -2153,7 +2467,12 @@ def torrents_set_category(self, category=None, torrent_hashes=None, **kwargs): @alias("torrents_setAutoManagement") @handle_hashes @login_required - def torrents_set_auto_management(self, enable=None, torrent_hashes=None, **kwargs): + def torrents_set_auto_management( + self, + enable: bool | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Enable or disable automatic torrent management for one or more torrents. @@ -2175,7 +2494,11 @@ def torrents_set_auto_management(self, enable=None, torrent_hashes=None, **kwarg @alias("torrents_toggleSequentialDownload") @handle_hashes @login_required - def torrents_toggle_sequential_download(self, torrent_hashes=None, **kwargs): + def torrents_toggle_sequential_download( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Toggle sequential download for one or more torrents. @@ -2193,7 +2516,11 @@ def torrents_toggle_sequential_download(self, torrent_hashes=None, **kwargs): @alias("torrents_toggleFirstLastPiecePrio") @handle_hashes @login_required - def torrents_toggle_first_last_piece_priority(self, torrent_hashes=None, **kwargs): + def torrents_toggle_first_last_piece_priority( + self, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Toggle priority of first/last piece downloading. @@ -2211,7 +2538,12 @@ def torrents_toggle_first_last_piece_priority(self, torrent_hashes=None, **kwarg @alias("torrents_setForceStart") @handle_hashes @login_required - def torrents_set_force_start(self, enable=None, torrent_hashes=None, **kwargs): + def torrents_set_force_start( + self, + enable: bool | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Force start one or more torrents. @@ -2234,7 +2566,12 @@ def torrents_set_force_start(self, enable=None, torrent_hashes=None, **kwargs): @alias("torrents_setSuperSeeding") @handle_hashes @login_required - def torrents_set_super_seeding(self, enable=None, torrent_hashes=None, **kwargs): + def torrents_set_super_seeding( + self, + enable: bool | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set one or more torrents as super seeding. @@ -2257,7 +2594,12 @@ def torrents_set_super_seeding(self, enable=None, torrent_hashes=None, **kwargs) @endpoint_introduced("2.3.0", "torrents/addPeers") @handle_hashes @login_required - def torrents_add_peers(self, peers=None, torrent_hashes=None, **kwargs): + def torrents_add_peers( + self, + peers: Iterable[str] | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> TorrentsAddPeersDictionary: """ Add one or more peers to one or more torrents. @@ -2271,7 +2613,7 @@ def torrents_add_peers(self, peers=None, torrent_hashes=None, **kwargs): "hashes": self._list2string(torrent_hashes, "|"), "peers": self._list2string(peers, "|"), } - return self._post( + return self._post_cast( _name=APINames.Torrents, _method="addPeers", data=data, @@ -2282,7 +2624,7 @@ def torrents_add_peers(self, peers=None, torrent_hashes=None, **kwargs): # TORRENT CATEGORIES ENDPOINTS @endpoint_introduced("2.1.1", "torrents/categories") @login_required - def torrents_categories(self, **kwargs): + def torrents_categories(self, **kwargs: APIKwargsT) -> TorrentCategoriesDictionary: """ Retrieve all category definitions. @@ -2290,7 +2632,7 @@ def torrents_categories(self, **kwargs): :return: :class:`TorrentCategoriesDictionary` """ - return self._get( + return self._get_cast( _name=APINames.Torrents, _method="categories", response_class=TorrentCategoriesDictionary, @@ -2301,12 +2643,12 @@ def torrents_categories(self, **kwargs): @login_required def torrents_create_category( self, - name=None, - save_path=None, - download_path=None, - enable_download_path=None, - **kwargs, - ): + name: str | None = None, + save_path: str | None = None, + download_path: str | None = None, + enable_download_path: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Create a new torrent category. @@ -2340,12 +2682,12 @@ def torrents_create_category( @login_required def torrents_edit_category( self, - name=None, - save_path=None, - download_path=None, - enable_download_path=None, - **kwargs, - ): + name: str | None = None, + save_path: str | None = None, + download_path: str | None = None, + enable_download_path: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Edit an existing category. @@ -2374,7 +2716,11 @@ def torrents_edit_category( @alias("torrents_removeCategories") @login_required - def torrents_remove_categories(self, categories=None, **kwargs): + def torrents_remove_categories( + self, + categories: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Delete one or more categories. @@ -2392,13 +2738,13 @@ def torrents_remove_categories(self, categories=None, **kwargs): # TORRENT TAGS ENDPOINTS @endpoint_introduced("2.3.0", "torrents/tags") @login_required - def torrents_tags(self, **kwargs): + def torrents_tags(self, **kwargs: APIKwargsT) -> TagList: """ Retrieve all tag definitions. :return: :class:`TagList` """ - return self._get( + return self._get_cast( _name=APINames.Torrents, _method="tags", response_class=TagList, @@ -2409,7 +2755,12 @@ def torrents_tags(self, **kwargs): @endpoint_introduced("2.3.0", "torrents/addTags") @handle_hashes @login_required - def torrents_add_tags(self, tags=None, torrent_hashes=None, **kwargs): + def torrents_add_tags( + self, + tags: Iterable[str] | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Add one or more tags to one or more torrents. @@ -2429,7 +2780,12 @@ def torrents_add_tags(self, tags=None, torrent_hashes=None, **kwargs): @endpoint_introduced("2.3.0", "torrents/removeTags") @handle_hashes @login_required - def torrents_remove_tags(self, tags=None, torrent_hashes=None, **kwargs): + def torrents_remove_tags( + self, + tags: Iterable[str] | None = None, + torrent_hashes: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Add one or more tags to one or more torrents. @@ -2446,7 +2802,11 @@ def torrents_remove_tags(self, tags=None, torrent_hashes=None, **kwargs): @alias("torrents_createTags") @endpoint_introduced("2.3.0", "torrents/createTags") @login_required - def torrents_create_tags(self, tags=None, **kwargs): + def torrents_create_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Create one or more tags. @@ -2459,7 +2819,11 @@ def torrents_create_tags(self, tags=None, **kwargs): @alias("torrents_deleteTags") @endpoint_introduced("2.3.0", "torrents/deleteTags") @login_required - def torrents_delete_tags(self, tags=None, **kwargs): + def torrents_delete_tags( + self, + tags: Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Delete one or more tags. diff --git a/src/qbittorrentapi/torrents.pyi b/src/qbittorrentapi/torrents.pyi deleted file mode 100644 index c596cfea..00000000 --- a/src/qbittorrentapi/torrents.pyi +++ /dev/null @@ -1,949 +0,0 @@ -from logging import Logger -from typing import IO -from typing import Any -from typing import Callable -from typing import Iterable -from typing import Literal -from typing import Mapping -from typing import Optional -from typing import Text -from typing import TypeVar - -from qbittorrentapi._types import DictMutableInputT -from qbittorrentapi._types import FilesToSendT -from qbittorrentapi._types import JsonDictionaryT -from qbittorrentapi._types import KwargsT -from qbittorrentapi._types import ListInputT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import List -from qbittorrentapi.definitions import ListEntry -from qbittorrentapi.definitions import TorrentState - -logger: Logger - -TorrentStatusesT = Literal[ - "all", - "downloading", - "seeding", - "completed", - "paused", - "active", - "inactive", - "resumed", - "stalled", - "stalled_uploading", - "stalled_downloading", - "checking", - "moving", - "errored", -] - -TorrentFilesT = TypeVar( - "TorrentFilesT", - bytes, - Text, - IO[bytes], - Mapping[Text, bytes | Text | IO[bytes]], - Iterable[bytes | Text | IO[bytes]], -) - -class TorrentDictionary(JsonDictionaryT): - def __init__(self, data: DictMutableInputT, client: TorrentsAPIMixIn) -> None: ... - def sync_local(self) -> None: ... - @property - def state_enum(self) -> TorrentState: ... - @property - def info(self) -> TorrentDictionary: ... - def resume(self, **kwargs: KwargsT) -> None: ... - def pause(self, **kwargs: KwargsT) -> None: ... - def delete( - self, - delete_files: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def recheck(self, **kwargs: KwargsT) -> None: ... - def reannounce(self, **kwargs: KwargsT) -> None: ... - def increase_priority(self, **kwargs: KwargsT) -> None: ... - increasePrio = increase_priority - def decrease_priority(self, **kwargs: KwargsT) -> None: ... - decreasePrio = decrease_priority - def top_priority(self, **kwargs: KwargsT) -> None: ... - topPrio = top_priority - def bottom_priority(self, **kwargs: KwargsT) -> None: ... - bottomPrio = bottom_priority - def set_share_limits( - self, - ratio_limit: Optional[Text | int] = None, - seeding_time_limit: Optional[Text | int] = None, - inactive_seeding_time_limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - setShareLimits = set_share_limits - @property - def download_limit(self) -> TorrentLimitsDictionary: ... - @download_limit.setter - def download_limit(self, v: Text | int) -> None: ... - @property - def downloadLimit(self) -> TorrentLimitsDictionary: ... - @downloadLimit.setter - def downloadLimit(self, v: Text | int) -> None: ... - def set_download_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - setDownloadLimit = set_download_limit - @property - def upload_limit(self) -> TorrentLimitsDictionary: ... - @upload_limit.setter - def upload_limit(self, v: Text | int) -> None: ... - @property - def uploadLimit(self) -> TorrentLimitsDictionary: ... - @uploadLimit.setter - def uploadLimit(self, v: Text | int) -> None: ... - def set_upload_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - setUploadLimit = set_upload_limit - def set_location( - self, location: Optional[Text] = None, **kwargs: KwargsT - ) -> None: ... - setLocation = set_location - def set_download_path( - self, - download_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - def set_save_path( - self, - save_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - setSavePath = set_save_path - setDownloadPath = set_download_path - def set_category( - self, - category: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - setCategory = set_category - def set_auto_management( - self, - enable: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - setAutoManagement = set_auto_management - def toggle_sequential_download(self, **kwargs: KwargsT) -> None: ... - toggleSequentialDownload = toggle_sequential_download - def toggle_first_last_piece_priority(self, **kwargs: KwargsT) -> None: ... - toggleFirstLastPiecePrio = toggle_first_last_piece_priority - def set_force_start( - self, - enable: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - setForceStart = set_force_start - def set_super_seeding( - self, - enable: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - setSuperSeeding = set_super_seeding - @property - def properties(self) -> TorrentPropertiesDictionary: ... - @property - def trackers(self) -> TrackersList: ... - @trackers.setter - def trackers(self, v: Iterable[Text]) -> None: ... - @property - def webseeds(self) -> WebSeedsList: ... - @property - def files(self) -> TorrentFilesList: ... - def rename_file( - self, - file_id: Optional[Text | int] = None, - new_file_name: Optional[Text] = None, - old_path: Optional[Text] = None, - new_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - renameFile = rename_file - def rename_folder( - self, - old_path: Optional[Text] = None, - new_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - renameFolder = rename_folder - @property - def piece_states(self) -> TorrentPieceInfoList: ... - @property - def pieceStates(self) -> TorrentPieceInfoList: ... - @property - def piece_hashes(self) -> TorrentPieceInfoList: ... - @property - def pieceHashes(self) -> TorrentPieceInfoList: ... - def add_trackers( - self, - urls: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - addTrackers = add_trackers - def edit_tracker( - self, - orig_url: Optional[Text] = None, - new_url: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - editTracker = edit_tracker - def remove_trackers( - self, - urls: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - removeTrackers = remove_trackers - def file_priority( - self, - file_ids: Optional[int | Iterable[Text | int]] = None, - priority: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - filePriority = file_priority - def rename(self, new_name: Optional[Text] = None, **kwargs: KwargsT) -> None: ... - def add_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - addTags = add_tags - def remove_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - removeTags = remove_tags - def export(self, **kwargs: KwargsT) -> bytes: ... - -class TorrentPropertiesDictionary(JsonDictionaryT): ... -class TorrentLimitsDictionary(JsonDictionaryT): ... -class TorrentCategoriesDictionary(JsonDictionaryT): ... -class TorrentsAddPeersDictionary(JsonDictionaryT): ... -class TorrentFile(ListEntry): ... - -class TorrentFilesList(List[TorrentFile]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class WebSeed(ListEntry): ... - -class WebSeedsList(List[WebSeed]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class Tracker(ListEntry): ... - -class TrackersList(List[Tracker]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class TorrentInfoList(List[TorrentDictionary]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class TorrentPieceData(ListEntry): ... - -class TorrentPieceInfoList(List[TorrentPieceData]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class Tag(ListEntry): ... - -class TagList(List[Tag]): - def __init__( - self, - list_entries: ListInputT, - client: Optional[TorrentsAPIMixIn] = None, - ) -> None: ... - -class Torrents(ClientCache): - info: _Info - resume: _ActionForAllTorrents - pause: _ActionForAllTorrents - delete: _ActionForAllTorrents - recheck: _ActionForAllTorrents - reannounce: _ActionForAllTorrents - increase_priority: _ActionForAllTorrents - increasePrio: _ActionForAllTorrents - decrease_priority: _ActionForAllTorrents - decreasePrio: _ActionForAllTorrents - top_priority: _ActionForAllTorrents - topPrio: _ActionForAllTorrents - bottom_priority: _ActionForAllTorrents - bottomPrio: _ActionForAllTorrents - download_limit: _ActionForAllTorrents - downloadLimit: _ActionForAllTorrents - upload_limit: _ActionForAllTorrents - uploadLimit: _ActionForAllTorrents - set_download_limit: _ActionForAllTorrents - setDownloadLimit: _ActionForAllTorrents - set_share_limits: _ActionForAllTorrents - setShareLimits: _ActionForAllTorrents - set_upload_limit: _ActionForAllTorrents - setUploadLimit: _ActionForAllTorrents - set_location: _ActionForAllTorrents - setLocation: _ActionForAllTorrents - set_save_path: _ActionForAllTorrents - setSavePath: _ActionForAllTorrents - set_download_path: _ActionForAllTorrents - setDownloadPath: _ActionForAllTorrents - set_category: _ActionForAllTorrents - setCategory: _ActionForAllTorrents - set_auto_management: _ActionForAllTorrents - setAutoManagement: _ActionForAllTorrents - toggle_sequential_download: _ActionForAllTorrents - toggleSequentialDownload: _ActionForAllTorrents - toggle_first_last_piece_priority: _ActionForAllTorrents - toggleFirstLastPiecePrio: _ActionForAllTorrents - set_force_start: _ActionForAllTorrents - setForceStart: _ActionForAllTorrents - set_super_seeding: _ActionForAllTorrents - setSuperSeeding: _ActionForAllTorrents - add_peers: _ActionForAllTorrents - addPeers: _ActionForAllTorrents - def __init__(self, client: TorrentsAPIMixIn) -> None: ... - def add( - self, - urls: Optional[Iterable[Text]] = None, - torrent_files: Optional[TorrentFilesT] = None, - save_path: Optional[Text] = None, - cookie: Optional[Text] = None, - category: Optional[Text] = None, - is_skip_checking: Optional[bool] = None, - is_paused: Optional[bool] = None, - is_root_folder: Optional[bool] = None, - rename: Optional[Text] = None, - upload_limit: Optional[Text | int] = None, - download_limit: Optional[Text | int] = None, - use_auto_torrent_management: Optional[bool] = None, - is_sequential_download: Optional[bool] = None, - is_first_last_piece_priority: Optional[bool] = None, - tags: Optional[Iterable[Text]] = None, - content_layout: Optional[ - Literal["Original", "Subfolder", "NoSubFolder"] - ] = None, - ratio_limit: Optional[Text | float] = None, - seeding_time_limit: Optional[Text | int] = None, - download_path: Optional[Text] = None, - use_download_path: Optional[bool] = None, - stop_condition: Optional[Literal["MetadataReceived", "FilesChecked"]] = None, - **kwargs: KwargsT, - ) -> Text: ... - - class _ActionForAllTorrents(ClientCache): - func: Callable[..., Any] - def __init__( - self, - client: TorrentsAPIMixIn, - func: Callable[..., Any], - ) -> None: ... - def __call__( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> Optional[Any]: ... - def all(self, **kwargs: KwargsT) -> Optional[Any]: ... - - class _Info(ClientCache): - def __call__( - self, - status_filter: Optional[TorrentStatusesT] = None, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def all( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def downloading( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def seeding( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def completed( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def paused( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def active( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def inactive( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def resumed( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def stalled( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def stalled_uploading( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def stalled_downloading( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def checking( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def moving( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def errored( - self, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - -class TorrentCategories(ClientCache): - @property - def categories(self) -> TorrentCategoriesDictionary: ... - @categories.setter - def categories(self, v: Iterable[Text]) -> None: ... - def create_category( - self, - name: Optional[Text] = None, - save_path: Optional[Text] = None, - download_path: Optional[Text] = None, - enable_download_path: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - createCategory = create_category - def edit_category( - self, - name: Optional[Text] = None, - save_path: Optional[Text] = None, - download_path: Optional[Text] = None, - enable_download_path: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - editCategory = edit_category - def remove_categories( - self, - categories: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - removeCategories = remove_categories - -class TorrentTags(ClientCache): - @property - def tags(self) -> TagList: ... - @tags.setter - def tags(self, v: Optional[Iterable[Text]] = None) -> None: ... - def add_tags( - self, - tags: Optional[Iterable[Text]] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - addTags = add_tags - def remove_tags( - self, - tags: Optional[Iterable[Text]] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - removeTags = remove_tags - def create_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - createTags = create_tags - def delete_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - deleteTags = delete_tags - -class TorrentsAPIMixIn(AppAPIMixIn): - @property - def torrents(self) -> Torrents: ... - @property - def torrent_categories(self) -> TorrentCategories: ... - @property - def torrent_tags(self) -> TorrentTags: ... - def torrents_add( - self, - urls: Optional[Iterable[Text]] = None, - torrent_files: Optional[TorrentFilesT] = None, - save_path: Optional[Text] = None, - cookie: Optional[Text] = None, - category: Optional[Text] = None, - is_skip_checking: Optional[bool] = None, - is_paused: Optional[bool] = None, - is_root_folder: Optional[bool] = None, - rename: Optional[Text] = None, - upload_limit: Optional[Text | int] = None, - download_limit: Optional[Text | int] = None, - use_auto_torrent_management: Optional[bool] = None, - is_sequential_download: Optional[bool] = None, - is_first_last_piece_priority: Optional[bool] = None, - tags: Optional[Iterable[Text]] = None, - content_layout: Optional[ - Literal["Original", "Subfolder", "NoSubFolder"] - ] = None, - ratio_limit: Optional[Text | float] = None, - seeding_time_limit: Optional[Text | int] = None, - download_path: Optional[Text] = None, - use_download_path: Optional[bool] = None, - stop_condition: Optional[Literal["MetadataReceived", "FilesChecked"]] = None, - **kwargs: KwargsT, - ) -> Literal["Ok.", "Fails."]: ... - @staticmethod - def _normalize_torrent_files( - user_files: TorrentFilesT, - ) -> FilesToSendT | None: ... - def torrents_properties( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentPropertiesDictionary: ... - def torrents_trackers( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TrackersList: ... - def torrents_webseeds( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> WebSeedsList: ... - def torrents_files( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentFilesList: ... - def torrents_piece_states( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentPieceInfoList: ... - torrents_pieceStates = torrents_piece_states - def torrents_piece_hashes( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentPieceInfoList: ... - torrents_pieceHashes = torrents_piece_hashes - def torrents_add_trackers( - self, - torrent_hash: Optional[Text] = None, - urls: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_addTrackers = torrents_add_trackers - def torrents_edit_tracker( - self, - torrent_hash: Optional[Text] = None, - original_url: Optional[Text] = None, - new_url: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_editTracker = torrents_edit_tracker - def torrents_remove_trackers( - self, - torrent_hash: Optional[Text] = None, - urls: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_removeTrackers = torrents_remove_trackers - def torrents_file_priority( - self, - torrent_hash: Optional[Text] = None, - file_ids: Optional[int | Iterable[Text | int]] = None, - priority: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_filePrio = torrents_file_priority - def torrents_rename( - self, - torrent_hash: Optional[Text] = None, - new_torrent_name: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_rename_file( - self, - torrent_hash: Optional[Text] = None, - file_id: Optional[Text | int] = None, - new_file_name: Optional[Text] = None, - old_path: Optional[Text] = None, - new_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_renameFile = torrents_rename_file - def torrents_rename_folder( - self, - torrent_hash: Optional[Text] = None, - old_path: Optional[Text] = None, - new_path: Optional[Text] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_renameFolder = torrents_rename_folder - def torrents_export( - self, - torrent_hash: Optional[Text] = None, - **kwargs: KwargsT, - ) -> bytes: ... - def torrents_info( - self, - status_filter: Optional[TorrentStatusesT] = None, - category: Optional[Text] = None, - sort: Optional[Text] = None, - reverse: Optional[bool] = None, - limit: Optional[Text | int] = None, - offset: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - tag: Optional[Text] = None, - **kwargs: KwargsT, - ) -> TorrentInfoList: ... - def torrents_resume( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_pause( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_delete( - self, - delete_files: Optional[bool] = False, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_recheck( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_reannounce( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def torrents_increase_priority( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_increasePrio = torrents_increase_priority - def torrents_decrease_priority( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_decreasePrio = torrents_decrease_priority - def torrents_top_priority( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_topPrio = torrents_top_priority - def torrents_bottom_priority( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_bottomPrio = torrents_bottom_priority - def torrents_download_limit( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> TorrentLimitsDictionary: ... - torrents_downloadLimit = torrents_download_limit - def torrents_set_download_limit( - self, - limit: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setDownloadLimit = torrents_set_download_limit - def torrents_set_share_limits( - self, - ratio_limit: Optional[Text | int] = None, - seeding_time_limit: Optional[Text | int] = None, - inactive_seeding_time_limit: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setShareLimits = torrents_set_share_limits - def torrents_upload_limit( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> TorrentLimitsDictionary: ... - torrents_uploadLimit = torrents_upload_limit - def torrents_set_upload_limit( - self, - limit: Optional[Text | int] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setUploadLimit = torrents_set_upload_limit - def torrents_set_location( - self, - location: Optional[Text] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setLocation = torrents_set_location - def torrents_set_save_path( - self, - save_path: Optional[Text] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setSavePath = torrents_set_save_path - def torrents_set_download_path( - self, - download_path: Optional[Text] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setDownloadPath = torrents_set_download_path - def torrents_set_category( - self, - category: Optional[Text] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setCategory = torrents_set_category - def torrents_set_auto_management( - self, - enable: Optional[bool] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setAutoManagement = torrents_set_auto_management - def torrents_toggle_sequential_download( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_toggleSequentialDownload = torrents_toggle_sequential_download - def torrents_toggle_first_last_piece_priority( - self, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_toggleFirstLastPiecePrio = torrents_toggle_first_last_piece_priority - def torrents_set_force_start( - self, - enable: Optional[bool] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setForceStart = torrents_set_force_start - def torrents_set_super_seeding( - self, - enable: Optional[bool] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_setSuperSeeding = torrents_set_super_seeding - def torrents_add_peers( - self, - peers: Optional[Iterable[Text]] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> TorrentsAddPeersDictionary: ... - torrents_addPeers = torrents_add_peers - def torrents_categories(self, **kwargs: KwargsT) -> TorrentCategoriesDictionary: ... - def torrents_create_category( - self, - name: Optional[Text] = None, - save_path: Optional[Text] = None, - download_path: Optional[Text] = None, - enable_download_path: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_createCategory = torrents_create_category - def torrents_edit_category( - self, - name: Optional[Text] = None, - save_path: Optional[Text] = None, - download_path: Optional[Text] = None, - enable_download_path: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_editCategory = torrents_edit_category - def torrents_remove_categories( - self, - categories: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_removeCategories = torrents_remove_categories - def torrents_tags(self, **kwargs: KwargsT) -> TagList: ... - def torrents_add_tags( - self, - tags: Optional[Iterable[Text]] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_addTags = torrents_add_tags - def torrents_remove_tags( - self, - tags: Optional[Iterable[Text]] = None, - torrent_hashes: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_removeTags = torrents_remove_tags - def torrents_create_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_createTags = torrents_create_tags - def torrents_delete_tags( - self, - tags: Optional[Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - torrents_deleteTags = torrents_delete_tags diff --git a/src/qbittorrentapi/transfer.py b/src/qbittorrentapi/transfer.py index 675aa03d..3fbd31a8 100644 --- a/src/qbittorrentapi/transfer.py +++ b/src/qbittorrentapi/transfer.py @@ -1,20 +1,26 @@ +from __future__ import annotations + +from typing import Iterable + from qbittorrentapi._version_support import v from qbittorrentapi.app import AppAPIMixIn from qbittorrentapi.decorators import alias from qbittorrentapi.decorators import aliased from qbittorrentapi.decorators import endpoint_introduced from qbittorrentapi.decorators import login_required +from qbittorrentapi.definitions import APIKwargsT from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache from qbittorrentapi.definitions import Dictionary +from qbittorrentapi.definitions import JsonValueT -class TransferInfoDictionary(Dictionary): +class TransferInfoDictionary(Dictionary[str, JsonValueT]): """Response to :meth:`~TransferAPIMixIn.transfer_info`""" @aliased -class Transfer(ClientCache): +class Transfer(ClientCache["TransferAPIMixIn"]): """ Allows interaction with the ``Transfer`` API endpoints. @@ -33,29 +39,36 @@ class Transfer(ClientCache): """ @property - def info(self): + def info(self) -> TransferInfoDictionary: """Implements :meth:`~TransferAPIMixIn.transfer_info`""" return self._client.transfer_info() @property - def speed_limits_mode(self): + def speed_limits_mode(self) -> str: """Implements :meth:`~TransferAPIMixIn.transfer_speed_limits_mode`""" return self._client.transfer_speed_limits_mode() - speedLimitsMode = speed_limits_mode - - @speedLimitsMode.setter - def speedLimitsMode(self, v): + @speed_limits_mode.setter + def speed_limits_mode(self, val: bool) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_speed_limits_mode`""" - self.speed_limits_mode = v + self.set_speed_limits_mode(intended_state=val) - @speed_limits_mode.setter - def speed_limits_mode(self, v): + @property + def speedLimitsMode(self) -> str: + """Implements :meth:`~TransferAPIMixIn.transfer_speed_limits_mode`""" + return self._client.transfer_speed_limits_mode() + + @speedLimitsMode.setter + def speedLimitsMode(self, val: bool) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_speed_limits_mode`""" - self.set_speed_limits_mode(intended_state=v) + self.set_speed_limits_mode(intended_state=val) @alias("setSpeedLimitsMode", "toggleSpeedLimitsMode", "toggle_speed_limits_mode") - def set_speed_limits_mode(self, intended_state=None, **kwargs): + def set_speed_limits_mode( + self, + intended_state: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_speed_limits_mode`""" return self._client.transfer_set_speed_limits_mode( intended_state=intended_state, @@ -63,51 +76,69 @@ def set_speed_limits_mode(self, intended_state=None, **kwargs): ) @property - def download_limit(self): + def download_limit(self) -> int: """Implements :meth:`~TransferAPIMixIn.transfer_download_limit`""" return self._client.transfer_download_limit() - downloadLimit = download_limit - - @downloadLimit.setter - def downloadLimit(self, v): + @download_limit.setter + def download_limit(self, val: int | str) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_download_limit`""" - self.download_limit = v + self.set_download_limit(limit=val) - @download_limit.setter - def download_limit(self, v): + @property + def downloadLimit(self) -> int: + """Implements :meth:`~TransferAPIMixIn.transfer_download_limit`""" + return self._client.transfer_download_limit() + + @downloadLimit.setter + def downloadLimit(self, val: int | str) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_download_limit`""" - self.set_download_limit(limit=v) + self.set_download_limit(limit=val) @property - def upload_limit(self): + def upload_limit(self) -> int: """Implements :meth:`~TransferAPIMixIn.transfer_upload_limit`""" return self._client.transfer_upload_limit() - uploadLimit = upload_limit - - @uploadLimit.setter - def uploadLimit(self, v): + @upload_limit.setter + def upload_limit(self, val: int | str) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_upload_limit`""" - self.upload_limit = v + self.set_upload_limit(limit=val) - @upload_limit.setter - def upload_limit(self, v): + @property + def uploadLimit(self) -> int: + """Implements :meth:`~TransferAPIMixIn.transfer_upload_limit`""" + return self._client.transfer_upload_limit() + + @uploadLimit.setter + def uploadLimit(self, val: int | str) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_upload_limit`""" - self.set_upload_limit(limit=v) + self.set_upload_limit(limit=val) @alias("setDownloadLimit") - def set_download_limit(self, limit=None, **kwargs): + def set_download_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_download_limit`""" return self._client.transfer_set_download_limit(limit=limit, **kwargs) @alias("setUploadLimit") - def set_upload_limit(self, limit=None, **kwargs): + def set_upload_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_set_upload_limit`""" return self._client.transfer_set_upload_limit(limit=limit, **kwargs) @alias("banPeers") - def ban_peers(self, peers=None, **kwargs): + def ban_peers( + self, + peers: str | Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """Implements :meth:`~TransferAPIMixIn.transfer_ban_peers`""" return self._client.transfer_ban_peers(peers=peers, **kwargs) @@ -125,7 +156,7 @@ class TransferAPIMixIn(AppAPIMixIn): """ @property - def transfer(self): + def transfer(self) -> Transfer: """ Allows for transparent interaction with Transfer endpoints. @@ -137,13 +168,13 @@ def transfer(self): return self._transfer @login_required - def transfer_info(self, **kwargs): + def transfer_info(self, **kwargs: APIKwargsT) -> TransferInfoDictionary: """ Retrieves the global transfer info found in qBittorrent status bar. :return: :class:`TransferInfoDictionary` - ``_ """ # noqa: E501 - return self._get( + return self._get_cast( _name=APINames.Transfer, _method="info", response_class=TransferInfoDictionary, @@ -152,13 +183,13 @@ def transfer_info(self, **kwargs): @alias("transfer_speedLimitsMode") @login_required - def transfer_speed_limits_mode(self, **kwargs): + def transfer_speed_limits_mode(self, **kwargs: APIKwargsT) -> str: """ Retrieves whether alternative speed limits are enabled. :return: ``1`` if alternative speed limits are currently enabled, ``0`` otherwise """ - return self._get( + return self._get_cast( _name=APINames.Transfer, _method="speedLimitsMode", response_class=str, @@ -171,7 +202,11 @@ def transfer_speed_limits_mode(self, **kwargs): "transfer_toggle_speed_limits_mode", ) @login_required - def transfer_set_speed_limits_mode(self, intended_state=None, **kwargs): + def transfer_set_speed_limits_mode( + self, + intended_state: bool | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Sets whether alternative speed limits are enabled. @@ -204,13 +239,13 @@ def transfer_set_speed_limits_mode(self, intended_state=None, **kwargs): @alias("transfer_downloadLimit") @login_required - def transfer_download_limit(self, **kwargs): + def transfer_download_limit(self, **kwargs: APIKwargsT) -> int: """ Retrieves download limit. 0 is unlimited. :return: integer """ - return self._get( + return self._get_cast( _name=APINames.Transfer, _method="downloadLimit", response_class=int, @@ -219,13 +254,13 @@ def transfer_download_limit(self, **kwargs): @alias("transfer_uploadLimit") @login_required - def transfer_upload_limit(self, **kwargs): + def transfer_upload_limit(self, **kwargs: APIKwargsT) -> int: """ Retrieves upload limit. 0 is unlimited. :return: integer """ - return self._get( + return self._get_cast( _name=APINames.Transfer, _method="uploadLimit", response_class=int, @@ -234,7 +269,11 @@ def transfer_upload_limit(self, **kwargs): @alias("transfer_setDownloadLimit") @login_required - def transfer_set_download_limit(self, limit=None, **kwargs): + def transfer_set_download_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the global download limit in bytes/second. @@ -251,7 +290,11 @@ def transfer_set_download_limit(self, limit=None, **kwargs): @alias("transfer_setUploadLimit") @login_required - def transfer_set_upload_limit(self, limit=None, **kwargs): + def transfer_set_upload_limit( + self, + limit: str | int | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Set the global download limit in bytes/second. @@ -269,7 +312,11 @@ def transfer_set_upload_limit(self, limit=None, **kwargs): @alias("transfer_banPeers") @endpoint_introduced("2.3", "transfer/banPeers") @login_required - def transfer_ban_peers(self, peers=None, **kwargs): + def transfer_ban_peers( + self, + peers: str | Iterable[str] | None = None, + **kwargs: APIKwargsT, + ) -> None: """ Ban one or more peers. diff --git a/src/qbittorrentapi/transfer.pyi b/src/qbittorrentapi/transfer.pyi deleted file mode 100644 index 86b48ae3..00000000 --- a/src/qbittorrentapi/transfer.pyi +++ /dev/null @@ -1,149 +0,0 @@ -from typing import Iterable -from typing import Optional -from typing import Text - -from qbittorrentapi._types import JsonValueT -from qbittorrentapi._types import KwargsT -from qbittorrentapi.app import AppAPIMixIn -from qbittorrentapi.definitions import ClientCache -from qbittorrentapi.definitions import Dictionary - -# mypy crashes when this is imported from _types... -JsonDictionaryT = Dictionary[Text, JsonValueT] - -class TransferInfoDictionary(JsonDictionaryT): ... - -class Transfer(ClientCache): - @property - def info(self) -> TransferInfoDictionary: ... - @property - def speed_limits_mode(self) -> Text: ... - @speed_limits_mode.setter - def speed_limits_mode(self, v: Text | int) -> None: ... - @property - def speedLimitsMode(self) -> Text: ... - @speedLimitsMode.setter - def speedLimitsMode(self, v: Text | int) -> None: ... - def set_speed_limits_mode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - setSpeedLimitsMode = set_speed_limits_mode - def toggleSpeedLimitsMode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def toggle_speed_limits_mode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - @property - def download_limit(self) -> int: ... - @download_limit.setter - def download_limit(self, v: Text | int) -> None: ... - @property - def downloadLimit(self) -> int: ... - @downloadLimit.setter - def downloadLimit(self, v: Text | int) -> None: ... - @property - def upload_limit(self) -> int: ... - @upload_limit.setter - def upload_limit(self, v: Text | int) -> None: ... - @property - def uploadLimit(self) -> int: ... - @uploadLimit.setter - def uploadLimit(self, v: Text | int) -> None: ... - def set_download_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def setDownloadLimit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def set_upload_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def setUploadLimit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def ban_peers( - self, - peers: Optional[Text | Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def banPeers( - self, - peers: Optional[Text | Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - -class TransferAPIMixIn(AppAPIMixIn): - @property - def transfer(self) -> Transfer: ... - def transfer_info(self, **kwargs: KwargsT) -> TransferInfoDictionary: ... - def transfer_speed_limits_mode(self, **kwargs: KwargsT) -> str: ... - def transfer_speedLimitsMode(self, **kwargs: KwargsT) -> str: ... - def transfer_set_speed_limits_mode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_setSpeedLimitsMode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_toggleSpeedLimitsMode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_toggle_speed_limits_mode( - self, - intended_state: Optional[bool] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_download_limit(self, **kwargs: KwargsT) -> int: ... - def transfer_downloadLimit(self, **kwargs: KwargsT) -> int: ... - def transfer_upload_limit(self, **kwargs: KwargsT) -> int: ... - def transfer_uploadLimit(self, **kwargs: KwargsT) -> int: ... - def transfer_set_download_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_setDownloadLimit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_set_upload_limit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_setUploadLimit( - self, - limit: Optional[Text | int] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_ban_peers( - self, - peers: Optional[Text | Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... - def transfer_banPeers( - self, - peers: Optional[Text | Iterable[Text]] = None, - **kwargs: KwargsT, - ) -> None: ... diff --git a/tests/conftest.py b/tests/conftest.py index ac509f29..65331a34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,12 +120,16 @@ def client(): def client_mock(client): """qBittorrent Client for testing with request mocks.""" client._get = MagicMock(wraps=client._get) + client._get_cast = MagicMock(wraps=client._get_cast) client._post = MagicMock(wraps=client._post) + client._post_cast = MagicMock(wraps=client._post_cast) try: yield client finally: client._get = client._get + client._get_cast = client._get_cast client._post = client._post + client._post_cast = client._post_cast @pytest.fixture diff --git a/tests/test_definitions.py b/tests/test_definitions.py index 8632a2e1..60caa614 100644 --- a/tests/test_definitions.py +++ b/tests/test_definitions.py @@ -168,39 +168,31 @@ def test_dictionary(): def test_list(client): assert len(List()) == 0 list_entries = [{"one": "1"}, {"two": "2"}, {"three": "3"}] - test_list = List(list_entries, entry_class=ListEntry, client=client) + test_list = List(list_entries, entry_class=ListEntry) assert len(test_list) == 3 assert issubclass(type(test_list[0]), ListEntry) assert test_list[0].one == "1" -def test_list_without_client(): - list_one = List([{"one": "1"}, {"two": "2"}], entry_class=ListEntry) - # without client, the entries will not be converted - assert not isinstance(list_one[0], ListEntry) - assert isinstance(list_one[0], dict) - - def test_list_actions(client): list_one = List( [{"one": "1"}, {"two": "2"}, {"three": "3"}], entry_class=ListEntry, - client=client, ) - list_two = List([{"four": "4"}], entry_class=ListEntry, client=client) + list_two = List([{"four": "4"}], entry_class=ListEntry) assert list_one[1:3] == [ - ListEntry({"two": "2"}, client=client), - ListEntry({"three": "3"}, client=client), + ListEntry({"two": "2"}), + ListEntry({"three": "3"}), ] assert list_one + list_two == [ - ListEntry({"one": "1"}, client=client), - ListEntry({"two": "2"}, client=client), - ListEntry({"three": "3"}, client=client), - ] + [ListEntry({"four": "4"}, client=client)] + ListEntry({"one": "1"}), + ListEntry({"two": "2"}), + ListEntry({"three": "3"}), + ] + [ListEntry({"four": "4"})] assert list_one.copy() == [ - ListEntry({"one": "1"}, client=client), - ListEntry({"two": "2"}, client=client), - ListEntry({"three": "3"}, client=client), + ListEntry({"one": "1"}), + ListEntry({"two": "2"}), + ListEntry({"three": "3"}), ] diff --git a/tests/test_log.py b/tests/test_log.py index e771d442..5028a313 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -29,7 +29,7 @@ def test_log_main_slice(client, main_func): def test_log_main_info(client_mock): assert isinstance(client_mock.log.main.info(), LogMainList) - client_mock._get.assert_called_with( + client_mock._get_cast.assert_called_with( _name=APINames.Log, _method="main", params={ @@ -45,7 +45,7 @@ def test_log_main_info(client_mock): def test_log_main_normal(client_mock): assert isinstance(client_mock.log.main.normal(), LogMainList) - client_mock._get.assert_called_with( + client_mock._get_cast.assert_called_with( _name=APINames.Log, _method="main", params={ @@ -61,7 +61,7 @@ def test_log_main_normal(client_mock): def test_log_main_warning(client_mock): assert isinstance(client_mock.log.main.warning(), LogMainList) - client_mock._get.assert_called_with( + client_mock._get_cast.assert_called_with( _name=APINames.Log, _method="main", params={ @@ -77,7 +77,7 @@ def test_log_main_warning(client_mock): def test_log_main_critical(client_mock): assert isinstance(client_mock.log.main.critical(), LogMainList) - client_mock._get.assert_called_with( + client_mock._get_cast.assert_called_with( _name=APINames.Log, _method="main", params={ @@ -102,7 +102,7 @@ def test_log_main_levels(client_mock, main_func, include_level): ) actual_include = None if include_level is None else bool(include_level) - client_mock._get.assert_called_with( + client_mock._get_cast.assert_called_with( _name=APINames.Log, _method="main", params={ diff --git a/tests/test_request.py b/tests/test_request.py index ac06870d..b978d2b3 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -261,13 +261,6 @@ def test_port(app_version): assert client.app.version == app_version -def test_response_none(client): - response = MagicMock(spec_set=Response) - - assert client._cast(response, None) == response - assert client._cast(response, response_class=None) == response - - def test_response_str(client): response = MagicMock(spec_set=Response) @@ -462,22 +455,17 @@ def test_request_extra_params(client, orig_torrent): assert isinstance(torrent, TorrentDictionary) -def test_mock_api_version(): - client = Client(MOCK_WEB_API_VERSION="1.5", VERIFY_WEBUI_CERTIFICATE=False) - assert client.app_web_api_version() == "1.5" - - -def test_unsupported_version_error(): +def test_unsupported_version_error(monkeypatch): if IS_QBT_DEV: return client = Client( - MOCK_WEB_API_VERSION="0.0.0", VERIFY_WEBUI_CERTIFICATE=False, RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS=True, ) + monkeypatch.setattr(client, "app_version", MagicMock(return_value="1.0.0")) with pytest.raises(exceptions.UnsupportedQbittorrentVersion): - client.app_version() + client.app_web_api_version() client = Client( VERIFY_WEBUI_CERTIFICATE=False, @@ -568,27 +556,31 @@ def test_http404(client, params): client.torrents_rename(hash="zxcv", new_torrent_name="erty") assert "zxcv" in exc_info.value.args[0] - response = MagicMock(spec=Response, status_code=404, text="") + response = MagicMock(spec=Response, status_code=404, text="", request=object()) with pytest.raises(exceptions.HTTPError, match="") as exc_info: Request._handle_error_responses(data={}, params=params, response=response) assert exc_info.value.http_status_code == 404 if params: assert params[list(params.keys())[0]] in exc_info.value.args[0] - response = MagicMock(spec=Response, status_code=404, text="unexpected msg") + response = MagicMock( + spec=Response, status_code=404, text="unexpected msg", request=object() + ) with pytest.raises(exceptions.HTTPError, match="unexpected msg") as exc_info: Request._handle_error_responses(data={}, params=params, response=response) assert exc_info.value.http_status_code == 404 assert exc_info.value.args[0] == "unexpected msg" - response = MagicMock(spec=Response, status_code=404, text="") + response = MagicMock(spec=Response, status_code=404, text="", request=object()) with pytest.raises(exceptions.HTTPError, match="") as exc_info: Request._handle_error_responses(data=params, params={}, response=response) assert exc_info.value.http_status_code == 404 if params: assert params[list(params.keys())[0]] in exc_info.value.args[0] - response = MagicMock(spec=Response, status_code=404, text="unexpected msg") + response = MagicMock( + spec=Response, status_code=404, text="unexpected msg", request=object() + ) with pytest.raises(exceptions.HTTPError, match="unexpected msg") as exc_info: Request._handle_error_responses(data=params, params={}, response=response) assert exc_info.value.http_status_code == 404 @@ -618,7 +610,9 @@ def test_http415(client): @pytest.mark.parametrize("status_code", (500, 503)) def test_http500(status_code): - response = MagicMock(spec=Response, status_code=status_code, text="asdf") + response = MagicMock( + spec=Response, status_code=status_code, text="asdf", request=object() + ) with pytest.raises(exceptions.InternalServerError500Error) as exc_info: Request._handle_error_responses(data={}, params={}, response=response) assert exc_info.value.http_status_code == status_code @@ -626,7 +620,9 @@ def test_http500(status_code): @pytest.mark.parametrize("status_code", (402, 406)) def test_http_error(status_code): - response = MagicMock(spec=Response, status_code=status_code, text="asdf") + response = MagicMock( + spec=Response, status_code=status_code, text="asdf", request=object() + ) with pytest.raises(exceptions.HTTPError) as exc_info: Request._handle_error_responses(data={}, params={}, response=response) assert exc_info.value.http_status_code == status_code @@ -664,7 +660,8 @@ def test_verbose_logging(caplog): def test_stack_printing(capsys): - client = Client(PRINT_STACK_FOR_EACH_REQUEST=True, VERIFY_WEBUI_CERTIFICATE=False) + client = Client(VERIFY_WEBUI_CERTIFICATE=False) + client._PRINT_STACK_FOR_EACH_REQUEST = True client.app_version() assert "print_stack()" in capsys.readouterr().err diff --git a/tests/test_torrent.py b/tests/test_torrent.py index ade3c29c..be771a77 100644 --- a/tests/test_torrent.py +++ b/tests/test_torrent.py @@ -1,6 +1,7 @@ import platform from time import sleep from types import MethodType +from unittest.mock import MagicMock import pytest @@ -24,9 +25,9 @@ def test_info(orig_torrent, monkeypatch): assert orig_torrent.info.hash == orig_torrent.hash # mimic <=v2.0.1 where torrents_info() doesn't support hash arg - orig_torrent._client._MOCK_WEB_API_VERSION = "2" - assert orig_torrent.info.hash == orig_torrent.hash - orig_torrent._client._MOCK_WEB_API_VERSION = None + with monkeypatch.context() as m: + m.setattr(orig_torrent._client, "app_version", MagicMock(return_value="2.0.0")) + assert orig_torrent.info.hash == orig_torrent.hash # ensure if things are really broken, an empty TorrentDictionary is returned... if platform.python_implementation() == "CPython": diff --git a/tests/utils.py b/tests/utils.py index 96468794..f98a5bb2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,6 +6,7 @@ from qbittorrentapi import APIConnectionError from qbittorrentapi import Client +from qbittorrentapi import TorrentDictionary from qbittorrentapi._version_support import ( APP_VERSION_2_API_VERSION_MAP as api_version_map, ) @@ -72,7 +73,7 @@ def wrapper(*args, **kwargs): return inner -def get_torrent(client, torrent_hash): +def get_torrent(client, torrent_hash) -> TorrentDictionary: """Retrieve a torrent from qBittorrent.""" try: # not all versions of torrents_info() support passing a hash