cliV1 0.0.6__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.
Files changed (92) hide show
  1. cliv1-0.0.6.dist-info/METADATA +212 -0
  2. cliv1-0.0.6.dist-info/RECORD +92 -0
  3. cliv1-0.0.6.dist-info/WHEEL +4 -0
  4. cliv1-0.0.6.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_1_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
  8. swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
  9. swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
  10. swagger_petstore_open_api_3_1_0/async_client.py +87 -0
  11. swagger_petstore_open_api_3_1_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_1_0/base_client.py +27 -0
  13. swagger_petstore_open_api_3_1_0/client.py +85 -0
  14. swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
  15. swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
  18. swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
  19. swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
  20. swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
  21. swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
  22. swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
  23. swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
  24. swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
  25. swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
  26. swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
  27. swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
  28. swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
  29. swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
  30. swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
  31. swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
  32. swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
  33. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
  34. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
  35. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
  36. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
  37. swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
  38. swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
  39. swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
  40. swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
  41. swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
  42. swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
  43. swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
  44. swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
  45. swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
  46. swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
  47. swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
  48. swagger_petstore_open_api_3_1_0/core/files.py +174 -0
  49. swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
  50. swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
  51. swagger_petstore_open_api_3_1_0/core/models.py +143 -0
  52. swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
  53. swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
  54. swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
  55. swagger_petstore_open_api_3_1_0/core/params.py +157 -0
  56. swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
  57. swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
  58. swagger_petstore_open_api_3_1_0/core/results.py +178 -0
  59. swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
  60. swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
  61. swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
  62. swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
  63. swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
  64. swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
  65. swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
  66. swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
  68. swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
  69. swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
  70. swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
  71. swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
  72. swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
  73. swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
  74. swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
  75. swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
  76. swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
  77. swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
  78. swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
  79. swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
  80. swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
  81. swagger_petstore_open_api_3_1_0/models/category.py +15 -0
  82. swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
  83. swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
  84. swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
  85. swagger_petstore_open_api_3_1_0/models/order.py +27 -0
  86. swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
  87. swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
  88. swagger_petstore_open_api_3_1_0/models/user.py +29 -0
  89. swagger_petstore_open_api_3_1_0/py.typed +0 -0
  90. swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
  91. swagger_petstore_open_api_3_1_0/server/server.py +17 -0
  92. swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
