Skip to content

Commit 86bdd2b

Browse files
lesnik512claude
andcommitted
feat(story-2.4): AsyncClient.__init__ accepts auth= and tracks _user_middleware/_auth
Adds the `auth: AuthValue = None` keyword param. Coerces via _normalize_auth and appends the resulting middleware (if any) at the END of the user-supplied middleware list — so it runs just before the transport. Introduces two new private attrs on AsyncClient: - _user_middleware: the user's tuple, EXCLUDING the auth middleware - _auth: the raw AuthValue (so with_options can recompose) ClientConfig.middleware continues to hold the COMPOSED list (what actually runs), keeping the existing wiring contract. Three new construction tests verify: no auth → empty composed; string auth → 1-element composed; user middleware + auth → 3-element composed with user entries at positions 0,1 and auth at position 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 178890f commit 86bdd2b

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

‎src/httpware/client.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import typing
66
from collections.abc import Mapping, Sequence
77

8+
from httpware._internal.auth import AuthValue, _normalize_auth
89
from httpware._internal.chain import compose
910
from httpware.config import ClientConfig, Limits, Timeout
1011
from httpware.decoders import ResponseDecoder
@@ -52,6 +53,8 @@ class AsyncClient:
5253
_transport: Transport
5354
_dispatch: Next
5455
_owns_transport: bool
56+
_user_middleware: tuple[Middleware, ...]
57+
_auth: AuthValue
5558

5659
def __init__(
5760
self,
@@ -64,12 +67,23 @@ def __init__(
6467
transport: Transport | None = None,
6568
decoder: ResponseDecoder | None = None,
6669
middleware: Sequence[Middleware] | None = None,
70+
auth: AuthValue = None,
6771
) -> None:
6872
normalized_timeout = _normalize_timeout(timeout)
6973
resolved_limits = limits or Limits()
70-
resolved_transport: Transport = transport or Httpx2Transport(limits=resolved_limits, timeout=normalized_timeout)
74+
resolved_transport: Transport = transport or Httpx2Transport(
75+
limits=resolved_limits, timeout=normalized_timeout
76+
)
7177
resolved_decoder = decoder or PydanticDecoder()
72-
resolved_middleware = tuple(middleware) if middleware is not None else ()
78+
resolved_user_middleware: tuple[Middleware, ...] = (
79+
tuple(middleware) if middleware is not None else ()
80+
)
81+
resolved_auth_middleware = _normalize_auth(auth)
82+
composed_middleware: tuple[Middleware, ...] = (
83+
resolved_user_middleware
84+
if resolved_auth_middleware is None
85+
else (*resolved_user_middleware, resolved_auth_middleware)
86+
)
7387

7488
self._config = ClientConfig(
7589
base_url=base_url,
@@ -78,11 +92,13 @@ def __init__(
7892
timeout=normalized_timeout,
7993
limits=resolved_limits,
8094
decoder=resolved_decoder,
81-
middleware=resolved_middleware,
95+
middleware=composed_middleware,
8296
)
8397
self._transport = resolved_transport
84-
self._dispatch = compose(resolved_middleware, resolved_transport)
98+
self._dispatch = compose(composed_middleware, resolved_transport)
8599
self._owns_transport = True
100+
self._user_middleware = resolved_user_middleware
101+
self._auth = auth
86102

87103
@classmethod
88104
def from_url(cls, base_url: str, **kwargs: object) -> "AsyncClient":

‎tests/test_client_construction.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,37 @@ def test_construction_does_not_create_httpx2_client() -> None:
7676
# Httpx2Transport stores `_client` lazily; until first call, _client is None.
7777
# The attribute is private; we check it via getattr to keep the test resilient.
7878
assert getattr(client._transport, "_client", "missing") is None
79+
80+
81+
def test_init_no_auth_means_no_auth_middleware() -> None:
82+
transport = RecordedTransport()
83+
client = AsyncClient(transport=transport)
84+
assert client._config.middleware == ()
85+
assert client._auth is None
86+
assert client._user_middleware == ()
87+
88+
89+
def test_init_with_string_auth_appends_bearer_middleware() -> None:
90+
transport = RecordedTransport()
91+
client = AsyncClient(transport=transport, auth="tok")
92+
assert len(client._config.middleware) == 1
93+
assert isinstance(client._config.middleware[0], Middleware)
94+
assert client._auth == "tok"
95+
assert client._user_middleware == ()
96+
97+
98+
def test_init_with_user_middleware_plus_auth() -> None:
99+
class _M:
100+
async def __call__(self, request, next) -> Response: # noqa: A002, ANN001
101+
return await next(request)
102+
103+
m1 = _M()
104+
m2 = _M()
105+
transport = RecordedTransport()
106+
client = AsyncClient(transport=transport, middleware=[m1, m2], auth="tok")
107+
_expected_len = 3
108+
assert len(client._config.middleware) == _expected_len
109+
assert client._config.middleware[0] is m1
110+
assert client._config.middleware[1] is m2
111+
# The third entry is the auth middleware; identity-test that user_middleware excludes it.
112+
assert client._user_middleware == (m1, m2)

0 commit comments

Comments
 (0)