Skip to content
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
8 changes: 8 additions & 0 deletions lib/dl_httpx/dl_httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from .auth_providers import (
AuthProviderProtocol,
NoAuthProvider,
OauthAuthProvider,
)
from .client import (
HttpStatusHttpxClientException,
HttpxAsyncClient,
Expand Down Expand Up @@ -38,4 +43,7 @@
"NoRetriesHttpxClientException",
"TestingHttpxClient",
"serialize_datetime",
"OauthAuthProvider",
"NoAuthProvider",
"AuthProviderProtocol",
]
40 changes: 40 additions & 0 deletions lib/dl_httpx/dl_httpx/auth_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import abc
from typing import Protocol

import attrs


class AuthProviderProtocol(Protocol):
@abc.abstractmethod
def get_headers(self) -> dict[str, str]:
...

@abc.abstractmethod
def get_cookies(self) -> dict[str, str]:
...


class NoAuthProvider(AuthProviderProtocol):
def get_headers(self) -> dict[str, str]:
return {}

def get_cookies(self) -> dict[str, str]:
return {}


@attrs.define(kw_only=True)
class OauthAuthProvider(AuthProviderProtocol):
token: str

def get_headers(self) -> dict[str, str]:
return {"Authorization": f"OAuth {self.token}"}

def get_cookies(self) -> dict[str, str]:
return {}


__all__ = [
"AuthProviderProtocol",
"NoAuthProvider",
"OauthAuthProvider",
]
6 changes: 6 additions & 0 deletions lib/dl_httpx/dl_httpx/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import typing_extensions

import dl_configs
import dl_httpx.auth_providers as auth_providers
import dl_retrier


Expand Down Expand Up @@ -69,6 +70,7 @@ class HttpxClientSettings:
retry_policy_factory: dl_retrier.RetryPolicyFactorySettings = attrs.field(
factory=dl_retrier.RetryPolicyFactorySettings
)
auth_provider: auth_providers.AuthProviderProtocol = attrs.field(factory=auth_providers.NoAuthProvider)


@attrs.define(kw_only=True, auto_attribs=True, frozen=True)
Expand All @@ -77,6 +79,7 @@ class HttpxBaseClient(Generic[THttpxClient], abc.ABC):
_base_cookies: dict[str, str]
_base_headers: dict[str, str]
_retry_policy_factory: dl_retrier.RetryPolicyFactory
_auth_provider: auth_providers.AuthProviderProtocol

_base_client: THttpxClient

Expand All @@ -89,6 +92,7 @@ def from_settings(cls, settings: HttpxClientSettings) -> typing_extensions.Self:
retry_policy_factory=dl_retrier.RetryPolicyFactory.from_settings(
settings.retry_policy_factory,
),
auth_provider=settings.auth_provider,
base_client=cls._get_client(
ssl_context=settings.ssl_context,
),
Expand All @@ -104,12 +108,14 @@ def _prepare_url(self, url: str) -> str:

def _prepare_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
result = self._base_headers.copy()
result.update(self._auth_provider.get_headers())
if headers:
result.update(headers)
return result

def _prepare_cookies(self, cookies: dict[str, str] | None = None) -> dict[str, str]:
result = self._base_cookies.copy()
result.update(self._auth_provider.get_cookies())
if cookies:
result.update(cookies)
return result
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import dl_httpx


def test_no_auth_provider() -> None:
auth_provider = dl_httpx.NoAuthProvider()
assert auth_provider.get_headers() == {}
assert auth_provider.get_cookies() == {}
7 changes: 7 additions & 0 deletions lib/dl_httpx/dl_httpx_tests/unit/auth_providers/test_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import dl_httpx


def test_oauth_auth_provider() -> None:
auth_provider = dl_httpx.OauthAuthProvider(token="test-token")
assert auth_provider.get_headers() == {"Authorization": "OAuth test-token"}
assert auth_provider.get_cookies() == {}
42 changes: 42 additions & 0 deletions lib/dl_httpx/dl_httpx_tests/unit/clients/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
import ssl

import pytest
import pytest_mock

import dl_httpx
import dl_retrier
import dl_testing


@pytest.fixture(name="ssl_context")
def fixture_ssl_context() -> ssl.SSLContext:
return dl_testing.get_default_ssl_context()


@pytest.fixture(name="mock_retry")
def fixture_mock_retry() -> dl_retrier.Retry:
return dl_retrier.Retry(
request_timeout=10,
connect_timeout=30,
sleep_before_seconds=0,
)


@pytest.fixture(name="mock_retry_policy")
def fixture_mock_retry_policy(
mocker: pytest_mock.MockerFixture,
mock_retry: dl_retrier.Retry,
) -> dl_retrier.RetryPolicy:
retry_policy = mocker.Mock(spec=dl_retrier.RetryPolicy)
retry_policy.iter_retries.return_value = iter([mock_retry, mock_retry, mock_retry])
retry_policy.can_retry_error.return_value = False
return retry_policy


@pytest.fixture(name="mock_retry_policy_factory")
def fixture_mock_retry_policy_factory(
mocker: pytest_mock.MockerFixture,
mock_retry_policy: dl_retrier.RetryPolicy,
) -> dl_retrier.RetryPolicyFactory:
retry_policy_factory = mocker.MagicMock(spec=dl_retrier.RetryPolicyFactory)
retry_policy_factory.get_policy.return_value = mock_retry_policy

return retry_policy_factory


