Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: remove requests #443

Merged
merged 25 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
test:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
transmission: ["version-3.00-r8", "4.0.5"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
Expand Down Expand Up @@ -52,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-24.04

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ classifiers = [
]

dependencies = [
'requests~=2.23',
'urllib3~=2.2',
'certifi>=2017.4.17',
'typing-extensions>=4.5.0',
]

Expand All @@ -46,7 +47,6 @@ dev = [
'coverage==7.6.1',
# types
'mypy==1.13.0',
'types-requests==2.32.0.20241016',
# docs
'sphinx>=8,<=8.1.3; python_version >= "3.10"',
'furo==2024.8.6; python_version >= "3.10"',
Expand Down Expand Up @@ -149,4 +149,5 @@ ignore = [
'PLR0915',
'PLR2004',
'PGH003',
'TCH002',
]
21 changes: 20 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
# ruff: noqa: SIM117
import contextlib
import os
import secrets
import socket
import time

import pytest

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")
PASSWORD = os.getenv("TR_PASSWORD", "password")


def pytest_configure():
start = time.time()
while True:
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:
raise ConnectionError("timeout trying to connect to transmission-daemon, is transmission daemon started?")

Check warning on line 31 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L30-L31

Added lines #L30 - L31 were not covered by tests


@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
Expand Down
17 changes: 3 additions & 14 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -222,8 +211,8 @@ 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))
with mock.patch("requests.Session.post", m), pytest.raises(TransmissionAuthError):
m = mock.Mock(return_value=mock.Mock(status=status_code))
with mock.patch("urllib3.HTTPConnectionPool.request", m), pytest.raises(TransmissionAuthError):
Client()


Expand Down
12 changes: 10 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]):
Expand Down
13 changes: 10 additions & 3 deletions transmission_rpc/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -50,6 +50,7 @@
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``
Expand All @@ -61,18 +62,24 @@
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")

Check warning on line 73 in transmission_rpc/__init__.py

View check run for this annotation

Codecov / codecov/patch

transmission_rpc/__init__.py#L73

Added line #L73 was not covered by tests
host = urllib.parse.unquote(host, errors="strict")
else:
raise ValueError(f"unknown url scheme {u.scheme}")

return Client(
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,
Expand Down
53 changes: 53 additions & 0 deletions transmission_rpc/_unix_socket.py
Original file line number Diff line number Diff line change
@@ -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)

Check warning on line 41 in transmission_rpc/_unix_socket.py

View check run for this annotation

Codecov / codecov/patch

transmission_rpc/_unix_socket.py#L40-L41

Added lines #L40 - L41 were not covered by tests

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})"

Check warning on line 53 in transmission_rpc/_unix_socket.py

View check run for this annotation

Codecov / codecov/patch

transmission_rpc/_unix_socket.py#L53

Added line #L53 was not covered by tests
Loading