From 6f4e90ea14b916809d6363d432639df7525d900f Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sun, 9 Jun 2024 23:36:06 +0800 Subject: [PATCH 01/20] remove requests --- poetry.lock | 8 ++-- pyproject.toml | 10 +++-- transmission_rpc/client.py | 82 +++++++++++++++++++++----------------- transmission_rpc/error.py | 9 ++--- 4 files changed, 60 insertions(+), 49 deletions(-) diff --git a/poetry.lock b/poetry.lock index 014e865c..3743e35d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1185,13 +1185,13 @@ urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.12.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1539,4 +1539,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "5df4e17725cc46b7d7f92b65ae766a11373992a79127d6083fff1212c56553ab" +content-hash = "5f8678f14687b1f238272011262a070a8de00fdfa9ca385d990edb11e295b844" diff --git a/pyproject.toml b/pyproject.toml index b1bd4b6b..c81b3dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8" # dependencies -requests = "^2.23.0" +urllib3 = "^2.2.1" +certifi = '>=2017.4.17' typing-extensions = ">=4.5.0" [tool.poetry.group.docs.dependencies] @@ -37,10 +38,12 @@ sphinx = { version = "^7.3.7", python = "^3.9" } furo = { version = "2024.5.6", python = "^3.9" } sphinx-copybutton = { version = "^0.5.2", python = "^3.9" } sphinx-new-tab-link = { version = "^0.4.0", python = "^3.9" } -sphinx-github-style = {version = "^1.2.2", python = "^3.9"} +sphinx-github-style = { version = "^1.2.2", python = "^3.9" } [tool.poetry.group.dev.dependencies] yarl = "==1.9.4" +# we have a example need this +requests = "^2.23.0" # tests pytest = "==8.2.2" pytest-github-actions-annotate-failures = "==0.2.0" @@ -52,7 +55,7 @@ mypy = { version = "==1.10.0", markers = "implementation_name != 'pypy'", python # stubs types-requests = "==2.32.0.20240602" -sphinx-autobuild = {version = "2024.4.16", python = "^3.9"} +sphinx-autobuild = { version = "2024.4.16", python = "^3.9" } [tool.pytest.ini_options] addopts = '-rav -Werror' @@ -147,4 +150,5 @@ ignore = [ 'PLR0915', 'PLR2004', 'PGH003', + 'TCH002', ] diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 2209e8b7..5ec72377 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.metadata import json import logging import pathlib @@ -7,12 +8,11 @@ import time import types from typing import Any, BinaryIO, Iterable, List, TypeVar, Union -from urllib.parse import quote -import requests -import requests.auth -import requests.exceptions +import certifi +import urllib3 from typing_extensions import Literal, Self, TypedDict, deprecated +from urllib3.util import make_headers from transmission_rpc.constants import DEFAULT_TIMEOUT, LOGGER, RpcMethod from transmission_rpc.error import ( @@ -26,12 +26,16 @@ from transmission_rpc.types import Group, _Timeout from transmission_rpc.utils import _try_read_torrent, get_torrent_arguments +_USER_AGENT = "transmission-rpc/{} (https://github.com/trim21/transmission-rpc)".format( + importlib.metadata.version("transmission_rpc") +) + _hex_chars = frozenset(string.hexdigits.lower()) _TorrentID = Union[int, str] _TorrentIDs = Union[_TorrentID, List[_TorrentID], None] -_header_session_id = "x-transmission-session-id" +_header_session_id_key = "x-transmission-session-id" class ResponseData(TypedDict): @@ -110,23 +114,23 @@ def __init__( ) self._query_timeout: _Timeout = timeout - username = quote(username or "", safe="$-_.+!*'(),;&=", encoding="utf8") if username else "" - password = ":" + quote(password or "", safe="$-_.+!*'(),;&=", encoding="utf8") if password else "" - auth = f"{username}{password}@" if (username or password) else "" + if username or password: + self.__auth_headers = make_headers(basic_auth=f"{username}:{password}", user_agent=_USER_AGENT) + else: + self.__auth_headers = make_headers(user_agent=_USER_AGENT) if path == "/transmission/": path = "/transmission/rpc" - url = f"{protocol}://{auth}{host}:{port}{path}" + url = f"{protocol}://{host}:{port}{path}" self._url = str(url) self.__raw_session: dict[str, Any] = {} self.__session_id = "0" self.__server_version: str = "(unknown)" self.__protocol_version: int = 17 # default 17 - self._http_session = requests.Session() - self._http_session.trust_env = False + self.__http_client = urllib3.PoolManager(ca_certs=certifi.where()) self.__semver_version = None - self.get_session() + self.get_session(arguments=["rpc-version", "rpc-version-semver", "version"]) self.__torrent_get_arguments = get_torrent_arguments(self.__protocol_version) @property @@ -185,9 +189,8 @@ def timeout(self) -> None: """ self._query_timeout = DEFAULT_TIMEOUT - @property - def _http_header(self) -> dict[str, str]: - return {_header_session_id: self.__session_id} + def __get_headers(self) -> dict[str, str]: + return self.__auth_headers | {_header_session_id_key: self.__session_id} def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> str: """ @@ -199,38 +202,34 @@ def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> while True: if request_count >= 3: raise TransmissionError("too much request, try enable logger to see what happened") - self.logger.debug( - { - "url": self._url, - "headers": self._http_header, - "data": query, - "timeout": timeout, - } - ) + + headers = self.__get_headers() + self.logger.debug({"url": self._url, "headers": headers, "data": query, "timeout": timeout}) request_count += 1 try: - r = self._http_session.post( + r = self.__http_client.request( + "POST", self._url, - headers=self._http_header, + headers=headers, json=query, timeout=timeout, ) - except requests.exceptions.Timeout as e: + except urllib3.exceptions.TimeoutError as e: raise TransmissionTimeoutError("timeout when connection to transmission daemon") from e - except requests.exceptions.ConnectionError as e: + except urllib3.exceptions.ConnectionError as e: raise TransmissionConnectError(f"can't connect to transmission daemon: {e!s}") from e - self.logger.debug(r.text) - if r.status_code in {401, 403}: - self.logger.debug(r.request.headers) + self.logger.debug(r.data) + if r.status in {401, 403}: + self.logger.debug(headers) raise TransmissionAuthError("transmission daemon require auth", original=r) - if _header_session_id in r.headers: - self.__session_id = r.headers["x-transmission-session-id"] + if _header_session_id_key in r.headers: + self.__session_id = r.headers[_header_session_id_key] - if r.status_code != 409: - return r.text + if r.status != 409: + return r.data.decode("utf-8") def _request( self, @@ -810,11 +809,20 @@ def queue_down(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """Move transfer down in the queue.""" self._request(RpcMethod.QueueMoveDown, ids=ids, require_ids=True, timeout=timeout) - def get_session(self, timeout: _Timeout | None = None) -> Session: + def get_session( + self, + timeout: _Timeout | None = None, + arguments: Iterable[str] | None = None, + ) -> Session: """ Get session parameters. See the Session class for more information. """ - self._request(RpcMethod.SessionGet, timeout=timeout) + + data = {} + if arguments: + data["fields"] = list(arguments) + + self._request(RpcMethod.SessionGet, timeout=timeout, arguments=data) self._update_server_version() return Session(fields=self.__raw_session) @@ -1142,7 +1150,7 @@ def __exit__( exc_val: BaseException | None, exc_tb: types.TracebackType | None, ) -> None: - self._http_session.close() + self.__http_client.close() T = TypeVar("T") diff --git a/transmission_rpc/error.py b/transmission_rpc/error.py index 8530b839..f98488bc 100644 --- a/transmission_rpc/error.py +++ b/transmission_rpc/error.py @@ -4,10 +4,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any -if TYPE_CHECKING: - from requests.models import Response +from urllib3 import BaseHTTPResponse class TransmissionError(Exception): @@ -21,7 +20,7 @@ class TransmissionError(Exception): argument: Any | None # rpc call arguments response: Any | None # parsed json response, may be dict with keys 'result' and 'arguments' rawResponse: str | None # raw text http response - original: Response | None # original http requests + original: BaseHTTPResponse | None # original http requests def __init__( self, @@ -30,7 +29,7 @@ def __init__( argument: Any | None = None, response: Any | None = None, rawResponse: str | None = None, - original: Response | None = None, + original: BaseHTTPResponse | None = None, ): super().__init__() self.message = message From a8b6b757f424d6fd70f2b0c055305309d3bf4c9f Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:06:49 +0800 Subject: [PATCH 02/20] improve --- transmission_rpc/__init__.py | 4 +- transmission_rpc/client.py | 116 ++++++++++++++++++---------------- transmission_rpc/constants.py | 3 - transmission_rpc/types.py | 5 +- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/transmission_rpc/__init__.py b/transmission_rpc/__init__.py index 1ea5caf0..7300ac95 100644 --- a/transmission_rpc/__init__.py +++ b/transmission_rpc/__init__.py @@ -1,8 +1,8 @@ import logging import urllib.parse -from transmission_rpc.client import Client -from transmission_rpc.constants import DEFAULT_TIMEOUT, LOGGER, IdleMode, Priority, RatioLimitMode +from transmission_rpc.client import DEFAULT_TIMEOUT, Client +from transmission_rpc.constants import LOGGER, IdleMode, Priority, RatioLimitMode from transmission_rpc.error import ( TransmissionAuthError, TransmissionConnectError, diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 5ec72377..66355623 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -12,9 +12,10 @@ import certifi import urllib3 from typing_extensions import Literal, Self, TypedDict, deprecated +from urllib3 import Timeout from urllib3.util import make_headers -from transmission_rpc.constants import DEFAULT_TIMEOUT, LOGGER, RpcMethod +from transmission_rpc.constants import LOGGER, RpcMethod from transmission_rpc.error import ( TransmissionAuthError, TransmissionConnectError, @@ -23,10 +24,10 @@ ) from transmission_rpc.session import Session, SessionStats from transmission_rpc.torrent import Torrent -from transmission_rpc.types import Group, _Timeout +from transmission_rpc.types import Group from transmission_rpc.utils import _try_read_torrent, get_torrent_arguments -_USER_AGENT = "transmission-rpc/{} (https://github.com/trim21/transmission-rpc)".format( +__USER_AGENT__ = "transmission-rpc/{} (https://github.com/trim21/transmission-rpc)".format( importlib.metadata.version("transmission_rpc") ) @@ -37,6 +38,8 @@ _header_session_id_key = "x-transmission-session-id" +DEFAULT_TIMEOUT = 30.0 + class ResponseData(TypedDict): arguments: Any @@ -82,6 +85,8 @@ def _parse_torrent_ids(args: Any) -> str | list[str | int]: class Client: + __query_timeout: Timeout + def __init__( self, *, @@ -112,24 +117,32 @@ def __init__( raise TypeError( "logger must be instance of `logging.Logger`, default: logging.getLogger('transmission-rpc')" ) - self._query_timeout: _Timeout = timeout + if isinstance(timeout, (int, float)): + self.__query_timeout = Timeout(timeout) + elif isinstance(timeout, Timeout): + self.__query_timeout = timeout + else: + raise TypeError(f"unsupported value {timeout!r}, only Timeout/float/int are supported") if username or password: - self.__auth_headers = make_headers(basic_auth=f"{username}:{password}", user_agent=_USER_AGENT) + self.__auth_headers = make_headers(basic_auth=f"{username}:{password}", user_agent=__USER_AGENT__) else: - self.__auth_headers = make_headers(user_agent=_USER_AGENT) + self.__auth_headers = make_headers(user_agent=__USER_AGENT__) if path == "/transmission/": path = "/transmission/rpc" url = f"{protocol}://{host}:{port}{path}" self._url = str(url) + self.__raw_session: dict[str, Any] = {} self.__session_id = "0" + self.__server_version: str = "(unknown)" self.__protocol_version: int = 17 # default 17 - self.__http_client = urllib3.PoolManager(ca_certs=certifi.where()) self.__semver_version = None + + self.__http_client = urllib3.PoolManager(ca_certs=certifi.where(), timeout=self.timeout) self.get_session(arguments=["rpc-version", "rpc-version-semver", "version"]) self.__torrent_get_arguments = get_torrent_arguments(self.__protocol_version) @@ -159,46 +172,39 @@ def server_version(self) -> str: return self.__server_version @property - def timeout(self) -> _Timeout: + def timeout(self) -> Timeout: """ Get current timeout for HTTP queries. """ - return self._query_timeout + return self.__query_timeout @timeout.setter - def timeout(self, value: _Timeout) -> None: + def timeout(self, value: Timeout) -> None: """ Set timeout for HTTP queries. """ - if isinstance(value, (tuple, list)): - if len(value) != 2: - raise ValueError("timeout tuple can only include 2 numbers elements") - for v in value: - if not isinstance(v, (float, int)): - raise TypeError("element of timeout tuple can only be int of float") - self._query_timeout = (value[0], value[1]) # for type checker - elif value is None: - self._query_timeout = DEFAULT_TIMEOUT - else: - self._query_timeout = float(value) + if not isinstance(value, Timeout): + raise TypeError("must use Timeout instance") + + self.__query_timeout = value @timeout.deleter def timeout(self) -> None: """ Reset the HTTP query timeout to the default. """ - self._query_timeout = DEFAULT_TIMEOUT + self.__query_timeout = Timeout(DEFAULT_TIMEOUT) def __get_headers(self) -> dict[str, str]: - return self.__auth_headers | {_header_session_id_key: self.__session_id} + self.__auth_headers[_header_session_id_key] = self.__session_id + + return self.__auth_headers - def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> str: + def _http_query(self, query: dict[str, Any], timeout: Timeout | None = None) -> str: """ Query Transmission through HTTP. """ request_count = 0 - if timeout is None: - timeout = self.timeout while True: if request_count >= 3: raise TransmissionError("too much request, try enable logger to see what happened") @@ -210,7 +216,7 @@ def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> try: r = self.__http_client.request( "POST", - self._url, + url=self._url, headers=headers, json=query, timeout=timeout, @@ -237,7 +243,7 @@ def _request( arguments: dict[str, Any] | None = None, ids: _TorrentIDs | None = None, require_ids: bool = False, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, ) -> dict[str, Any]: """ Send json-rpc request to Transmission using http POST @@ -373,7 +379,7 @@ def _rpc_version_warning(self, required_version: int) -> None: def add_torrent( self, torrent: BinaryIO | str | bytes | pathlib.Path, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, *, download_dir: str | None = None, files_unwanted: list[int] | None = None, @@ -458,7 +464,7 @@ def add_torrent( return next(iter(self._request(RpcMethod.TorrentAdd, kwargs, timeout=timeout).values())) - def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: _Timeout | None = None) -> None: + def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: Timeout | None = None) -> None: """ remove torrent(s) with provided id(s). @@ -472,14 +478,14 @@ def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: _ timeout=timeout, ) - def start_torrent(self, ids: _TorrentIDs, bypass_queue: bool = False, timeout: _Timeout | None = None) -> None: + def start_torrent(self, ids: _TorrentIDs, bypass_queue: bool = False, timeout: Timeout | None = None) -> None: """Start torrent(s) with provided id(s)""" method = RpcMethod.TorrentStart if bypass_queue: method = RpcMethod.TorrentStartNow self._request(method, {}, ids, True, timeout=timeout) - def start_all(self, bypass_queue: bool = False, timeout: _Timeout | None = None) -> None: + def start_all(self, bypass_queue: bool = False, timeout: Timeout | None = None) -> None: """Start all torrents respecting the queue order""" method = RpcMethod.TorrentStart if bypass_queue: @@ -493,15 +499,15 @@ def start_all(self, bypass_queue: bool = False, timeout: _Timeout | None = None) timeout=timeout, ) - def stop_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def stop_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """stop torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentStop, {}, ids, True, timeout=timeout) - def verify_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def verify_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """verify torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentVerify, {}, ids, True, timeout=timeout) - def reannounce_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def reannounce_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """Reannounce torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentReannounce, {}, ids, True, timeout=timeout) @@ -509,7 +515,7 @@ def get_torrent( self, torrent_id: _TorrentID, arguments: Iterable[str] | None = None, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, ) -> Torrent: """ Get information for torrent with provided id. @@ -567,7 +573,7 @@ def get_torrents( self, ids: _TorrentIDs | None = None, arguments: Iterable[str] | None = None, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, ) -> list[Torrent]: """ Get information for torrents with provided ids. For more information see :py:meth:`Client.get_torrent`. @@ -584,7 +590,7 @@ def get_torrents( ] def get_recently_active_torrents( - self, arguments: Iterable[str] | None = None, timeout: _Timeout | None = None + self, arguments: Iterable[str] | None = None, timeout: Timeout | None = None ) -> tuple[list[Torrent], list[int]]: """ Get information for torrents for recently active torrent. If you want to get recently-removed @@ -606,7 +612,7 @@ def get_recently_active_torrents( def change_torrent( self, ids: _TorrentIDs, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, *, bandwidth_priority: int | None = None, download_limit: int | None = None, @@ -740,7 +746,7 @@ def move_torrent_data( self, ids: _TorrentIDs, location: str | pathlib.Path, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, *, move: bool = True, ) -> None: @@ -758,7 +764,7 @@ def rename_torrent_path( torrent_id: _TorrentID, location: str, name: str, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, ) -> tuple[str, str]: """ Warnings: @@ -785,7 +791,7 @@ def rename_torrent_path( return result["path"], result["name"] - def queue_top(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def queue_top(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """ Move transfer to the top of the queue. @@ -793,7 +799,7 @@ def queue_top(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """ self._request(RpcMethod.QueueMoveTop, ids=ids, require_ids=True, timeout=timeout) - def queue_bottom(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def queue_bottom(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """ Move transfer to the bottom of the queue. @@ -801,17 +807,17 @@ def queue_bottom(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> Non """ self._request(RpcMethod.QueueMoveBottom, ids=ids, require_ids=True, timeout=timeout) - def queue_up(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def queue_up(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """Move transfer up in the queue.""" self._request(RpcMethod.QueueMoveUp, ids=ids, require_ids=True, timeout=timeout) - def queue_down(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: + def queue_down(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """Move transfer down in the queue.""" self._request(RpcMethod.QueueMoveDown, ids=ids, require_ids=True, timeout=timeout) def get_session( self, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, arguments: Iterable[str] | None = None, ) -> Session: """ @@ -828,7 +834,7 @@ def get_session( def set_session( self, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, *, alt_speed_down: int | None = None, alt_speed_enabled: bool | None = None, @@ -1058,12 +1064,12 @@ def set_session( if args: self._request(RpcMethod.SessionSet, args, timeout=timeout) - def blocklist_update(self, timeout: _Timeout | None = None) -> int | None: + def blocklist_update(self, timeout: Timeout | None = None) -> int | None: """Update block list. Returns the size of the block list.""" result = self._request(RpcMethod.BlocklistUpdate, timeout=timeout) return result.get("blocklist-size") - def port_test(self, timeout: _Timeout | None = None) -> bool | None: + def port_test(self, timeout: Timeout | None = None) -> bool | None: """ Tests to see if your incoming peer port is accessible from the outside world. @@ -1071,7 +1077,7 @@ def port_test(self, timeout: _Timeout | None = None) -> bool | None: result = self._request(RpcMethod.PortTest, timeout=timeout) return result.get("port-is-open") - def free_space(self, path: str | pathlib.Path, timeout: _Timeout | None = None) -> int | None: + def free_space(self, path: str | pathlib.Path, timeout: Timeout | None = None) -> int | None: """ Get the amount of free space (in bytes) at the provided location. """ @@ -1082,7 +1088,7 @@ def free_space(self, path: str | pathlib.Path, timeout: _Timeout | None = None) return result["size-bytes"] return None - def session_stats(self, timeout: _Timeout | None = None) -> SessionStats: + def session_stats(self, timeout: Timeout | None = None) -> SessionStats: """Get session statistics""" result = self._request(RpcMethod.SessionStats, timeout=timeout) return SessionStats(fields=result) @@ -1091,7 +1097,7 @@ def set_group( self, name: str, *, - timeout: _Timeout | None = None, + timeout: Timeout | None = None, honors_session_limits: bool | None = None, speed_limit_down_enabled: bool | None = None, speed_limit_down: int | None = None, @@ -1123,7 +1129,7 @@ def set_group( self._request(RpcMethod.GroupSet, arguments, timeout=timeout) - def get_group(self, name: str, *, timeout: _Timeout | None = None) -> Group | None: + def get_group(self, name: str, *, timeout: Timeout | None = None) -> Group | None: self._rpc_version_warning(17) result: dict[str, Any] = self._request(RpcMethod.GroupGet, {"group": name}, timeout=timeout) @@ -1132,7 +1138,7 @@ def get_group(self, name: str, *, timeout: _Timeout | None = None) -> Group | No return None - def get_groups(self, name: list[str] | None = None, *, timeout: _Timeout | None = None) -> dict[str, Group]: + def get_groups(self, name: list[str] | None = None, *, timeout: Timeout | None = None) -> dict[str, Group]: payload = {} if name is not None: payload = {"group": name} @@ -1150,7 +1156,7 @@ def __exit__( exc_val: BaseException | None, exc_tb: types.TracebackType | None, ) -> None: - self.__http_client.close() + self.__http_client.clear() T = TypeVar("T") diff --git a/transmission_rpc/constants.py b/transmission_rpc/constants.py index 81332bca..b63ffca6 100644 --- a/transmission_rpc/constants.py +++ b/transmission_rpc/constants.py @@ -11,9 +11,6 @@ LOGGER.setLevel(logging.ERROR) -DEFAULT_TIMEOUT = 30.0 - - class Priority(enum.IntEnum): Low = -1 Normal = 0 diff --git a/transmission_rpc/types.py b/transmission_rpc/types.py index fa7d7b09..3c88b2e2 100644 --- a/transmission_rpc/types.py +++ b/transmission_rpc/types.py @@ -1,12 +1,9 @@ from __future__ import annotations -from typing import Any, NamedTuple, Optional, Tuple, TypeVar, Union +from typing import Any, NamedTuple, TypeVar from transmission_rpc.constants import Priority -_Number = Union[int, float] -_Timeout = Optional[Union[_Number, Tuple[_Number, _Number]]] - T = TypeVar("T") From 621f332fdc24f70f62f5124f121598591700c8b6 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:07:39 +0800 Subject: [PATCH 03/20] improve --- transmission_rpc/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 66355623..ce573d17 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -96,7 +96,7 @@ def __init__( host: str = "127.0.0.1", port: int = 9091, path: str = "/transmission/rpc", - timeout: float = DEFAULT_TIMEOUT, + timeout: float | Timeout = DEFAULT_TIMEOUT, logger: logging.Logger = LOGGER, ): """ From 432d8306746a071b4bde1de9cccdef27bca8233a Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:15:09 +0800 Subject: [PATCH 04/20] improve timeout types --- transmission_rpc/client.py | 62 +++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index ce573d17..b3e4d212 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -40,6 +40,8 @@ DEFAULT_TIMEOUT = 30.0 +_Timeout = Union[Timeout, int, float] + class ResponseData(TypedDict): arguments: Any @@ -200,11 +202,15 @@ def __get_headers(self) -> dict[str, str]: return self.__auth_headers - def _http_query(self, query: dict[str, Any], timeout: Timeout | None = None) -> str: + def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> str: """ Query Transmission through HTTP. """ request_count = 0 + + if isinstance(timeout, (int, float)): + timeout = Timeout(read=timeout, connect=timeout) + while True: if request_count >= 3: raise TransmissionError("too much request, try enable logger to see what happened") @@ -243,7 +249,7 @@ def _request( arguments: dict[str, Any] | None = None, ids: _TorrentIDs | None = None, require_ids: bool = False, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, ) -> dict[str, Any]: """ Send json-rpc request to Transmission using http POST @@ -379,7 +385,7 @@ def _rpc_version_warning(self, required_version: int) -> None: def add_torrent( self, torrent: BinaryIO | str | bytes | pathlib.Path, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, *, download_dir: str | None = None, files_unwanted: list[int] | None = None, @@ -464,7 +470,7 @@ def add_torrent( return next(iter(self._request(RpcMethod.TorrentAdd, kwargs, timeout=timeout).values())) - def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: Timeout | None = None) -> None: + def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: _Timeout | None = None) -> None: """ remove torrent(s) with provided id(s). @@ -478,14 +484,14 @@ def remove_torrent(self, ids: _TorrentIDs, delete_data: bool = False, timeout: T timeout=timeout, ) - def start_torrent(self, ids: _TorrentIDs, bypass_queue: bool = False, timeout: Timeout | None = None) -> None: + def start_torrent(self, ids: _TorrentIDs, bypass_queue: bool = False, timeout: _Timeout | None = None) -> None: """Start torrent(s) with provided id(s)""" method = RpcMethod.TorrentStart if bypass_queue: method = RpcMethod.TorrentStartNow self._request(method, {}, ids, True, timeout=timeout) - def start_all(self, bypass_queue: bool = False, timeout: Timeout | None = None) -> None: + def start_all(self, bypass_queue: bool = False, timeout: _Timeout | None = None) -> None: """Start all torrents respecting the queue order""" method = RpcMethod.TorrentStart if bypass_queue: @@ -499,15 +505,15 @@ def start_all(self, bypass_queue: bool = False, timeout: Timeout | None = None) timeout=timeout, ) - def stop_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def stop_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """stop torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentStop, {}, ids, True, timeout=timeout) - def verify_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def verify_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """verify torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentVerify, {}, ids, True, timeout=timeout) - def reannounce_torrent(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def reannounce_torrent(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """Reannounce torrent(s) with provided id(s)""" self._request(RpcMethod.TorrentReannounce, {}, ids, True, timeout=timeout) @@ -515,7 +521,7 @@ def get_torrent( self, torrent_id: _TorrentID, arguments: Iterable[str] | None = None, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, ) -> Torrent: """ Get information for torrent with provided id. @@ -573,7 +579,7 @@ def get_torrents( self, ids: _TorrentIDs | None = None, arguments: Iterable[str] | None = None, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, ) -> list[Torrent]: """ Get information for torrents with provided ids. For more information see :py:meth:`Client.get_torrent`. @@ -590,7 +596,7 @@ def get_torrents( ] def get_recently_active_torrents( - self, arguments: Iterable[str] | None = None, timeout: Timeout | None = None + self, arguments: Iterable[str] | None = None, timeout: _Timeout | None = None ) -> tuple[list[Torrent], list[int]]: """ Get information for torrents for recently active torrent. If you want to get recently-removed @@ -612,7 +618,7 @@ def get_recently_active_torrents( def change_torrent( self, ids: _TorrentIDs, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, *, bandwidth_priority: int | None = None, download_limit: int | None = None, @@ -746,7 +752,7 @@ def move_torrent_data( self, ids: _TorrentIDs, location: str | pathlib.Path, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, *, move: bool = True, ) -> None: @@ -764,7 +770,7 @@ def rename_torrent_path( torrent_id: _TorrentID, location: str, name: str, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, ) -> tuple[str, str]: """ Warnings: @@ -791,7 +797,7 @@ def rename_torrent_path( return result["path"], result["name"] - def queue_top(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def queue_top(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """ Move transfer to the top of the queue. @@ -799,7 +805,7 @@ def queue_top(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: """ self._request(RpcMethod.QueueMoveTop, ids=ids, require_ids=True, timeout=timeout) - def queue_bottom(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def queue_bottom(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """ Move transfer to the bottom of the queue. @@ -807,17 +813,17 @@ def queue_bottom(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None """ self._request(RpcMethod.QueueMoveBottom, ids=ids, require_ids=True, timeout=timeout) - def queue_up(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def queue_up(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """Move transfer up in the queue.""" self._request(RpcMethod.QueueMoveUp, ids=ids, require_ids=True, timeout=timeout) - def queue_down(self, ids: _TorrentIDs, timeout: Timeout | None = None) -> None: + def queue_down(self, ids: _TorrentIDs, timeout: _Timeout | None = None) -> None: """Move transfer down in the queue.""" self._request(RpcMethod.QueueMoveDown, ids=ids, require_ids=True, timeout=timeout) def get_session( self, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, arguments: Iterable[str] | None = None, ) -> Session: """ @@ -834,7 +840,7 @@ def get_session( def set_session( self, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, *, alt_speed_down: int | None = None, alt_speed_enabled: bool | None = None, @@ -1064,12 +1070,12 @@ def set_session( if args: self._request(RpcMethod.SessionSet, args, timeout=timeout) - def blocklist_update(self, timeout: Timeout | None = None) -> int | None: + def blocklist_update(self, timeout: _Timeout | None = None) -> int | None: """Update block list. Returns the size of the block list.""" result = self._request(RpcMethod.BlocklistUpdate, timeout=timeout) return result.get("blocklist-size") - def port_test(self, timeout: Timeout | None = None) -> bool | None: + def port_test(self, timeout: _Timeout | None = None) -> bool | None: """ Tests to see if your incoming peer port is accessible from the outside world. @@ -1077,7 +1083,7 @@ def port_test(self, timeout: Timeout | None = None) -> bool | None: result = self._request(RpcMethod.PortTest, timeout=timeout) return result.get("port-is-open") - def free_space(self, path: str | pathlib.Path, timeout: Timeout | None = None) -> int | None: + def free_space(self, path: str | pathlib.Path, timeout: _Timeout | None = None) -> int | None: """ Get the amount of free space (in bytes) at the provided location. """ @@ -1088,7 +1094,7 @@ def free_space(self, path: str | pathlib.Path, timeout: Timeout | None = None) - return result["size-bytes"] return None - def session_stats(self, timeout: Timeout | None = None) -> SessionStats: + def session_stats(self, timeout: _Timeout | None = None) -> SessionStats: """Get session statistics""" result = self._request(RpcMethod.SessionStats, timeout=timeout) return SessionStats(fields=result) @@ -1097,7 +1103,7 @@ def set_group( self, name: str, *, - timeout: Timeout | None = None, + timeout: _Timeout | None = None, honors_session_limits: bool | None = None, speed_limit_down_enabled: bool | None = None, speed_limit_down: int | None = None, @@ -1129,7 +1135,7 @@ def set_group( self._request(RpcMethod.GroupSet, arguments, timeout=timeout) - def get_group(self, name: str, *, timeout: Timeout | None = None) -> Group | None: + def get_group(self, name: str, *, timeout: _Timeout | None = None) -> Group | None: self._rpc_version_warning(17) result: dict[str, Any] = self._request(RpcMethod.GroupGet, {"group": name}, timeout=timeout) @@ -1138,7 +1144,7 @@ def get_group(self, name: str, *, timeout: Timeout | None = None) -> Group | Non return None - def get_groups(self, name: list[str] | None = None, *, timeout: Timeout | None = None) -> dict[str, Group]: + def get_groups(self, name: list[str] | None = None, *, timeout: _Timeout | None = None) -> dict[str, Group]: payload = {} if name is not None: payload = {"group": name} From 614900a680b2d01f29d62071ac6ea3ae32eb43ba Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:15:20 +0800 Subject: [PATCH 05/20] improve timeout types --- transmission_rpc/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index b3e4d212..d75cbf53 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -208,9 +208,6 @@ def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> """ request_count = 0 - if isinstance(timeout, (int, float)): - timeout = Timeout(read=timeout, connect=timeout) - while True: if request_count >= 3: raise TransmissionError("too much request, try enable logger to see what happened") From 2703b55f0c4139183f4e54e09d8afa007d9156e9 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:15:42 +0800 Subject: [PATCH 06/20] improve timeout types --- transmission_rpc/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index d75cbf53..c5bdcd32 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -40,6 +40,7 @@ DEFAULT_TIMEOUT = 30.0 +# urllib3 may remove support for int/float in the future _Timeout = Union[Timeout, int, float] From a9c78ec4a3e82ec5892f0abe963d131108688e91 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:18:07 +0800 Subject: [PATCH 07/20] improve timeout types --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 97e48c75..55fea42f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,8 +9,8 @@ import pytest -from transmission_rpc import from_url, utils -from transmission_rpc.constants import DEFAULT_TIMEOUT, LOGGER +from transmission_rpc import DEFAULT_TIMEOUT, from_url, utils +from transmission_rpc.constants import LOGGER def assert_almost_eq(value: float, expected: float): From 23a4b17675cd4df96261b73a493cdb00afe3b9b5 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:21:00 +0800 Subject: [PATCH 08/20] fix --- poetry.lock | 204 +------------------------------------------ pyproject.toml | 1 - tests/test_client.py | 13 +-- 3 files changed, 2 insertions(+), 216 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3743e35d..37f6ef4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -510,105 +510,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - [[package]] name = "mypy" version = "1.10.0" @@ -1418,109 +1319,6 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [[package]] name = "zipp" version = "3.19.2" @@ -1539,4 +1337,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "5f8678f14687b1f238272011262a070a8de00fdfa9ca385d990edb11e295b844" +content-hash = "3288dfc5e3e15d2cef74e0b15c54ba290caf79d725a0905c8275e6f620afaebb" diff --git a/pyproject.toml b/pyproject.toml index c81b3dac..ce646e11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ sphinx-new-tab-link = { version = "^0.4.0", python = "^3.9" } sphinx-github-style = { version = "^1.2.2", python = "^3.9" } [tool.poetry.group.dev.dependencies] -yarl = "==1.9.4" # we have a example need this requests = "^2.23.0" # tests diff --git a/tests/test_client.py b/tests/test_client.py index 6e7c54b0..97172bc3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,7 +5,6 @@ from urllib.parse import urljoin import pytest -import yarl from typing_extensions import Literal from tests.util import ServerTooLowError, skip_on @@ -48,18 +47,8 @@ def test_client_parse_url(protocol: Literal["http", "https"], username, password port=port, path=path, ) - u = str( - yarl.URL.build( - scheme=protocol, - user=username, - password=password, - host=host, - port=port, - path=urljoin(path, "rpc"), - ) - ) - assert client._url == u # noqa: SLF001 + assert client._url == f'{protocol}://{host}:{port}{urljoin(path, "rpc")}' # noqa: SLF001 def hash_to_magnet(h): From 4994941183a97219bba86d700737075e21911263 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:23:45 +0800 Subject: [PATCH 09/20] fix --- transmission_rpc/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index c5bdcd32..8e1f042b 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -145,7 +145,7 @@ def __init__( self.__protocol_version: int = 17 # default 17 self.__semver_version = None - self.__http_client = urllib3.PoolManager(ca_certs=certifi.where(), timeout=self.timeout) + self.__http_client = urllib3.PoolManager(ca_certs=certifi.where(), timeout=self.timeout, retries=False) self.get_session(arguments=["rpc-version", "rpc-version-semver", "version"]) self.__torrent_get_arguments = get_torrent_arguments(self.__protocol_version) From 6e32df838a41ab2d199b09bcf81705556392e359 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:33:33 +0800 Subject: [PATCH 10/20] check connection before test --- tests/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 12249617..67dacde0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,9 @@ +# ruff: noqa: SIM117 +import contextlib import os import secrets +import socket +import time import pytest @@ -12,6 +16,17 @@ PASSWORD = os.getenv("TR_PASSWORD", "password") +def pytest_configure(): + start = time.time() + while True: + with contextlib.suppress(ConnectionError): + with socket.create_connection((HOST, PORT), timeout=5): + break + + if time.time() - start > 30: + print() + + @pytest.fixture() def tr_client(): LOGGER.setLevel("INFO") From a3b1cb3f8b428b2ae90a9fdcb79ced8379ebd40c Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:35:17 +0800 Subject: [PATCH 11/20] fix --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cdebe141..a0d424f2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: max-parallel: 2 + fail-fast: false matrix: transmission: ["version-3.00-r8", "4.0.5"] python: ["3.8", "3.9", "3.10", "3.11", "3.12"] From a4459a26ded0b789796ffd593e7532270ccca8bc Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:39:16 +0800 Subject: [PATCH 12/20] fix --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 67dacde0..9b10a746 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,11 +20,11 @@ def pytest_configure(): start = time.time() while True: with contextlib.suppress(ConnectionError): - with socket.create_connection((HOST, PORT), timeout=5): + with socket.create_connection((HOST, PORT), timeout=3): break if time.time() - start > 30: - print() + raise ConnectionError("timeout trying to connect to transmission-daemon, is transmission daemon started?") @pytest.fixture() From b28faf4a91d240dd87b7bb0ae0d047f59f28afc2 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:41:37 +0800 Subject: [PATCH 13/20] fix --- transmission_rpc/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 8e1f042b..a9194e35 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -209,6 +209,9 @@ def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> """ request_count = 0 + if timeout is None: + timeout = self.__query_timeout + while True: if request_count >= 3: raise TransmissionError("too much request, try enable logger to see what happened") From 0527e26d4697d1d2919b0c1b5ae402a971f6555f Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:45:26 +0800 Subject: [PATCH 14/20] fix --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 97172bc3..84abfe66 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -212,7 +212,7 @@ def test_real_torrent_get_files(tr_client: Client): ) def test_raise_unauthorized(status_code): m = mock.Mock(return_value=mock.Mock(status_code=status_code)) - with mock.patch("requests.Session.post", m), pytest.raises(TransmissionAuthError): + with mock.patch("urllib3.PoolManager.request", m), pytest.raises(TransmissionAuthError): Client() From 8add1b4b93ee6c86fc3219df420c5ff330c948a0 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:47:47 +0800 Subject: [PATCH 15/20] fix --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 84abfe66..96e7f17e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -211,7 +211,7 @@ def test_real_torrent_get_files(tr_client: Client): [401, 403], ) def test_raise_unauthorized(status_code): - m = mock.Mock(return_value=mock.Mock(status_code=status_code)) + m = mock.Mock(return_value=mock.Mock(status=status_code)) with mock.patch("urllib3.PoolManager.request", m), pytest.raises(TransmissionAuthError): Client() From 80560025c613036d4b36f1a2d97accd7b4fe4953 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 00:54:32 +0800 Subject: [PATCH 16/20] add ua fallback --- transmission_rpc/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index a9194e35..d3198759 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -27,9 +27,12 @@ from transmission_rpc.types import Group from transmission_rpc.utils import _try_read_torrent, get_torrent_arguments -__USER_AGENT__ = "transmission-rpc/{} (https://github.com/trim21/transmission-rpc)".format( - importlib.metadata.version("transmission_rpc") -) +try: + __version__ = importlib.metadata.version("transmission-rpc") +except ImportError: + __version__ = "develop" + +__USER_AGENT__ = f"transmission-rpc/{__version__} (https://github.com/trim21/transmission-rpc)" _hex_chars = frozenset(string.hexdigits.lower()) From e72efdf9a69446ca7db476d396cc4264562edfdc Mon Sep 17 00:00:00 2001 From: Trim21 Date: Mon, 10 Jun 2024 05:33:26 +0800 Subject: [PATCH 17/20] fix --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00927c9c..d3b533e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,6 @@ jobs: test: runs-on: ubuntu-22.04 strategy: - max-parallel: 2 fail-fast: false matrix: transmission: ["version-3.00-r8", "4.0.5"] From 73cfc4b69eb54c81f7fa517c05559eb7bc5284d6 Mon Sep 17 00:00:00 2001 From: Etienne Dechamps Date: Mon, 24 Jun 2024 10:06:02 +0100 Subject: [PATCH 18/20] feat(client): Add support for Unix sockets (#447) --- .github/workflows/ci.yaml | 27 ++++++++++++++++ tests/conftest.py | 10 ++++-- tests/test_client.py | 2 +- transmission_rpc/_unix_socket.py | 53 ++++++++++++++++++++++++++++++++ transmission_rpc/client.py | 23 ++++++++++---- 5 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 transmission_rpc/_unix_socket.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d3b533e7..ec1dac29 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,6 +53,33 @@ jobs: flags: "${{ matrix.python }}" token: ${{ secrets.CODECOV_TOKEN }} + test-unix-socket: + # At the time of writing ubuntu-latest is ubuntu-22.04. But we need + # an even later version because ubuntu-22.04 provides Transmission 3.0.0 but + # Unix socket support was added in Transmission 4.0.0. Use 24.04 which is + # currently in beta. + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - run: sudo apt-get install -y transmission-daemon + - run: mkdir -p $HOME/Downloads + - run: transmission-daemon --rpc-bind-address unix:/tmp/transmission.socket + - uses: actions/setup-python@v5 + with: + python-version: 3.9 # Oldest version available for ubuntu-24.04 + cache: pip + - run: pip install -e .[dev] + - run: coverage run -m pytest + env: + TR_PROTOCOL: 'http+unix' + TR_HOST: '/tmp/transmission.socket' + - uses: codecov/codecov-action@v4 + with: + flags: "unix-socket" + token: ${{ secrets.CODECOV_TOKEN }} + dist-files: runs-on: ubuntu-22.04 diff --git a/tests/conftest.py b/tests/conftest.py index 9b10a746..dac5c29d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from transmission_rpc import LOGGER from transmission_rpc.client import Client +PROTOCOL = os.getenv("TR_PROTOCOL", "http") HOST = os.getenv("TR_HOST", "127.0.0.1") PORT = int(os.getenv("TR_PORT", "9091")) USER = os.getenv("TR_USER", "admin") @@ -19,8 +20,11 @@ def pytest_configure(): start = time.time() while True: - with contextlib.suppress(ConnectionError): - with socket.create_connection((HOST, PORT), timeout=3): + with contextlib.suppress(ConnectionError, FileNotFoundError): + is_unix = PROTOCOL == "http+unix" + with socket.socket(socket.AF_UNIX if is_unix else socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(3) + sock.connect(HOST if is_unix else (HOST, PORT)) break if time.time() - start > 30: @@ -30,7 +34,7 @@ def pytest_configure(): @pytest.fixture() def tr_client(): LOGGER.setLevel("INFO") - with Client(host=HOST, port=PORT, username=USER, password=PASSWORD) as c: + with Client(protocol=PROTOCOL, host=HOST, port=PORT, username=USER, password=PASSWORD) as c: for torrent in c.get_torrents(): c.remove_torrent(torrent.id, delete_data=True) yield c diff --git a/tests/test_client.py b/tests/test_client.py index 96e7f17e..37567e48 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -212,7 +212,7 @@ def test_real_torrent_get_files(tr_client: Client): ) def test_raise_unauthorized(status_code): m = mock.Mock(return_value=mock.Mock(status=status_code)) - with mock.patch("urllib3.PoolManager.request", m), pytest.raises(TransmissionAuthError): + with mock.patch("urllib3.HTTPConnectionPool.request", m), pytest.raises(TransmissionAuthError): Client() diff --git a/transmission_rpc/_unix_socket.py b/transmission_rpc/_unix_socket.py new file mode 100644 index 00000000..adaf8b6f --- /dev/null +++ b/transmission_rpc/_unix_socket.py @@ -0,0 +1,53 @@ +# Inspired from: +# https://github.com/getsentry/sentry/blob/9d03adef66f63e29a5d95189447d02ba0b68c2af/src/sentry/net/http.py#L215-L244 +# See also: +# https://github.com/urllib3/urllib3/issues/1465 + +from __future__ import annotations + +import socket +from typing import Any + +from urllib3.connection import HTTPConnection +from urllib3.connectionpool import HTTPConnectionPool +from urllib3.util.connection import _TYPE_SOCKET_OPTIONS +from urllib3.util.timeout import _DEFAULT_TIMEOUT + + +class UnixHTTPConnection(HTTPConnection): + def __init__( + self, + host: str, + *, + # The default socket options include `TCP_NODELAY` which won't work here. + socket_options: None | _TYPE_SOCKET_OPTIONS = None, + **kwargs: Any, + ): + self.socket_path = host + # We're using the `host` as the socket path, but + # urllib3 uses this host as the Host header by default. + # If we send along the socket path as a Host header, this is + # never what you want and would typically be malformed value. + # So we fake this by sending along `localhost` by default as + # other libraries do. + super().__init__(host="localhost", socket_options=socket_options, **kwargs) + + def _new_conn(self) -> socket.socket: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + socket_options = self.socket_options + if socket_options is not None: + for opt in socket_options: + sock.setsockopt(*opt) + + if self.timeout is not _DEFAULT_TIMEOUT: # type: ignore + sock.settimeout(self.timeout) + sock.connect(self.socket_path) + return sock + + +class UnixHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = UnixHTTPConnection + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host})" diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index d3198759..8c2e3397 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -15,6 +15,7 @@ from urllib3 import Timeout from urllib3.util import make_headers +from transmission_rpc._unix_socket import UnixHTTPConnectionPool from transmission_rpc.constants import LOGGER, RpcMethod from transmission_rpc.error import ( TransmissionAuthError, @@ -96,7 +97,7 @@ class Client: def __init__( self, *, - protocol: Literal["http", "https"] = "http", + protocol: Literal["http", "https", "http+unix"] = "http", username: str | None = None, password: str | None = None, host: str = "127.0.0.1", @@ -116,6 +117,9 @@ def __init__( path: rpc request target path, default ``/transmission/rpc`` timeout: logger: + + To connect to a Unix socket, pass "http+unix" as `protocol` and the path to + the socket as `host`. """ if isinstance(logger, logging.Logger): self.logger = logger @@ -138,8 +142,10 @@ def __init__( if path == "/transmission/": path = "/transmission/rpc" - url = f"{protocol}://{host}:{port}{path}" + url_host = "localhost" if protocol == "http+unix" else host + url = f"{protocol}://{url_host}:{port}{path}" self._url = str(url) + self._path = path self.__raw_session: dict[str, Any] = {} self.__session_id = "0" @@ -148,7 +154,12 @@ def __init__( self.__protocol_version: int = 17 # default 17 self.__semver_version = None - self.__http_client = urllib3.PoolManager(ca_certs=certifi.where(), timeout=self.timeout, retries=False) + common_args: dict[str, Any] = {"host": host, "timeout": self.timeout, "retries": False} + self.__http_client = { + "http": urllib3.HTTPConnectionPool(port=port, **common_args), + "https": urllib3.HTTPSConnectionPool(port=port, ca_certs=certifi.where(), **common_args), + "http+unix": UnixHTTPConnectionPool(**common_args), + }[protocol] self.get_session(arguments=["rpc-version", "rpc-version-semver", "version"]) self.__torrent_get_arguments = get_torrent_arguments(self.__protocol_version) @@ -220,13 +231,13 @@ def _http_query(self, query: dict[str, Any], timeout: _Timeout | None = None) -> raise TransmissionError("too much request, try enable logger to see what happened") headers = self.__get_headers() - self.logger.debug({"url": self._url, "headers": headers, "data": query, "timeout": timeout}) + self.logger.debug({"path": self._path, "headers": headers, "data": query, "timeout": timeout}) request_count += 1 try: r = self.__http_client.request( "POST", - url=self._url, + url=self._path, headers=headers, json=query, timeout=timeout, @@ -1166,7 +1177,7 @@ def __exit__( exc_val: BaseException | None, exc_tb: types.TracebackType | None, ) -> None: - self.__http_client.clear() + self.__http_client.close() T = TypeVar("T") From f9608bb1fcb5bc06c40a8ae996e8d25f89cad326 Mon Sep 17 00:00:00 2001 From: Etienne Dechamps Date: Mon, 29 Jul 2024 11:47:57 +0200 Subject: [PATCH 19/20] feat: Make it possible to use an infinite timeout --- transmission_rpc/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 8c2e3397..7c088e11 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -92,7 +92,7 @@ def _parse_torrent_ids(args: Any) -> str | list[str | int]: class Client: - __query_timeout: Timeout + __query_timeout: Timeout | None def __init__( self, @@ -103,7 +103,7 @@ def __init__( host: str = "127.0.0.1", port: int = 9091, path: str = "/transmission/rpc", - timeout: float | Timeout = DEFAULT_TIMEOUT, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, logger: logging.Logger = LOGGER, ): """ @@ -129,7 +129,7 @@ def __init__( ) if isinstance(timeout, (int, float)): self.__query_timeout = Timeout(timeout) - elif isinstance(timeout, Timeout): + elif isinstance(timeout, Timeout) or timeout is None: self.__query_timeout = timeout else: raise TypeError(f"unsupported value {timeout!r}, only Timeout/float/int are supported") @@ -189,7 +189,7 @@ def server_version(self) -> str: return self.__server_version @property - def timeout(self) -> Timeout: + def timeout(self) -> Timeout | None: """ Get current timeout for HTTP queries. """ From 9b60793bcf9b65082adecb5d8f0a97d3ee179a2d Mon Sep 17 00:00:00 2001 From: Etienne Dechamps Date: Sat, 31 Aug 2024 10:52:06 +0100 Subject: [PATCH 20/20] feat(client): support http+unix URLs (#473) --- tests/test_utils.py | 8 ++++++++ transmission_rpc/__init__.py | 9 ++++++++- transmission_rpc/client.py | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 55fea42f..5f21f0e4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -106,6 +106,14 @@ def test_format_timedelta(delta, expected): "port": 443, "path": "/", }, + "http+unix://%2Fvar%2Frun%2Ftransmission.sock/transmission/rpc": { + "protocol": "http+unix", + "username": None, + "password": None, + "host": "/var/run/transmission.sock", + "port": None, + "path": "/transmission/rpc", + }, }.items(), ) def test_from_url(url: str, kwargs: dict[str, Any]): diff --git a/transmission_rpc/__init__.py b/transmission_rpc/__init__.py index 7300ac95..ff4c83a6 100644 --- a/transmission_rpc/__init__.py +++ b/transmission_rpc/__init__.py @@ -50,6 +50,7 @@ def from_url( from_url("https://127.0.0.1/transmission/rpc") # https://127.0.0.1:443/transmission/rpc from_url("http://127.0.0.1") # http://127.0.0.1:80/transmission/rpc from_url("http://127.0.0.1/") # http://127.0.0.1:80/ + from_url("http+unix://%2Fvar%2Frun%2Ftransmission.sock/transmission/rpc") # /transmission/rpc on /var/run/transmission.sock Unix socket Warnings: you can't ignore scheme, ``127.0.0.1:9091`` is not valid url, please use ``http://127.0.0.1:9091`` @@ -61,10 +62,16 @@ def from_url( u = urllib.parse.urlparse(url) protocol = u.scheme + host = u.hostname + default_port = None if protocol == "http": default_port = 80 elif protocol == "https": default_port = 443 + elif protocol == "http+unix": + if host is None: + raise ValueError("http+unix URL is missing Unix socket path") + host = urllib.parse.unquote(host, errors="strict") else: raise ValueError(f"unknown url scheme {u.scheme}") @@ -72,7 +79,7 @@ def from_url( protocol=protocol, # type: ignore username=u.username, password=u.password, - host=u.hostname or "127.0.0.1", + host=host or "127.0.0.1", port=u.port or default_port, path=u.path or "/transmission/rpc", timeout=timeout, diff --git a/transmission_rpc/client.py b/transmission_rpc/client.py index 7c088e11..734e17ba 100644 --- a/transmission_rpc/client.py +++ b/transmission_rpc/client.py @@ -101,7 +101,7 @@ def __init__( username: str | None = None, password: str | None = None, host: str = "127.0.0.1", - port: int = 9091, + port: int | None = 9091, path: str = "/transmission/rpc", timeout: float | Timeout | None = DEFAULT_TIMEOUT, logger: logging.Logger = LOGGER, @@ -143,7 +143,7 @@ def __init__( path = "/transmission/rpc" url_host = "localhost" if protocol == "http+unix" else host - url = f"{protocol}://{url_host}:{port}{path}" + url = f"{protocol}://{url_host}{'' if port is None else f':{port}'}{path}" self._url = str(url) self._path = path