cloudru-qa-http-client 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ # Pitfalls
2
+
3
+ Non-obvious behaviour that costs time when you hit it unprepared.
4
+
5
+ ## Options run before the httpx client exists
6
+
7
+ Order inside `__init__`: `BaseHttpClient.__init__` creates typed internal configuration and runs
8
+ the options; then `HttpClient.__init__` creates the httpx transport from that configuration.
9
+
10
+ Two consequences:
11
+
12
+ 1. An option that touches `client.client` raises `AttributeError` — there is no transport yet.
13
+ 2. Setting `client.timeout`, `auth`, `verify` or `url_suffix` after construction raises `RuntimeError`.
14
+ Set them through constructor options. For transport-level changes supported by httpx, use its
15
+ public API directly, for example `client.client.timeout`, or rebuild the client.
16
+
17
+ ## Decorators are frozen once
18
+
19
+ Decorators are composed in `__init__`. Register them through constructor options with
20
+ `add_send_decorator` / `add_stream_decorator` or the built-in option factories. Late registration
21
+ raises `RuntimeError`.
22
+
23
+ Composition order is a plain loop:
24
+
25
+ ```python
26
+ for decorator in decorators:
27
+ func = decorator(func)
28
+ ```
29
+
30
+ so the last decorator added ends up outermost and runs first.
31
+
32
+ ## `endpoint()` only accepts relative paths
33
+
34
+ An absolute URL or a `..` segment raises `ValueError` instead of silently escaping the client's base URL. Build cross-host calls with a separate client, not by passing a full URL to `endpoint()`.
35
+
36
+ ## A decorator that sends twice runs the hooks twice
37
+
38
+ Event hooks fire inside `httpx.Client.send`, so a decorator that calls `func()` twice runs every
39
+ request hook twice. The built-in ID hooks preserve existing `X-Request-ID` and `Traceparent` values;
40
+ reusing one request keeps its identifiers stable across retries. A newly built request gets new
41
+ identifiers unless the caller provided them. Other hooks, including logging, still run per attempt.
42
+
43
+ ## Streaming: the logged response body is always a placeholder
44
+
45
+ `stream_request` marks the request with `request.extensions["is_streaming"] = True`. The logging response hook sees the marker and, while the body is unread, writes `Body: <streamed content>`. This is by design — reading the body inside the hook would defeat the purpose of streaming.
46
+
47
+ ## `HttpClient` and `AsyncHttpClient` are declared abstract yet instantiable
48
+
49
+ Both carry `metaclass=ABCMeta`, but the only abstract member, `BaseHttpClient.client`, is implemented in each. So `HttpClient("https://...")` works — the library's own tests rely on it. Subclassing is still the preferred approach, but direct construction is not a mistake.
50
+
51
+ ## False alarms when reading the code
52
+
53
+ - `logutils.create_request_log` writes `r.extensions["trace_id"]`, meaning the logging hook mutates the request. Keep that in mind when comparing `extensions` in tests.
@@ -0,0 +1,16 @@
1
+ from http_client import options, utils
2
+
3
+ from .client import AsyncHttpClient, BaseHttpClient, ClientOption, HttpClient
4
+ from .config import configure_logging
5
+ from .types import EventHook
6
+
7
+ __all__ = [
8
+ "AsyncHttpClient",
9
+ "BaseHttpClient",
10
+ "ClientOption",
11
+ "EventHook",
12
+ "HttpClient",
13
+ "configure_logging",
14
+ "options",
15
+ "utils",
16
+ ]
http_client/client.py ADDED
@@ -0,0 +1,603 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
5
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager, asynccontextmanager, contextmanager
6
+ from typing import TYPE_CHECKING, Self, Unpack, cast
7
+ from urllib.parse import unquote, urljoin, urlsplit
8
+
9
+ if TYPE_CHECKING:
10
+ import ssl
11
+ from types import TracebackType
12
+
13
+ from httpx._types import (
14
+ AuthTypes,
15
+ TimeoutTypes,
16
+ URLTypes,
17
+ )
18
+
19
+ from .types import (
20
+ AsyncSendDecorator,
21
+ AsyncSendMethod,
22
+ AsyncStreamRequestDecorator,
23
+ AsyncStreamRequestMethod,
24
+ RequestHook,
25
+ ResponseHook,
26
+ SendDecorator,
27
+ StreamRequestDecorator,
28
+ SyncSendDecorator,
29
+ SyncSendMethod,
30
+ SyncStreamRequestDecorator,
31
+ SyncStreamRequestMethod,
32
+ )
33
+
34
+ import httpx
35
+ from httpx._client import USE_CLIENT_DEFAULT
36
+ from httpx._config import DEFAULT_MAX_REDIRECTS
37
+
38
+ from .config import HttpClientConfig
39
+ from .types import QueryOptions, RequestOptions, SendOptions, StreamOptions
40
+
41
+ ClientOption = Callable[["BaseHttpClient"], None]
42
+
43
+
44
+ def _reject_unknown_options(options: Mapping[str, object], allowed: frozenset[str]) -> None:
45
+ unknown = set(options).difference(allowed)
46
+ if unknown:
47
+ message = f"Unexpected request options: {', '.join(sorted(unknown))}"
48
+ raise TypeError(message)
49
+
50
+
51
+ def _mark_streaming(request: httpx.Request) -> None:
52
+ extensions = cast("dict[str, object]", request.extensions)
53
+ extensions["is_streaming"] = True
54
+
55
+
56
+ def _ensure_async_hook[T](hook: Callable[[T], Awaitable[None] | None]) -> Callable[[T], Awaitable[None]]:
57
+ """Wrap a synchronous event hook in an asynchronous wrapper if needed."""
58
+
59
+ async def async_hook(message: T) -> None:
60
+ result = hook(message)
61
+ if result is not None:
62
+ await result
63
+
64
+ return async_hook
65
+
66
+
67
+ def _resolve_auth(auth: AuthTypes | None, client_auth: AuthTypes | None) -> AuthTypes | None:
68
+ """Select request authentication.
69
+
70
+ Compare with None rather than checking truthiness: a valid Auth may have __bool__ == False.
71
+ """
72
+ return auth if auth is not None else client_auth
73
+
74
+
75
+ class BaseHttpClient(metaclass=ABCMeta):
76
+ """Base HTTP client class."""
77
+
78
+ DEFAULT_URL_SUFFIX = ""
79
+ DEFAULT_TIMEOUT = httpx.Timeout(60)
80
+
81
+ def __init__(
82
+ self,
83
+ base_url: URLTypes,
84
+ *options: ClientOption,
85
+ ) -> None:
86
+ """Apply options to the settings before creating the transport.
87
+
88
+ :param base_url: HTTP service address
89
+ :param options: Built-in and custom client options
90
+ """
91
+ self._config = HttpClientConfig(
92
+ base_url=base_url, timeout=self.DEFAULT_TIMEOUT, url_suffix=self.DEFAULT_URL_SUFFIX
93
+ )
94
+ self._configuration_open = True
95
+
96
+ for opt in options:
97
+ opt(self)
98
+ self._configuration_open = False
99
+
100
+ @property
101
+ def verify(self) -> ssl.SSLContext | str | bool:
102
+ """Return the transport certificate verification setting."""
103
+ return self._config.verify
104
+
105
+ @verify.setter
106
+ def verify(self, value: ssl.SSLContext | str | bool) -> None:
107
+ self._assert_configuring()
108
+ self._config.verify = value
109
+
110
+ @property
111
+ def auth(self) -> AuthTypes | None:
112
+ """Return the transport authentication setting."""
113
+ return self._config.auth
114
+
115
+ @auth.setter
116
+ def auth(self, value: AuthTypes | None) -> None:
117
+ self._assert_configuring()
118
+ self._config.auth = value
119
+
120
+ @property
121
+ def timeout(self) -> TimeoutTypes:
122
+ """Return the transport timeout."""
123
+ return self._config.timeout
124
+
125
+ @timeout.setter
126
+ def timeout(self, value: TimeoutTypes) -> None:
127
+ self._assert_configuring()
128
+ self._config.timeout = value
129
+
130
+ @property
131
+ def url_suffix(self) -> str:
132
+ """Return the prefix for relative request paths."""
133
+ return self._config.url_suffix
134
+
135
+ @url_suffix.setter
136
+ def url_suffix(self, value: str) -> None:
137
+ self._assert_configuring()
138
+ self._config.url_suffix = value
139
+
140
+ def add_request_hook(self, hook: RequestHook, *, prepend: bool = False) -> None:
141
+ """Register a request hook while applying options.
142
+
143
+ :param hook: HTTP request handler
144
+ :param prepend: Run the hook before previously registered handlers
145
+ :raises RuntimeError: Client initialization has already finished
146
+ """
147
+ self._assert_configuring()
148
+ if prepend:
149
+ self._config.request_hooks.insert(0, hook)
150
+ else:
151
+ self._config.request_hooks.append(hook)
152
+
153
+ def add_response_hook(self, hook: ResponseHook) -> None:
154
+ """Register a response hook while applying options.
155
+
156
+ :param hook: HTTP response handler
157
+ :raises RuntimeError: Client initialization has already finished
158
+ """
159
+ self._assert_configuring()
160
+ self._config.response_hooks.append(hook)
161
+
162
+ def add_send_decorator(self, decorator: SendDecorator) -> None:
163
+ """Register a request send decorator while applying options.
164
+
165
+ :param decorator: Synchronous or asynchronous send decorator
166
+ :raises RuntimeError: Client initialization has already finished
167
+ """
168
+ self._assert_configuring()
169
+ self._config.send_decorators.append(decorator)
170
+
171
+ def add_stream_decorator(self, decorator: StreamRequestDecorator) -> None:
172
+ """Register a streaming request decorator while applying options.
173
+
174
+ :param decorator: Synchronous or asynchronous context manager decorator
175
+ :raises RuntimeError: Client initialization has already finished
176
+ """
177
+ self._assert_configuring()
178
+ self._config.stream_decorators.append(decorator)
179
+
180
+ def enable_redirects(self, max_redirects: int = DEFAULT_MAX_REDIRECTS) -> None:
181
+ """Enable redirects while applying options.
182
+
183
+ :param max_redirects: Maximum number of redirects
184
+ :raises RuntimeError: Client initialization has already finished
185
+ """
186
+ self._assert_configuring()
187
+ self._config.follow_redirects = True
188
+ self._config.max_redirects = max_redirects
189
+
190
+ @property
191
+ def base_url(self) -> httpx.URL:
192
+ """Return the transport base URL."""
193
+ return self.client.base_url
194
+
195
+ @property
196
+ @abstractmethod
197
+ def client(self) -> httpx._client.BaseClient:
198
+ """Return the synchronous or asynchronous HTTP transport."""
199
+ ...
200
+
201
+ def endpoint(self, path: str) -> str:
202
+ """Build a URL using the suffix.
203
+
204
+ :param path: Relative endpoint path
205
+ :raises ValueError: If the path is absolute or traverses above the suffix
206
+ :returns: Path with the suffix applied
207
+
208
+ >>> assert "api/v1/endpoint" == self.endpoint("api/v1/endpoint")
209
+ >>> self.url_suffix = "/u-api/container-registry/v1"
210
+ >>> assert "/u-api/container-registry/v1/api/v1/endpoint" == self.endpoint("api/v1/endpoint")
211
+ """
212
+ parsed = urlsplit(path)
213
+ if parsed.scheme or parsed.netloc:
214
+ message = f"endpoint() принимает только относительный путь, получен абсолютный URL: {path!r}"
215
+ raise ValueError(message)
216
+ if ".." in unquote(parsed.path).split("/"):
217
+ message = f"endpoint() не допускает выход вверх по пути: {path!r}"
218
+ raise ValueError(message)
219
+
220
+ suffix = self.url_suffix
221
+ if suffix and not suffix.endswith("/"):
222
+ suffix += "/"
223
+
224
+ return urljoin(suffix, path.removeprefix("/"))
225
+
226
+ def _prepare_request(
227
+ self, method: str, url: URLTypes, options: RequestOptions
228
+ ) -> tuple[httpx.Request, StreamOptions]:
229
+ _reject_unknown_options(options, RequestOptions.__optional_keys__)
230
+ request = self.client.build_request(
231
+ method,
232
+ url,
233
+ content=options.get("content"),
234
+ data=options.get("data"),
235
+ files=options.get("files"),
236
+ json=options.get("json"),
237
+ params=options.get("params"),
238
+ headers=options.get("headers"),
239
+ cookies=options.get("cookies"),
240
+ timeout=options.get("timeout", USE_CLIENT_DEFAULT),
241
+ extensions=options.get("extensions"),
242
+ )
243
+ send_options: StreamOptions = {
244
+ "auth": options.get("auth"),
245
+ "follow_redirects": options.get("follow_redirects"),
246
+ "anonymous": options.get("anonymous", False),
247
+ }
248
+ return request, send_options
249
+
250
+ def _assert_configuring(self) -> None:
251
+ if not self._configuration_open:
252
+ message = "Register client configuration through constructor options"
253
+ raise RuntimeError(message)
254
+
255
+
256
+ class HttpClient(BaseHttpClient, metaclass=ABCMeta):
257
+ """HTTP client class."""
258
+
259
+ def __init__(
260
+ self,
261
+ base_url: URLTypes,
262
+ *options: ClientOption,
263
+ ) -> None:
264
+ """Create a synchronous HTTP client.
265
+
266
+ :param base_url: HTTP service address
267
+ :param options: Built-in and custom options
268
+ """
269
+ super().__init__(base_url, *options)
270
+ self._client = httpx.Client(
271
+ base_url=self._config.base_url,
272
+ auth=self.auth,
273
+ timeout=self.timeout,
274
+ verify=self.verify,
275
+ follow_redirects=self._config.follow_redirects,
276
+ max_redirects=self._config.max_redirects,
277
+ event_hooks={
278
+ "request": self._config.request_hooks,
279
+ "response": self._config.response_hooks,
280
+ },
281
+ )
282
+ self._decorated_send = self._apply_decorators(self._send)
283
+ self._decorated_stream_request = self._apply_stream_request_decorators(self._stream_request)
284
+
285
+ @property
286
+ def client(self) -> httpx.Client:
287
+ """Return the synchronous HTTP transport."""
288
+ return self._client
289
+
290
+ def __enter__(self) -> Self:
291
+ """Open the connection pool and return the client."""
292
+ self.client.__enter__()
293
+ return self
294
+
295
+ def __exit__(
296
+ self,
297
+ exc_type: type[BaseException] | None,
298
+ exc_val: BaseException | None,
299
+ exc_tb: TracebackType | None,
300
+ ) -> None:
301
+ """Close the connection pool when leaving the context.
302
+
303
+ :param exc_type: Type of the exception in the context
304
+ :param exc_val: Exception in the context
305
+ :param exc_tb: Exception traceback
306
+ """
307
+ self.client.__exit__(exc_type, exc_val, exc_tb)
308
+
309
+ def close(self) -> None:
310
+ """Close the connection pool."""
311
+ self.client.close()
312
+
313
+ def get(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
314
+ """Send a GET request."""
315
+ return self.request("GET", url, **options)
316
+
317
+ def post(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
318
+ """Send a POST request."""
319
+ return self.request("POST", url, **options)
320
+
321
+ def put(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
322
+ """Send a PUT request."""
323
+ return self.request("PUT", url, **options)
324
+
325
+ def patch(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
326
+ """Send a PATCH request."""
327
+ return self.request("PATCH", url, **options)
328
+
329
+ def delete(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
330
+ """Send a DELETE request."""
331
+ return self.request("DELETE", url, **options)
332
+
333
+ def head(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
334
+ """Send a HEAD request."""
335
+ return self.request("HEAD", url, **options)
336
+
337
+ def options(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
338
+ """Send an OPTIONS request."""
339
+ return self.request("OPTIONS", url, **options)
340
+
341
+ def request(self, method: str, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
342
+ """Send an HTTP request with an arbitrary method."""
343
+ request, send_options = self._prepare_request(method, url, options)
344
+ return self.send(request, **send_options)
345
+
346
+ def send(self, request: httpx.Request, **options: Unpack[SendOptions]) -> httpx.Response:
347
+ """Send an HTTP request."""
348
+ _reject_unknown_options(options, SendOptions.__optional_keys__)
349
+ if options.get("stream", False):
350
+ _mark_streaming(request)
351
+ return self._decorated_send(request, **options)
352
+
353
+ def _send(
354
+ self,
355
+ request: httpx.Request,
356
+ *,
357
+ stream: bool = False,
358
+ auth: AuthTypes | None = None,
359
+ follow_redirects: bool | None = None,
360
+ anonymous: bool = False,
361
+ ) -> httpx.Response:
362
+ follow = follow_redirects if follow_redirects is not None else self.client.follow_redirects
363
+
364
+ if anonymous:
365
+ return self.client.send(request, stream=stream, auth=None, follow_redirects=follow)
366
+ return self.client.send(
367
+ request,
368
+ stream=stream,
369
+ auth=_resolve_auth(auth, self.client.auth),
370
+ follow_redirects=follow,
371
+ )
372
+
373
+ def stream(
374
+ self, method: str, url: URLTypes, **options: Unpack[RequestOptions]
375
+ ) -> AbstractContextManager[httpx.Response]:
376
+ """Send a streaming HTTP request using the httpx contract."""
377
+ request, send_options = self._prepare_request(method, url, options)
378
+ return self.stream_request(request, **send_options)
379
+
380
+ def stream_request(
381
+ self, request: httpx.Request, **options: Unpack[StreamOptions]
382
+ ) -> AbstractContextManager[httpx.Response]:
383
+ """Send a streaming HTTP request.
384
+
385
+ Return a context manager for accessing the streaming response.
386
+
387
+ Usage example:
388
+ with client.stream_request(request) as response:
389
+ for chunk in response.iter_bytes():
390
+ process(chunk)
391
+ """
392
+ _reject_unknown_options(options, StreamOptions.__optional_keys__)
393
+ return self._decorated_stream_request(request, **options)
394
+
395
+ @contextmanager
396
+ def _stream_request(
397
+ self,
398
+ request: httpx.Request,
399
+ *,
400
+ anonymous: bool = False,
401
+ auth: AuthTypes | None = None,
402
+ follow_redirects: bool | None = None,
403
+ ) -> Iterator[httpx.Response]:
404
+ auth_to_use = None if anonymous else _resolve_auth(auth, self.client.auth)
405
+ follow = follow_redirects if follow_redirects is not None else self.client.follow_redirects
406
+
407
+ _mark_streaming(request)
408
+
409
+ # Send the supplied Request: rebuilding it with client.stream()
410
+ # broke streaming request bodies and passed a different object to hooks.
411
+ response = self.client.send(request, stream=True, auth=auth_to_use, follow_redirects=follow)
412
+ try:
413
+ yield response
414
+ finally:
415
+ response.close()
416
+
417
+ def _apply_decorators(self, func: SyncSendMethod) -> SyncSendMethod:
418
+ """Apply all decorators to the function."""
419
+ for decorator in self._config.send_decorators:
420
+ func = cast("SyncSendDecorator", decorator)(func)
421
+ return func
422
+
423
+ def _apply_stream_request_decorators(self, func: SyncStreamRequestMethod) -> SyncStreamRequestMethod:
424
+ """Apply all stream_request decorators to the function."""
425
+ for decorator in self._config.stream_decorators:
426
+ func = cast("SyncStreamRequestDecorator", decorator)(func)
427
+ return func
428
+
429
+
430
+ class AsyncHttpClient(BaseHttpClient, metaclass=ABCMeta):
431
+ """Asynchronous HTTP client."""
432
+
433
+ def __init__(
434
+ self,
435
+ base_url: URLTypes,
436
+ *options: ClientOption,
437
+ ) -> None:
438
+ """Create an asynchronous HTTP client.
439
+
440
+ :param base_url: HTTP service address
441
+ :param options: Built-in and custom options
442
+ """
443
+ super().__init__(base_url, *options)
444
+ self._config.request_hooks = [_ensure_async_hook(h) for h in self._config.request_hooks]
445
+ self._config.response_hooks = [_ensure_async_hook(h) for h in self._config.response_hooks]
446
+ self._client = httpx.AsyncClient(
447
+ base_url=self._config.base_url,
448
+ auth=self.auth,
449
+ timeout=self.timeout,
450
+ verify=self.verify,
451
+ follow_redirects=self._config.follow_redirects,
452
+ max_redirects=self._config.max_redirects,
453
+ event_hooks={
454
+ "request": self._config.request_hooks,
455
+ "response": self._config.response_hooks,
456
+ },
457
+ )
458
+ self._decorated_send = self._apply_decorators(self._send)
459
+ self._decorated_stream_request = self._apply_stream_request_decorators(self._stream_request)
460
+
461
+ @property
462
+ def client(self) -> httpx.AsyncClient:
463
+ """Return the asynchronous HTTP transport."""
464
+ return self._client
465
+
466
+ async def __aenter__(self) -> Self:
467
+ """Open the connection pool and return the client."""
468
+ await self.client.__aenter__()
469
+ return self
470
+
471
+ async def __aexit__(
472
+ self,
473
+ exc_type: type[BaseException] | None,
474
+ exc_val: BaseException | None,
475
+ exc_tb: TracebackType | None,
476
+ ) -> None:
477
+ """Close the connection pool when leaving the context.
478
+
479
+ :param exc_type: Type of the exception in the context
480
+ :param exc_val: Exception in the context
481
+ :param exc_tb: Exception traceback
482
+ """
483
+ await self.client.__aexit__(exc_type, exc_val, exc_tb)
484
+
485
+ async def aclose(self) -> None:
486
+ """Close the connection pool."""
487
+ await self.client.aclose()
488
+
489
+ async def get(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
490
+ """Send a GET request asynchronously."""
491
+ return await self.request("GET", url, **options)
492
+
493
+ async def post(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
494
+ """Send a POST request asynchronously."""
495
+ return await self.request("POST", url, **options)
496
+
497
+ async def put(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
498
+ """Send a PUT request asynchronously."""
499
+ return await self.request("PUT", url, **options)
500
+
501
+ async def patch(self, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
502
+ """Send a PATCH request asynchronously."""
503
+ return await self.request("PATCH", url, **options)
504
+
505
+ async def delete(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
506
+ """Send a DELETE request asynchronously."""
507
+ return await self.request("DELETE", url, **options)
508
+
509
+ async def head(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
510
+ """Send a HEAD request asynchronously."""
511
+ return await self.request("HEAD", url, **options)
512
+
513
+ async def options(self, url: URLTypes, **options: Unpack[QueryOptions]) -> httpx.Response:
514
+ """Send an OPTIONS request asynchronously."""
515
+ return await self.request("OPTIONS", url, **options)
516
+
517
+ async def request(self, method: str, url: URLTypes, **options: Unpack[RequestOptions]) -> httpx.Response:
518
+ """Send an HTTP request with an arbitrary method asynchronously."""
519
+ request, send_options = self._prepare_request(method, url, options)
520
+ return await self.send(request, **send_options)
521
+
522
+ async def send(self, request: httpx.Request, **options: Unpack[SendOptions]) -> httpx.Response:
523
+ """Send an HTTP request asynchronously."""
524
+ _reject_unknown_options(options, SendOptions.__optional_keys__)
525
+ if options.get("stream", False):
526
+ _mark_streaming(request)
527
+ return await self._decorated_send(request, **options)
528
+
529
+ async def _send(
530
+ self,
531
+ request: httpx.Request,
532
+ *,
533
+ stream: bool = False,
534
+ auth: AuthTypes | None = None,
535
+ follow_redirects: bool | None = None,
536
+ anonymous: bool = False,
537
+ ) -> httpx.Response:
538
+ follow = follow_redirects if follow_redirects is not None else self.client.follow_redirects
539
+
540
+ if anonymous:
541
+ return await self.client.send(request, stream=stream, auth=None, follow_redirects=follow)
542
+ return await self.client.send(
543
+ request,
544
+ stream=stream,
545
+ auth=_resolve_auth(auth, self.client.auth),
546
+ follow_redirects=follow,
547
+ )
548
+
549
+ def stream(
550
+ self, method: str, url: URLTypes, **options: Unpack[RequestOptions]
551
+ ) -> AbstractAsyncContextManager[httpx.Response]:
552
+ """Send a streaming HTTP request asynchronously using the httpx contract."""
553
+ request, send_options = self._prepare_request(method, url, options)
554
+ return self.stream_request(request, **send_options)
555
+
556
+ def stream_request(
557
+ self, request: httpx.Request, **options: Unpack[StreamOptions]
558
+ ) -> AbstractAsyncContextManager[httpx.Response]:
559
+ """Send a streaming HTTP request asynchronously.
560
+
561
+ Return an asynchronous context manager for accessing the streaming response.
562
+
563
+ Usage example:
564
+ async with client.stream_request(request) as response:
565
+ async for chunk in response.aiter_bytes():
566
+ await process(chunk)
567
+ """
568
+ _reject_unknown_options(options, StreamOptions.__optional_keys__)
569
+ return self._decorated_stream_request(request, **options)
570
+
571
+ @asynccontextmanager
572
+ async def _stream_request(
573
+ self,
574
+ request: httpx.Request,
575
+ *,
576
+ anonymous: bool = False,
577
+ auth: AuthTypes | None = None,
578
+ follow_redirects: bool | None = None,
579
+ ) -> AsyncIterator[httpx.Response]:
580
+ auth_to_use = None if anonymous else _resolve_auth(auth, self.client.auth)
581
+ follow = follow_redirects if follow_redirects is not None else self.client.follow_redirects
582
+
583
+ _mark_streaming(request)
584
+
585
+ # Send the supplied Request: rebuilding it with client.stream()
586
+ # broke streaming request bodies and passed a different object to hooks.
587
+ response = await self.client.send(request, stream=True, auth=auth_to_use, follow_redirects=follow)
588
+ try:
589
+ yield response
590
+ finally:
591
+ await response.aclose()
592
+
593
+ def _apply_decorators(self, func: AsyncSendMethod) -> AsyncSendMethod:
594
+ """Apply all decorators to the function."""
595
+ for decorator in self._config.send_decorators:
596
+ func = cast("AsyncSendDecorator", decorator)(func)
597
+ return func
598
+
599
+ def _apply_stream_request_decorators(self, func: AsyncStreamRequestMethod) -> AsyncStreamRequestMethod:
600
+ """Apply all stream_request decorators to the function."""
601
+ for decorator in self._config.stream_decorators:
602
+ func = cast("AsyncStreamRequestDecorator", decorator)(func)
603
+ return func