@pytest.fixture(name="mock_auth_provider")
def fixture_mock_auth_provider(
mocker: pytest_mock.MockerFixture,
) -> dl_httpx.AuthProviderProtocol:
auth_provider = mocker.MagicMock(spec=dl_httpx.AuthProviderProtocol)
return auth_provider
39 changes: 39 additions & 0 deletions lib/dl_httpx/dl_httpx_tests/unit/clients/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import unittest.mock

import httpx
import mock
import pytest
import pytest_asyncio
import pytest_mock
Expand Down Expand Up @@ -215,6 +216,7 @@ async def fixture_client_with_mocks(
base_headers={},
retry_policy_factory=mock_retry_policy_factory,
base_client=httpx.AsyncClient(base_url="https://example.com"),
auth_provider=dl_httpx.NoAuthProvider(),
) as client:
yield client

Expand Down Expand Up @@ -320,3 +322,40 @@ async def test_retry_no_retries(
pass

assert mock_route.call_count == 0


@pytest.mark.asyncio
async def test_auth_provider(
respx_mock: respx.MockRouter,
ssl_context: ssl.SSLContext,
mock_auth_provider: mock.AsyncMock,
) -> None:
mock_auth_provider.get_headers.return_value = {
"test-header-key1": "test-header-value1",
"test-header-key2": "test-header-value2",
}
mock_auth_provider.get_cookies.return_value = {
"test-cookie-key1": "test-cookie-value1",
"test-cookie-key2": "test-cookie-value2",
}

mock_route = respx_mock.get("https://example.com/api/data").respond(status_code=200)
async with dl_httpx.HttpxAsyncClient.from_settings(
dl_httpx.HttpxClientSettings(
base_url="https://example.com",
ssl_context=ssl_context,
auth_provider=mock_auth_provider,
),
) as client:
request = client.prepare_request("GET", "/api/data")
async with client.send(request) as response:
assert response.status_code == 200

assert mock_route.call_count == 1
request = mock_route.calls.last.request
assert request.headers == {
"host": "example.com",
"test-header-key1": "test-header-value1",
"test-header-key2": "test-header-value2",
"cookie": "test-cookie-key1=test-cookie-value1; test-cookie-key2=test-cookie-value2",
}
38 changes: 38 additions & 0 deletions lib/dl_httpx/dl_httpx_tests/unit/clients/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import unittest.mock

import httpx
import mock
import pytest
import pytest_mock
import respx
Expand Down Expand Up @@ -207,6 +208,7 @@ def fixture_client_with_mocks(
base_headers={},
retry_policy_factory=mock_retry_policy_factory,
base_client=httpx.Client(base_url="https://example.com"),
auth_provider=dl_httpx.NoAuthProvider(),
) as client:
yield client

Expand Down Expand Up @@ -308,3 +310,39 @@ def test_retry_no_retries(
pass

assert mock_route.call_count == 0


def test_auth_provider(
respx_mock: respx.MockRouter,
ssl_context: ssl.SSLContext,
mock_auth_provider: mock.Mock,
) -> None:
mock_auth_provider.get_headers.return_value = {
"test-header-key1": "test-header-value1",
"test-header-key2": "test-header-value2",
}
mock_auth_provider.get_cookies.return_value = {
"test-cookie-key1": "test-cookie-value1",
"test-cookie-key2": "test-cookie-value2",
}

mock_route = respx_mock.get("https://example.com/api/data").respond(status_code=200)
with dl_httpx.HttpxSyncClient.from_settings(
dl_httpx.HttpxClientSettings(
base_url="https://example.com",
ssl_context=ssl_context,
auth_provider=mock_auth_provider,
),
) as client:
request = client.prepare_request("GET", "/api/data")
with client.send(request) as response:
assert response.status_code == 200

assert mock_route.call_count == 1
request = mock_route.calls.last.request
assert request.headers == {
"host": "example.com",
"test-header-key1": "test-header-value1",
"test-header-key2": "test-header-value2",
"cookie": "test-cookie-key1=test-cookie-value1; test-cookie-key2=test-cookie-value2",
}
35 changes: 0 additions & 35 deletions lib/dl_httpx/dl_httpx_tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,35 +0,0 @@
import pytest
import pytest_mock

import dl_retrier


@pytest.fixture(name="mock_retry")
def fixture_mock_retry() -> dl_retrier.Retry:
return dl_retrier.Retry(
request_timeout=10,
connect_timeout=30,
sleep_before_seconds=0,
)


@pytest.fixture(name="mock_retry_policy")
def fixture_mock_retry_policy(
mocker: pytest_mock.MockerFixture,
mock_retry: dl_retrier.Retry,
) -> dl_retrier.RetryPolicy:
retry_policy = mocker.Mock(spec=dl_retrier.RetryPolicy)
retry_policy.iter_retries.return_value = iter([mock_retry, mock_retry, mock_retry])
retry_policy.can_retry_error.return_value = False
return retry_policy


@pytest.fixture(name="mock_retry_policy_factory")
def fixture_mock_retry_policy_factory(
mocker: pytest_mock.MockerFixture,
mock_retry_policy: dl_retrier.RetryPolicy,
) -> dl_retrier.RetryPolicyFactory:
retry_policy_factory = mocker.MagicMock(spec=dl_retrier.RetryPolicyFactory)
retry_policy_factory.get_policy.return_value = mock_retry_policy

return retry_policy_factory
1 change: 1 addition & 0 deletions lib/dl_httpx/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ python = ">=3.10, <3.13"
typing-extensions = "*"

[tool.poetry.group.tests.dependencies]
mock = "*"
pytest = "*"
pytest-asyncio = "*"
pytest-mock = "*"
Expand Down
Loading