@@ -0,0 +1,748 @@
1
+ """httpx2-backed transports -- the default implementations of the two transport protocols.
2
+
3
+ The sync and async adapters live together because they are peers differing only in ``await``, the
4
+ same reason :class:`RawClient` and :class:`AsyncRawClient` share a module. Everything
5
+ httpx2-specific is confined here, the default TLS trust included: swapping HTTP libraries means
6
+ writing one more module like this one, not touching anything else.
7
+
8
+ File ownership is written down here and nowhere else: every handle this module opens for a body --
9
+ a ``Path`` in a multipart part or a raw body, and the temp file an async part is spilled to -- is
10
+ registered on the send's ``ExitStack``, so it is closed when the send returns or raises,
11
+ deterministically and never by a finalizer. A handle the caller opened is passed through untouched
12
+ and never closed."""
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import inspect
18
+ import io
19
+ import os
20
+ import ssl
21
+ from collections.abc import AsyncIterable, AsyncIterator, Callable, Iterable, Iterator, Mapping
22
+ from contextlib import ExitStack, suppress
23
+ from dataclasses import dataclass, replace
24
+ from pathlib import Path
25
+ from tempfile import mkstemp
26
+ from typing import Any
27
+
28
+ import certifi
29
+ import httpx2
30
+ from typing_extensions import TypeIs
31
+
32
+ from .bodies import (
33
+ BinaryBody,
34
+ FormBody,
35
+ JsonBody,
36
+ MultipartBody,
37
+ MultipartFile,
38
+ MultipartPart,
39
+ MultipartText,
40
+ RequestBody,
41
+ TextBody,
42
+ )
43
+ from .exceptions import TransportError
44
+ from .files import AsyncBinaryContent, AsyncBinaryReader, BinaryReader
45
+ from .transport import (
46
+ AsyncHttpClient,
47
+ AsyncHttpResponse,
48
+ HttpClient,
49
+ HttpRequest,
50
+ HttpResponse,
51
+ )
52
+
53
+ _DEFAULT_TIMEOUT = 30.0
54
+
55
+ _CHUNK_SIZE = 65_536
56
+ """httpx2's own multipart chunk size (``FileField.CHUNK_SIZE``), matched for the raw-body readers."""
57
+
58
+ _ASYNC_SOURCE_ON_SYNC_CLIENT = (
59
+ "an async byte source cannot be sent by the sync client -- pass bytes, a Path, "
60
+ "a binary reader or an Iterator[bytes], or use the async client"
61
+ )
62
+ """What both sync legs raise for an async arm no type checker caught -- a raw body and a multipart
63
+ part give the same answer to the same mistake.
64
+
65
+ Only the message is shared. Each ``if`` stays written out at its own site, because it is what
66
+ narrows the union away from the async arms; behind a helper, neither leg could prove its return
67
+ type."""
68
+
69
+
70
+ def _is_async_reader(content: object) -> TypeIs[AsyncBinaryReader]:
71
+ """Whether ``content`` is a reader whose ``read`` must be awaited.
72
+
73
+ ``isinstance`` alone cannot tell the two reader protocols apart -- ``runtime_checkable``
74
+ checks only that ``read`` exists -- so the async arm is decided by whether ``read`` is a
75
+ coroutine function. ``TypeIs`` rather than ``TypeGuard``: the negative branch must narrow
76
+ too, or the sync adapter could not prove its return type."""
77
+ return isinstance(content, AsyncBinaryReader) and inspect.iscoroutinefunction(getattr(type(content), "read", None))
78
+
79
+
80
+ def _require_binary_reader(reader: BinaryReader) -> None:
81
+ """Reject a text-mode handle before its first chunk reaches the wire.
82
+
83
+ ``read(0)`` returns ``b""`` for a binary handle and ``""`` for a text one, without consuming
84
+ anything -- a one-sided check: only ``str`` raises (``RawIOBase`` may legally return ``None``).
85
+
86
+ Raises:
87
+ TypeError: If ``reader`` was opened in text mode."""
88
+ if isinstance(reader.read(0), str):
89
+ raise TypeError("a text-mode file cannot carry binary content -- open it in binary mode ('rb')")
90
+
91
+
92
+ def _reports_its_own_length(reader: BinaryReader) -> bool:
93
+ """Whether the library's length peek would describe what ``read`` will yield.
94
+
95
+ The multipart encoder peeks with ``fstat(fileno())``, else a seek to the end -- both of which
96
+ measure the stream's own extent (``FileField.get_length``). That is the body only when the
97
+ reader does not transform what it reads: a buffered reader straight over a file, or an
98
+ in-memory buffer. A decompressing reader answers with its *compressed* size, and wrapping one
99
+ changes nothing, because the raw layer is still the transformer. The raw-body peek seeks
100
+ instead, which would size a transforming reader only by decompressing all of it.
101
+
102
+ ``BufferedRandom`` belongs here beside ``BufferedReader``: it is what ``os.fdopen(fd, "w+b")``
103
+ returns, so :func:`_spilled`'s drained part stays measured.
104
+
105
+ A positive list, so a reader this does not recognise is chunked rather than mis-measured."""
106
+ if isinstance(reader, io.BytesIO):
107
+ return True
108
+ return isinstance(reader, (io.BufferedReader, io.BufferedRandom)) and isinstance(
109
+ getattr(reader, "raw", None), io.FileIO
110
+ )
111
+
112
+
113
+ def _is_at_start(reader: BinaryReader) -> bool:
114
+ """Whether nothing has been read yet, so the stream's extent is also what remains.
115
+
116
+ The raw-body peek has measured the whole extent in one release and the remainder in the next, so
117
+ a handle already positioned mid-file stays chunked whichever answer the library gives (retiring
118
+ this guard is ADR-0067's deferred follow-up). ``tell`` is not part of the protocol and raises on
119
+ a stream that is fd-backed but not seekable; either is the same answer as *cannot prove it*."""
120
+ tell = getattr(reader, "tell", None)
121
+ if tell is None:
122
+ return False
123
+ try:
124
+ return bool(tell() == 0)
125
+ except OSError:
126
+ return False
127
+
128
+
129
+ def _sync_content(content: AsyncBinaryContent, stack: ExitStack) -> bytes | BinaryReader | Iterable[bytes]:
130
+ """Adapt a raw-body descriptor for the sync client, opening what needs opening onto ``stack``.
131
+
132
+ Takes the full async union because that is ``BinaryBody.content``'s type -- the *static* gate
133
+ is the emitted endpoint signature, which offers the sync client only the sync arms. The
134
+ rejection below is the runtime backstop for a caller no checker saw, replacing the opaque
135
+ stream-type error the underlying library would raise mid-send.
136
+
137
+ A reader is handed on **only when the SDK can prove its size**, whatever the library's peek would
138
+ answer: :func:`_reports_its_own_length` and :func:`_is_at_start` are the two halves of
139
+ *provable*, and everything else is chunked. That keeps the framing the SDK's decision rather than
140
+ the peek's, whose answer has changed between releases (ADR-0067). Chunking is also what a reader
141
+ carrying nothing but ``read`` needs, since the library's ``content=`` gate is
142
+ ``isinstance(..., Iterable)`` and would refuse it outright.
143
+
144
+ Raises:
145
+ TypeError: If ``content`` is an async arm, or a reader opened in text mode."""
146
+
147
+ def chunks(reader: BinaryReader) -> Iterator[bytes]:
148
+ while chunk := reader.read(_CHUNK_SIZE):
149
+ yield chunk
150
+
151
+ if isinstance(content, (bytes, bytearray)):
152
+ return bytes(content)
153
+ if isinstance(content, Path):
154
+ return stack.enter_context(content.open("rb"))
155
+ if _is_async_reader(content) or isinstance(content, AsyncIterable):
156
+ raise TypeError(_ASYNC_SOURCE_ON_SYNC_CLIENT)
157
+ if isinstance(content, BinaryReader):
158
+ _require_binary_reader(content)
159
+ if _reports_its_own_length(content) and _is_at_start(content):
160
+ return content
161
+ return chunks(content)
162
+ return content
163
+
164
+
165
+ async def _awaited_chunks(reader: AsyncBinaryReader) -> AsyncIterator[bytes]:
166
+ """Read an async reader to exhaustion in ``_CHUNK_SIZE`` windows.
167
+
168
+ Module-level rather than nested because it has two callers, and they want the same windows for
169
+ opposite reasons: a raw body streams them straight to the transport, and a multipart part writes
170
+ them to a spill file first."""
171
+ while chunk := await reader.read(_CHUNK_SIZE):
172
+ yield chunk
173
+
174
+
175
+ async def _aiter(chunks: AsyncIterable[bytes]) -> AsyncIterator[bytes]:
176
+ """Iterate an async iterable, so a caller's own chunk sizes reach :func:`_spilled` unchanged.
177
+
178
+ Only :func:`_drained` needs this: an ``AsyncIterable`` is not an ``AsyncIterator``, and
179
+ ``_spilled`` takes the narrower type so both of its callers hand it the same shape."""
180
+ async for chunk in chunks:
181
+ yield chunk
182
+
183
+
184
+ def _async_content(content: AsyncBinaryContent, stack: ExitStack) -> bytes | AsyncIterable[bytes]:
185
+ """Adapt a raw-body descriptor to the async byte stream the async client demands.
186
+
187
+ Sync arms read through a worker thread so a large upload from a slow disk does not park the
188
+ event loop; the two async arms are consumed natively. A ``Path`` is opened here, eagerly, onto
189
+ ``stack`` -- a momentary open on the loop, exactly as the multipart leg's ``_open`` does -- so
190
+ a mid-send failure closes it on the stack's unwind rather than at a finalizer.
191
+
192
+ The arm order is load-bearing: readers before iterables (a file handle iterates by *lines*),
193
+ the async-reader guard before ``AsyncIterable`` (an async file is async-iterable by lines),
194
+ and ``bytes`` before everything (it is an ``Iterable[int]``).
195
+
196
+ Raises:
197
+ TypeError: If ``content`` is a reader opened in text mode."""
198
+
199
+ async def threaded_chunks(reader: BinaryReader) -> AsyncIterator[bytes]:
200
+ # Every read in a worker thread: the non-blocking property of the async raw-body leg.
201
+ while chunk := await asyncio.to_thread(reader.read, _CHUNK_SIZE):
202
+ yield chunk
203
+
204
+ async def iterable_chunks(chunks: Iterable[bytes]) -> AsyncIterator[bytes]:
205
+ # Pulled on the loop: a caller's generator is the caller's own code; one that must not
206
+ # block the loop is passed as an AsyncIterable instead.
207
+ for chunk in chunks:
208
+ yield chunk
209
+
210
+ if isinstance(content, (bytes, bytearray)):
211
+ return bytes(content)
212
+ if isinstance(content, Path):
213
+ return threaded_chunks(stack.enter_context(content.open("rb")))
214
+ if _is_async_reader(content):
215
+ return _awaited_chunks(content)
216
+ if isinstance(content, BinaryReader):
217
+ _require_binary_reader(content)
218
+ return threaded_chunks(content)
219
+ if isinstance(content, AsyncIterable):
220
+ return content
221
+ return iterable_chunks(content)
222
+
223
+
224
+ class _ChunkReader:
225
+ """A read-only view over an iterable of chunks -- the shape the multipart encoder demands.
226
+
227
+ The encoder can pull a part from ``bytes`` or from anything with a synchronous ``read``, and
228
+ from nothing else, so an iterable reaches it through this and not at all otherwise.
229
+
230
+ One of the encoder's two adapters, and the cheap one. This arm needs only a *shape* -- the
231
+ chunks can be pulled synchronously, they simply do not arrive behind a ``read`` -- so a view
232
+ suffices and nothing is copied or stored. Its sibling :func:`_spilled` answers the same demand
233
+ for a source that must be **awaited**, which no view can bridge, and pays a temp file for it.
234
+ The full statement of the httpx2 constraint both exist for is on ``_spilled``.
235
+
236
+ It carries nothing but ``read``, deliberately. No ``seek``, so ``FileField.render_data`` skips
237
+ the rewind it performs on a real handle; no ``fileno`` and no ``tell``, so the part reports no
238
+ length and the whole body goes out chunked. An iterable is single-use, and a view that cannot
239
+ rewind is what makes that visible rather than silently re-sending nothing.
240
+
241
+ Owns nothing and closes nothing: the iterable is the caller's, exactly as a reader is."""
242
+
243
+ __slots__ = ("_buffer", "_chunks")
244
+
245
+ def __init__(self, chunks: Iterable[bytes]) -> None:
246
+ self._chunks = iter(chunks)
247
+ self._buffer = bytearray()
248
+
249
+ def read(self, size: int = -1, /) -> bytes:
250
+ """Return up to ``size`` bytes, or everything remaining when ``size`` is negative.
251
+
252
+ Args:
253
+ size: How many bytes to take; negative reads the iterable to exhaustion.
254
+
255
+ Returns:
256
+ The bytes taken, empty once the iterable is exhausted."""
257
+ while size < 0 or len(self._buffer) < size:
258
+ # ``next(..., None)`` rather than a falsy test: a generator may legally yield b"" in the
259
+ # middle of a stream, and reading that as EOF would truncate the upload.
260
+ chunk = next(self._chunks, None)
261
+ if chunk is None:
262
+ break
263
+ self._buffer.extend(chunk)
264
+ taken = bytes(self._buffer) if size < 0 else bytes(self._buffer[:size])
265
+ del self._buffer[: len(taken)]
266
+ return taken
267
+
268
+
269
+ class _UnsizedReader:
270
+ """A caller's reader with its length claim withheld -- everything else passes through.
271
+
272
+ Withholding ``fileno`` and ``tell`` makes the encoder's length peek answer ``None``, so the part
273
+ claims no length and the body frames itself chunked. That peek measures the descriptor rather
274
+ than the bytes ``read`` will produce, and the two diverge for a transforming reader; chunked is
275
+ correct where a wrong length is not.
276
+
277
+ ``seek`` is forwarded, unlike :class:`_ChunkReader`'s deliberate omission: ``render_data``
278
+ rewinds a part before every render, and that rewind is what makes a caller's seekable handle
279
+ re-sendable. A reader that cannot seek raises ``io.UnsupportedOperation``, which the encoder
280
+ already catches -- the same answer a pipe gives.
281
+
282
+ Owns nothing and closes nothing: the reader is the caller's."""
283
+
284
+ __slots__ = ("_reader",)
285
+
286
+ def __init__(self, reader: BinaryReader) -> None:
287
+ self._reader = reader
288
+
289
+ def read(self, size: int = -1, /) -> bytes:
290
+ """Read from the wrapped reader, unchanged.
291
+
292
+ Args:
293
+ size: How many bytes to take; negative reads to exhaustion.
294
+
295
+ Returns:
296
+ Whatever the wrapped reader returned."""
297
+ return self._reader.read(size)
298
+
299
+ def seek(self, offset: int, whence: int = 0, /) -> int:
300
+ """Reposition the wrapped reader, or refuse as a non-seekable stream does.
301
+
302
+ Args:
303
+ offset: Passed through unchanged.
304
+ whence: Passed through unchanged.
305
+
306
+ Returns:
307
+ The wrapped reader's new absolute position.
308
+
309
+ Raises:
310
+ io.UnsupportedOperation: If the wrapped reader cannot seek."""
311
+ seek = getattr(self._reader, "seek", None)
312
+ if seek is None:
313
+ raise io.UnsupportedOperation("underlying reader is not seekable")
314
+ return int(seek(offset, whence))
315
+
316
+
317
+ def _open(content: AsyncBinaryContent, stack: ExitStack) -> bytes | BinaryReader:
318
+ """Resolve a multipart part's content descriptor, opening what needs opening onto ``stack``.
319
+
320
+ The one place the ownership rule is written down in code: a ``Path``'s handle is the SDK's,
321
+ registered on the stack and closed when the send returns or raises; a caller's handle is
322
+ returned untouched and never closed, and an iterable is wrapped, never consumed here.
323
+
324
+ The arm order is ``_sync_content``'s, for the same reasons: ``bytes`` first (it is an
325
+ ``Iterable[int]``), the async guard before the reader check (``runtime_checkable`` cannot tell
326
+ the two reader protocols apart), readers before iterables (a file handle iterates by *lines*).
327
+ On the async leg the async guard is unreachable -- :func:`_drained_parts` has already spilled
328
+ those arms to a file, because the encoder cannot await.
329
+
330
+ Raises:
331
+ TypeError: If ``content`` is an async arm, or a reader opened in text mode."""
332
+ if isinstance(content, (bytes, bytearray)):
333
+ return bytes(content)
334
+ if isinstance(content, Path):
335
+ return stack.enter_context(content.open("rb"))
336
+ if _is_async_reader(content) or isinstance(content, AsyncIterable):
337
+ raise TypeError(_ASYNC_SOURCE_ON_SYNC_CLIENT)
338
+ if isinstance(content, BinaryReader):
339
+ _require_binary_reader(content)
340
+ return content if _reports_its_own_length(content) else _UnsizedReader(content)
341
+ return _ChunkReader(content)
342
+
343
+
344
+ async def _spilled(chunks: AsyncIterator[bytes], stack: ExitStack) -> BinaryReader:
345
+ """Write an async source to a temp file the multipart encoder can read synchronously.
346
+
347
+ **The limitation this exists for, at source (httpx2 2.13.1).** ``MultipartStream.__aiter__`` is
348
+ ``for chunk in self.iter_chunks(): yield chunk`` -- an ``async`` generator delegating to a
349
+ *synchronous* one -- and ``FileField.render_data`` pulls each part with a plain
350
+ ``self.file.read(CHUNK_SIZE)``. There is no ``await`` anywhere in ``_multipart.py``. So the
351
+ encoder asks for bytes with a function call, and an awaitable source can only answer with one,
352
+ and the two cannot meet: **both** async arms are affected, ``AsyncBinaryReader`` and
353
+ ``AsyncIterable[bytes]`` alike, and neither can be driven from inside the encode at any cost.
354
+ Draining first is not an optimisation, it is the only way either arm reaches the wire.
355
+
356
+ A temp file is where it drains *to* because the encoder's demand is a synchronous ``read``, and
357
+ that is the cheapest thing that offers one without holding the payload: each chunk is written
358
+ straight out and dropped, so **nothing accumulates** -- the alternative that kept the bytes in
359
+ the process peaked at twice the payload, measured (ADR-0051).
360
+
361
+ **What bounds the residency is the chunk size, and only one arm's is the SDK's.**
362
+ :func:`_awaited_chunks` reads a reader in ``_CHUNK_SIZE`` windows, so that arm is bounded here
363
+ by construction. :func:`_aiter` forwards an ``AsyncIterable``'s own chunks unchanged, because
364
+ re-slicing a ``bytes`` the caller already built would add a copy rather than remove one -- so
365
+ there the peak is the caller's largest ``yield``, whatever that is, and a caller who yields
366
+ their whole payload at once has already made it resident before this is reached. Either way
367
+ this function's own overhead is one chunk, never the total.
368
+
369
+ **The sibling limitation, so the two are not confused.** A *sync* ``Iterator[bytes]`` in a part
370
+ hits the same ``read``-only demand from the other direction -- it can be pulled synchronously
371
+ but carries no ``read`` at all -- and is answered by :class:`_ChunkReader`, a view rather than a
372
+ file, because there is nothing to await and so nothing to drain. Same encoder constraint, two
373
+ adapters, and which one applies is decided by whether the source needs an ``await``.
374
+
375
+ Owning the encoder would retire both adapters and the loop-blocking with them. It was designed
376
+ and declined: ``core/`` is vendored per SDK, so a bug in it ships everywhere and is fixable only
377
+ by re-vendoring, and RFC 7578 escaping is the refusal that stops a CR/LF in a filename splicing
378
+ a header into the body. If httpx2 ever grows an ``__aiter__`` that awaits, consume it -- do not
379
+ write one (ADR-0051, ADR-0024).
380
+
381
+ Why not the two other things that offer a synchronous ``read``. Accumulating into ``bytes`` is
382
+ the defect this replaced -- it peaked at twice the source, once for the chunk list and once for
383
+ the join. A ``SpooledTemporaryFile``'s ``fileno()`` forces a rollover to disk (measured), so it
384
+ would reach disk for a ten-byte part *and* answer the encoder's length probe only by doing so;
385
+ a plain temp file is smaller and needs no threshold to tune. Being a real file is also what
386
+ keeps the part **sized**: the encoder reaches ``fstat`` through it, so this arm still contributes
387
+ a ``Content-Length`` exactly as the resident ``bytes`` did, and stays re-sendable because it is
388
+ seekable.
389
+
390
+ A caller whose async source is a local file should hand over the ``Path`` instead: this arm
391
+ then costs two reads and a write to move bytes that were already on disk in the right shape.
392
+ Said in ``files.py``'s module docstring, where a caller reads it.
393
+
394
+ Built from ``mkstemp`` rather than ``TemporaryFile`` so the object handed on is the same on
395
+ every platform. ``TemporaryFile`` is an alias for ``NamedTemporaryFile`` wherever an open file
396
+ cannot be unlinked -- Windows -- and that returns a wrapper proxying ``read`` through
397
+ ``__getattr__``. A ``runtime_checkable`` protocol tests the *class*, so the wrapper does not
398
+ satisfy :class:`BinaryReader`, and the part would fall through to the chunk-reader arm and go
399
+ out chunked. Normalised here rather than branched on later.
400
+
401
+ **Unlinked immediately where the platform allows it.** A POSIX file survives its own directory
402
+ entry as long as a descriptor is open, so removing the name now leaves the content reachable
403
+ through the handle and reclaimed by the kernel when the handle closes -- *including* if the
404
+ process is killed, which is the one exit an ``ExitStack`` cannot unwind. Windows refuses to
405
+ unlink an open file, so there the removal is deferred onto the stack instead. Probed rather than
406
+ branched on ``os.name``: the question is what this filesystem permits, and the answer decides
407
+ only *when* the name goes away.
408
+
409
+ Args:
410
+ chunks: The async source, normalised to an iterator of byte windows.
411
+ stack: The send's exit stack, which closes the file -- and deletes it, where it could not be
412
+ deleted up front -- when the send returns or raises.
413
+
414
+ Returns:
415
+ The file, positioned at 0 and ready for the encoder's own reads."""
416
+ descriptor, path = mkstemp()
417
+ # Registration order is the whole of the cleanup contract, and it reads backwards: an
418
+ # ``ExitStack`` unwinds last-registered-first, so the removal goes on *before* the handle in
419
+ # order to run *after* it. ``missing_ok`` is what lets the same callback stand whether or not
420
+ # the immediate unlink below already won.
421
+ stack.callback(Path(path).unlink, missing_ok=True)
422
+ spill = stack.enter_context(os.fdopen(descriptor, "w+b"))
423
+ with suppress(OSError):
424
+ os.unlink(path)
425
+ async for chunk in chunks:
426
+ spill.write(chunk)
427
+ spill.seek(0)
428
+ return spill
429
+
430
+
431
+ async def _drained(part: MultipartPart, stack: ExitStack) -> MultipartPart:
432
+ """Spill one part's content to a temp file if it is an async source, else hand it back unchanged.
433
+
434
+ Args:
435
+ part: The part to resolve.
436
+ stack: The send's exit stack, owning any file opened for this part.
437
+
438
+ Returns:
439
+ The part, or a copy of it reading from the file its source was drained into."""
440
+ if not isinstance(part, MultipartFile):
441
+ return part
442
+ if _is_async_reader(part.content):
443
+ return replace(part, content=await _spilled(_awaited_chunks(part.content), stack))
444
+ if isinstance(part.content, AsyncIterable):
445
+ return replace(part, content=await _spilled(_aiter(part.content), stack))
446
+ return part
447
+
448
+
449
+ async def _drained_parts(body: RequestBody | None, stack: ExitStack) -> RequestBody | None:
450
+ """Resolve any async part content before the multipart body is encoded.
451
+
452
+ The encoder pulls every part from a synchronous chunk generator in both flavours, so an
453
+ awaitable source cannot be driven from inside it. Draining it here is what makes the arm work;
454
+ :func:`_spilled` is what keeps it bounded. The loop stays free during the drain -- the source is
455
+ awaited natively -- and is then occupied by the encode itself, which is httpx2's synchronous
456
+ generator and the one cost this leg still pays.
457
+
458
+ Runs **inside** the send's ``ExitStack``, necessarily: it opens a file per async part, and those
459
+ must unwind with everything else the send opened.
460
+
461
+ Returns the body **unchanged** when nothing was drained, which is every multipart request whose
462
+ parts are all sync arms.
463
+
464
+ Args:
465
+ body: The request's body, of any shape.
466
+ stack: The send's exit stack.
467
+
468
+ Returns:
469
+ The body, or a multipart body whose async parts now read from a file."""
470
+ if not isinstance(body, MultipartBody):
471
+ return body
472
+ parts = [await _drained(part, stack) for part in body.parts]
473
+ # Identity, not equality: ``_drained`` hands an untouched part straight back, so ``is`` asks
474
+ # exactly what this needs to know and cannot be answered by a part that merely compares equal.
475
+ return body if all(new is old for new, old in zip(parts, body.parts, strict=True)) else MultipartBody(parts)
476
+
477
+
478
+ def _httpx2_part(part: MultipartPart, stack: ExitStack) -> tuple[str, Any]:
479
+ """Render one part in httpx2's ``files=`` tuple spelling.
480
+
481
+ Text parts go through the *file* spelling -- ``(None, content)``, a part with no filename --
482
+ rather than through ``data=``, because httpx2 chooses the encoding on the **truthiness** of
483
+ ``files``: an operation declaring ``multipart/form-data`` with no binary part would otherwise
484
+ be sent as ``application/x-www-form-urlencoded``. The rendered bytes are identical either way,
485
+ repeated keys and name escaping included, so the invariant costs nothing. A text part's bytes
486
+ pass through the encoder as given: it frames them and encodes nothing."""
487
+ match part:
488
+ case MultipartText(name, content, None):
489
+ return (name, (None, content))
490
+ case MultipartText(name, content, media_type):
491
+ return (name, (None, content, media_type))
492
+ case MultipartFile(name, content, filename, media_type):
493
+ return (name, (filename, _open(content, stack), media_type))
494
+
495
+
496
+ def _body_kwargs(
497
+ body: RequestBody | None,
498
+ stack: ExitStack,
499
+ *,
500
+ binary_content: Callable[[AsyncBinaryContent, ExitStack], object],
501
+ ) -> dict[str, Any]:
502
+ """Map the request's body onto the httpx2 keyword that carries it.
503
+
504
+ This ``match`` is the only place the body union is interpreted, and it is exhaustive by
505
+ construction: adding a body shape without handling it here is a type error.
506
+
507
+ Nothing here spells a header. A body's label -- its media type, and a raw body's
508
+ ``Content-Disposition`` -- is already among ``request.headers``, folded there when the request was
509
+ built (``bodies.content_headers``), so this function moves bytes and nothing else. The one label
510
+ a transport still writes is multipart's, whose boundary exists only once httpx2 has minted it.
511
+
512
+ ``binary_content`` is the one flavour-differing arm, passed as data with no default so each
513
+ transport must name its own adapter -- ``_sync_content`` or ``_async_content``. One shared
514
+ match rather than two sibling copies: this module is hand-written, where five duplicated arms
515
+ to differ in one is drift risk rather than generator output.
516
+
517
+ Args:
518
+ body: The body being mapped -- the request's own, or the one :func:`_drained_parts`
519
+ resolved from it.
520
+ stack: The send's exit stack, onto which any handle opened for this body is registered.
521
+ binary_content: The flavour's raw-body adapter.
522
+
523
+ Returns:
524
+ The httpx2 keyword argument carrying this body; empty when there is none."""
525
+ match body:
526
+ case None:
527
+ return {}
528
+ case JsonBody(content, _) | FormBody(content) | TextBody(content, _, _):
529
+ return {"content": content}
530
+ case MultipartBody(parts):
531
+ return {"files": [_httpx2_part(part, stack) for part in parts]}
532
+ case BinaryBody(content, _, _):
533
+ return {"content": binary_content(content, stack)}
534
+
535
+
536
+ def _timeout_kwargs(request: HttpRequest) -> dict[str, Any]:
537
+ """Map a per-request timeout onto httpx2's keyword, or contribute nothing.
538
+
539
+ Omitting is deliberate rather than passing ``timeout=None``: to httpx2 that means *no timeout at
540
+ all*, not *use the client's own*, so an absent override must not reach the call.
541
+
542
+ Args:
543
+ request: The request whose ``timeout`` is being mapped.
544
+
545
+ Returns:
546
+ ``{"timeout": ...}`` when the request set one, otherwise an empty mapping."""
547
+ return {} if request.timeout is None else {"timeout": httpx2.Timeout(request.timeout)}
548
+
549
+
550
+ def _default_ssl_context() -> ssl.SSLContext:
551
+ """The trust a transport gets when ``verify`` is left at ``True``.
552
+
553
+ ``SSL_CERT_FILE``, else ``SSL_CERT_DIR``, else certifi's bundle -- the order the SDK shipped with
554
+ before httpx2, which ends the same chain with ``truststore`` instead (ADR-0067). A plain
555
+ ``ssl.SSLContext`` also keeps anyio's TLS wrap on the event loop rather than a worker thread.
556
+
557
+ Returns:
558
+ A fresh verifying context, one per transport."""
559
+ if cafile := os.environ.get("SSL_CERT_FILE"):
560
+ return ssl.create_default_context(cafile=cafile)
561
+ if capath := os.environ.get("SSL_CERT_DIR"):
562
+ return ssl.create_default_context(capath=capath)
563
+ return ssl.create_default_context(cafile=certifi.where())
564
+
565
+
566
+ @dataclass(frozen=True, slots=True)
567
+ class _Httpx2Response:
568
+ """A thin veneer meeting :class:`HttpResponse` over the library's own response.
569
+
570
+ httpx2 lowercases header names on iteration, so ``headers`` meets the protocols' casing
571
+ obligation by construction; ``read`` is the library's own, which both buffers the body and
572
+ releases the connection -- the exact obligation the protocol states."""
573
+
574
+ _response: httpx2.Response
575
+
576
+ @property
577
+ def url(self) -> str:
578
+ # httpx2 answers this from the request it kept, and the SDK never sets follow_redirects, so
579
+ # it is the URL that was asked for. A property rather than a field: the generated dataclass
580
+ # repr would otherwise print a query string a credential can sit in (ADR-0017).
581
+ return str(self._response.url)
582
+
583
+ @property
584
+ def status_code(self) -> int:
585
+ return self._response.status_code
586
+
587
+ @property
588
+ def headers(self) -> Mapping[str, str]:
589
+ return dict(self._response.headers)
590
+
591
+ def iter_bytes(self, chunk_size: int | None) -> Iterator[bytes]:
592
+ return self._response.iter_bytes(chunk_size)
593
+
594
+ def read(self) -> bytes:
595
+ return self._response.read()
596
+
597
+ def close(self) -> None:
598
+ self._response.close()
599
+
600
+
601
+ @dataclass(frozen=True, slots=True)
602
+ class _AsyncHttpx2Response:
603
+ """The awaited twin of :class:`_Httpx2Response`, meeting :class:`AsyncHttpResponse`."""
604
+
605
+ _response: httpx2.Response
606
+
607
+ @property
608
+ def url(self) -> str:
609
+ # httpx2 answers this from the request it kept, and the SDK never sets follow_redirects, so
610
+ # it is the URL that was asked for. A property rather than a field: the generated dataclass
611
+ # repr would otherwise print a query string a credential can sit in (ADR-0017).
612
+ return str(self._response.url)
613
+
614
+ @property
615
+ def status_code(self) -> int:
616
+ return self._response.status_code
617
+
618
+ @property
619
+ def headers(self) -> Mapping[str, str]:
620
+ return dict(self._response.headers)
621
+
622
+ def aiter_bytes(self, chunk_size: int | None) -> AsyncIterator[bytes]:
623
+ return self._response.aiter_bytes(chunk_size)
624
+
625
+ async def aread(self) -> bytes:
626
+ return await self._response.aread()
627
+
628
+ async def aclose(self) -> None:
629
+ await self._response.aclose()
630
+
631
+
632
+ class Httpx2Client(HttpClient):
633
+ """Sync transport backed by httpx2.
634
+
635
+ Uses connection pooling via a single ``httpx2.Client``, and returns once the response head
636
+ has arrived; the body is read by whoever the raw client hands it to.
637
+
638
+ Requests honour the standard proxy environment variables -- ``HTTP_PROXY`` / ``HTTPS_PROXY`` /
639
+ ``ALL_PROXY`` / ``NO_PROXY``, consulted only when ``proxy_url`` is unset. That is the underlying
640
+ library's default rather than a decision made here, and it is stated because it is request
641
+ behaviour the environment can change; a caller needing it off supplies a transport of their own.
642
+
643
+ TLS trust *is* decided here: ``verify=True`` gets :func:`_default_ssl_context`, never httpx2's
644
+ own ``truststore`` default (ADR-0067). ``verify`` also takes an ``ssl.SSLContext``, used exactly
645
+ as given: a private CA bundle or a client certificate is configured by building one
646
+ (``ssl.create_default_context(cafile=...)``). The one leg no context reaches is the TLS hop *to*
647
+ an ``https://`` proxy, which the library verifies with its own default."""
648
+
649
+ def __init__(
650
+ self,
651
+ *,
652
+ timeout: float = _DEFAULT_TIMEOUT,
653
+ proxy_url: str | None = None,
654
+ verify: ssl.SSLContext | bool = True,
655
+ ) -> None:
656
+ self._client = httpx2.Client(
657
+ proxy=proxy_url,
658
+ timeout=httpx2.Timeout(timeout),
659
+ verify=_default_ssl_context() if verify is True else verify,
660
+ )
661
+ self._closed = False
662
+
663
+ def send(self, request: HttpRequest) -> HttpResponse:
664
+ # The stack owns every handle opened for this body, and closes once the head has arrived --
665
+ # which is after the request body has been sent in full, so closing there is correct, not
666
+ # early. An exception mid-send closes it on the unwind. The *response* body is pending; the
667
+ # returned wrapper owns that connection until it is closed.
668
+ #
669
+ # httpx2.TransportError is the exact line the protocol draws -- it covers a refused, dropped,
670
+ # timed-out or protocol-broken connection, and excludes httpx2.DecodingError and
671
+ # TooManyRedirects, which are things that went wrong *after* a response arrived. One clause
672
+ # for all of it: the wrapper carries no detail to sort them by, and the caller is handed the
673
+ # httpx2 exception itself. The same guard sits at both seams.
674
+ try:
675
+ with ExitStack() as stack:
676
+ httpx2_request = self._client.build_request(
677
+ method=request.method,
678
+ url=request.url,
679
+ headers=request.headers,
680
+ **_timeout_kwargs(request),
681
+ **_body_kwargs(request.body, stack, binary_content=_sync_content),
682
+ )
683
+ response = self._client.send(httpx2_request, stream=True)
684
+ except httpx2.TransportError as e:
685
+ raise TransportError(e) from e
686
+ return _Httpx2Response(response)
687
+
688
+ def close(self) -> None:
689
+ if self._closed:
690
+ return
691
+ self._closed = True
692
+ self._client.close()
693
+
694
+
695
+ class AsyncHttpx2Client(AsyncHttpClient):
696
+ """Async transport backed by httpx2.
697
+
698
+ Uses connection pooling via a single ``httpx2.AsyncClient``, and returns once the response
699
+ head has arrived; the body is read by whoever the raw client hands it to.
700
+
701
+ Requests honour the standard proxy environment variables -- ``HTTP_PROXY`` / ``HTTPS_PROXY`` /
702
+ ``ALL_PROXY`` / ``NO_PROXY``, consulted only when ``proxy_url`` is unset. That is the underlying
703
+ library's default rather than a decision made here, and it is stated because it is request
704
+ behaviour the environment can change; a caller needing it off supplies a transport of their own.
705
+
706
+ TLS trust *is* decided here: ``verify=True`` gets :func:`_default_ssl_context`, never httpx2's
707
+ own ``truststore`` default (ADR-0067). ``verify`` also takes an ``ssl.SSLContext``, used exactly
708
+ as given: a private CA bundle or a client certificate is configured by building one
709
+ (``ssl.create_default_context(cafile=...)``). The one leg no context reaches is the TLS hop *to*
710
+ an ``https://`` proxy, which the library verifies with its own default."""
711
+
712
+ def __init__(
713
+ self,
714
+ *,
715
+ timeout: float = _DEFAULT_TIMEOUT,
716
+ proxy_url: str | None = None,
717
+ verify: ssl.SSLContext | bool = True,
718
+ ) -> None:
719
+ self._client = httpx2.AsyncClient(
720
+ proxy=proxy_url,
721
+ timeout=httpx2.Timeout(timeout),
722
+ verify=_default_ssl_context() if verify is True else verify,
723
+ )
724
+ self._closed = False
725
+
726
+ async def send(self, request: HttpRequest) -> AsyncHttpResponse:
727
+ try:
728
+ with ExitStack() as stack:
729
+ # Inside the stack, necessarily: the pre-pass opens a spill file per async part, and
730
+ # those unwind with everything else this send opened.
731
+ body = await _drained_parts(request.body, stack)
732
+ httpx2_request = self._client.build_request(
733
+ method=request.method,
734
+ url=request.url,
735
+ headers=request.headers,
736
+ **_timeout_kwargs(request),
737
+ **_body_kwargs(body, stack, binary_content=_async_content),
738
+ )
739
+ response = await self._client.send(httpx2_request, stream=True)
740
+ except httpx2.TransportError as e:
741
+ raise TransportError(e) from e
742
+ return _AsyncHttpx2Response(response)
743
+
744
+ async def aclose(self) -> None:
745
+ if self._closed:
746
+ return
747
+ self._closed = True
748
+ await self._client.aclose()