httpxgen 0.1.0__tar.gz → 0.2.0__tar.gz

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 (79) hide show
  1. httpxgen-0.2.0/IMPROVEMENT_SPEC.md +483 -0
  2. {httpxgen-0.1.0 → httpxgen-0.2.0}/PKG-INFO +175 -55
  3. {httpxgen-0.1.0 → httpxgen-0.2.0}/README.md +174 -54
  4. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/cli.py +16 -0
  5. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/build.py +37 -4
  6. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/client.py +206 -41
  7. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/operations.py +96 -25
  8. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/package.py +59 -3
  9. httpxgen-0.2.0/httpxgen/generator/templates/binary_operation.jinja2 +51 -0
  10. httpxgen-0.2.0/httpxgen/generator/templates/client.jinja2 +29 -0
  11. httpxgen-0.2.0/httpxgen/generator/templates/enum.jinja2 +2 -0
  12. httpxgen-0.2.0/httpxgen/generator/templates/exceptions.jinja2 +74 -0
  13. httpxgen-0.2.0/httpxgen/generator/templates/generated_header.jinja2 +3 -0
  14. httpxgen-0.2.0/httpxgen/generator/templates/http_methods.jinja2 +12 -0
  15. httpxgen-0.2.0/httpxgen/generator/templates/model.jinja2 +9 -0
  16. httpxgen-0.2.0/httpxgen/generator/templates/models.jinja2 +6 -0
  17. httpxgen-0.2.0/httpxgen/generator/templates/operation.jinja2 +33 -0
  18. httpxgen-0.2.0/httpxgen/generator/templates/package_init.jinja2 +5 -0
  19. httpxgen-0.2.0/httpxgen/generator/templates/serialization.jinja2 +130 -0
  20. httpxgen-0.2.0/httpxgen/generator/templates/shared_init.jinja2 +51 -0
  21. httpxgen-0.2.0/httpxgen/generator/templates/transport.jinja2 +112 -0
  22. httpxgen-0.2.0/httpxgen/generator/templates.py +42 -0
  23. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/output.py +21 -2
  24. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/__init__.py +10 -1
  25. httpxgen-0.2.0/preview/invoices/client.py +264 -0
  26. httpxgen-0.2.0/preview/payments/client.py +646 -0
  27. httpxgen-0.2.0/preview/shared/__init__.py +56 -0
  28. httpxgen-0.2.0/preview/shared/exceptions.py +79 -0
  29. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/shared/serialization.py +1 -1
  30. httpxgen-0.2.0/preview/shared/transport.py +109 -0
  31. {httpxgen-0.1.0 → httpxgen-0.2.0}/pyproject.toml +2 -1
  32. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_build.py +58 -5
  33. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_client.py +5 -3
  34. httpxgen-0.2.0/tests/generator/test_improvements.py +340 -0
  35. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_operations.py +1 -1
  36. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_package.py +2 -2
  37. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_runtime.py +1 -3
  38. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/test_cli.py +25 -1
  39. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/test_output.py +2 -1
  40. httpxgen-0.1.0/httpxgen/generator/templates.py +0 -332
  41. httpxgen-0.1.0/preview/invoices/client.py +0 -184
  42. httpxgen-0.1.0/preview/payments/client.py +0 -432
  43. httpxgen-0.1.0/preview/shared/__init__.py +0 -28
  44. httpxgen-0.1.0/preview/shared/exceptions.py +0 -20
  45. {httpxgen-0.1.0 → httpxgen-0.2.0}/.github/workflows/ci.yml +0 -0
  46. {httpxgen-0.1.0 → httpxgen-0.2.0}/.gitignore +0 -0
  47. {httpxgen-0.1.0 → httpxgen-0.2.0}/.python-version +0 -0
  48. {httpxgen-0.1.0 → httpxgen-0.2.0}/CLAUDE.md +0 -0
  49. {httpxgen-0.1.0 → httpxgen-0.2.0}/LICENSE +0 -0
  50. {httpxgen-0.1.0 → httpxgen-0.2.0}/VISION.md +0 -0
  51. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/__init__.py +0 -0
  52. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/__init__.py +0 -0
  53. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/errors.py +0 -0
  54. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/models.py +0 -0
  55. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/naming.py +0 -0
  56. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/normalize.py +0 -0
  57. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/generator/schema.py +0 -0
  58. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/io.py +0 -0
  59. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/loading.py +0 -0
  60. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/openapi.py +0 -0
  61. {httpxgen-0.1.0 → httpxgen-0.2.0}/httpxgen/selection.py +0 -0
  62. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/invoices/__init__.py +0 -0
  63. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/invoices/models.py +0 -0
  64. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/payments/__init__.py +0 -0
  65. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/payments/models.py +0 -0
  66. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/py.typed +0 -0
  67. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/shared/http_methods.py +0 -0
  68. {httpxgen-0.1.0 → httpxgen-0.2.0}/preview/shared/models.py +0 -0
  69. {httpxgen-0.1.0 → httpxgen-0.2.0}/scripts/generate-preview.sh +0 -0
  70. {httpxgen-0.1.0 → httpxgen-0.2.0}/specs/api.yml +0 -0
  71. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/conftest.py +0 -0
  72. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_models.py +0 -0
  73. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_naming.py +0 -0
  74. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_normalize.py +0 -0
  75. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/generator/test_schema.py +0 -0
  76. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/test_loading.py +0 -0
  77. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/test_openapi.py +0 -0
  78. {httpxgen-0.1.0 → httpxgen-0.2.0}/tests/test_selection.py +0 -0
  79. {httpxgen-0.1.0 → httpxgen-0.2.0}/uv.lock +0 -0
@@ -0,0 +1,483 @@
1
+ # Cleaner generated client APIs
2
+
3
+ ## Context
4
+
5
+ The generated client is currently too closely coupled to `httpx` internals and
6
+ does not provide enough application-level abstractions for errors and large
7
+ binary transfers. The concrete integration in `browser-provisioner` exposes
8
+ three recurring problems:
9
+
10
+ 1. The generated client requires an `httpx.AsyncClient`, while the application
11
+ uses an API-compatible `httpx2.AsyncClient`. Consumers therefore need
12
+ `cast(Any, ...)` at every construction site.
13
+ 2. Only non-success HTTP responses are represented as `ApiError`. Transport
14
+ failures and response validation failures escape as unrelated `HTTPError`,
15
+ `ValidationError`, or `ValueError` instances. Consumers must repeat broad
16
+ exception tuples around almost every generated operation.
17
+ 3. Binary responses are always materialized through `response.content`. Large
18
+ recordings and downloads cannot be transferred incrementally, so consumers
19
+ bypass the generated operation and rebuild its URL, timeout, status handling,
20
+ and streaming behavior manually.
21
+
22
+ The multipart implementation has related limitations:
23
+
24
+ - It recognizes only properties whose own schema has `format: binary`.
25
+ Standard multi-file fields expressed as `array` with binary `items` are not
26
+ recognized as files.
27
+ - It builds `files` as `dict[str, bytes]`. A dictionary cannot represent
28
+ repeated multipart field names and the `bytes` restriction requires the
29
+ complete file to be resident in memory.
30
+ - Multipart bodies are passed through a Pydantic model and `model_dump()`.
31
+ This representation is appropriate for structured data but not for open file
32
+ handles or streaming content.
33
+ - Existing coverage exercises only one small in-memory `bytes` upload, so it
34
+ does not validate multi-file encoding or memory-safe file inputs.
35
+
36
+ ## Goals
37
+
38
+ - Generated clients accept compatible asynchronous HTTP transports without
39
+ casts in consumer code.
40
+ - Consumers can catch one generated base exception for all failures originating
41
+ from a client operation.
42
+ - Documented API errors retain their parsed, typed error model.
43
+ - Binary responses can be processed incrementally without duplicating generated
44
+ request and error logic.
45
+ - Binary and multipart requests can use file-like or streaming inputs rather
46
+ than requiring complete in-memory `bytes` values.
47
+ - Multipart arrays are encoded as repeated parts with the correct wire name.
48
+ - Ordinary JSON, form, text, and small binary operations remain simple.
49
+ - Generated client class names describe the API rather than its installation
50
+ path or the fact that its source was generated.
51
+
52
+ ## Non-goals
53
+
54
+ - Implementing a generic runtime framework behind generated clients.
55
+ - Adding automatic retries, pagination, or resumable upload protocols.
56
+ - Hiding the generated request construction or OpenAPI response branches.
57
+ - Requiring one concrete HTTP implementation such as `httpx` or `httpx2`.
58
+
59
+ ## 1. Client naming and package identity
60
+
61
+ Package location and public client identity are separate concepts and must not
62
+ be derived from the same unprocessed value.
63
+
64
+ The current single-client implementation constructs the class name from the
65
+ complete `package_name`:
66
+
67
+ ```python
68
+ client_name = f"{class_name(package_name)}Client"
69
+ ```
70
+
71
+ In `browser-provisioner`, the import package is `generated.browser_worker`.
72
+ Normalizing that full dotted path therefore produces the unnecessarily verbose
73
+ class name `GeneratedBrowserWorkerClient`. The `Generated` prefix is an
74
+ installation detail, not part of the API domain. A similar package path such as
75
+ `generated.backend_worker` would incorrectly produce
76
+ `GeneratedBackendWorkerClient`.
77
+
78
+ This is a generator design defect rather than an OpenAPI modeling error. The
79
+ consumer should not have to rename its package or add a handwritten alias to get
80
+ a clean public API.
81
+
82
+ ### Required naming precedence
83
+
84
+ For a single generated client, resolve its name in this order:
85
+
86
+ 1. An explicit `client_name` option supplied through the Python API or
87
+ `--client-name` through the CLI.
88
+ 2. Otherwise, the final segment of `package_name`, not the complete import path.
89
+ 3. If `package_name` was omitted, the final segment of the output directory as
90
+ it is today.
91
+
92
+ Examples:
93
+
94
+ | Package/import path | Explicit name | Generated class |
95
+ | --- | --- | --- |
96
+ | `generated.browser_worker` | omitted | `BrowserWorkerClient` |
97
+ | `company.payments` | omitted | `PaymentsClient` |
98
+ | `generated.api` | `ProvisionerClient` | `ProvisionerClient` |
99
+ | `ordinary_api` | omitted | `OrdinaryApiClient` |
100
+
101
+ The generator must not heuristically strip words such as `api`, `backend`,
102
+ `service`, or `worker`: they may be meaningful domain terms. Callers can remove
103
+ or replace them explicitly with `--client-name`. Only module-path prefixes are
104
+ discarded by the default rule.
105
+
106
+ The explicit name is the complete class name and is used verbatim after
107
+ validation. The generator must not append a `Client` suffix automatically.
108
+ Invalid Python identifiers, keyword names, and collisions with generated models
109
+ or support types must fail generation with an actionable error.
110
+
111
+ For workspaces split by multiple tags, the existing tag-derived client names
112
+ remain the appropriate default because each tag identifies a distinct API
113
+ surface. A single global `--client-name` must either be rejected for multi-tag
114
+ generation or replaced later by a separately designed per-tag naming option; it
115
+ must not silently rename only one generated client.
116
+
117
+ With this change, the intended `browser-provisioner` usage becomes:
118
+
119
+ ```python
120
+ from generated.browser_worker import BrowserWorkerClient
121
+
122
+ client = BrowserWorkerClient(http, browser_worker_url)
123
+ ```
124
+
125
+ There should be no `Generated` prefix in a public class merely because the
126
+ package lives below a `generated` namespace.
127
+
128
+ ## 2. Transport abstraction
129
+
130
+ Replace the concrete constructor annotation with a generated structural
131
+ protocol containing only the operations the client actually needs. At minimum,
132
+ the protocol must cover `request()`, `stream()`, and `aclose()` where applicable.
133
+
134
+ Conceptually:
135
+
136
+ ```python
137
+ class AsyncHttpClient(Protocol):
138
+ async def request(self, method: str, url: str, **kwargs: Any) -> HttpResponse: ...
139
+
140
+ def stream(
141
+ self, method: str, url: str, **kwargs: Any
142
+ ) -> AsyncContextManager[HttpResponse]: ...
143
+
144
+ async def aclose(self) -> None: ...
145
+ ```
146
+
147
+ The response should likewise be described structurally using only the members
148
+ needed by generated code, such as `status_code`, `headers`, `text`, `content`,
149
+ `json()`, and `aiter_bytes()`.
150
+
151
+ The generated constructor then accepts `AsyncHttpClient` rather than
152
+ `httpx.AsyncClient`:
153
+
154
+ ```python
155
+ client = BrowserWorkerClient(
156
+ http_client,
157
+ browser_worker_url,
158
+ timeout=30.0,
159
+ )
160
+ ```
161
+
162
+ Both `httpx.AsyncClient` and `httpx2.AsyncClient` must satisfy the protocol
163
+ without `Any` casts or adapters.
164
+
165
+ ### Transport ownership
166
+
167
+ The lifecycle contract must be explicit. Because the transport is injected by
168
+ the caller, it should be treated as borrowed by default. Closing a short-lived
169
+ generated wrapper must not unexpectedly close a shared application transport.
170
+ If owned transports are supported later, ownership should be an explicit
171
+ constructor option or separate factory.
172
+
173
+ ## 3. Unified client errors
174
+
175
+ Generate a common exception hierarchy:
176
+
177
+ ```python
178
+ class ClientError(Exception):
179
+ """Base class for failures produced by a generated client operation."""
180
+
181
+
182
+ class TransportError(ClientError):
183
+ cause: Exception
184
+
185
+
186
+ class ResponseValidationError(ClientError):
187
+ status_code: int
188
+ response: HttpResponse
189
+ cause: Exception
190
+
191
+
192
+ class ApiError(ClientError):
193
+ status_code: int
194
+ detail: object | None
195
+ response: HttpResponse
196
+ ```
197
+
198
+ The generated operation must translate failures at its boundary:
199
+
200
+ - Exceptions raised while executing the HTTP request become `TransportError`.
201
+ - Malformed JSON, incompatible response bodies, and invalid response headers
202
+ become `ResponseValidationError`.
203
+ - Documented non-success responses and unexpected status codes remain
204
+ `ApiError`.
205
+ - Existing `ClientError` instances must be re-raised unchanged.
206
+ - Cancellation and other `BaseException` subclasses must not be wrapped.
207
+
208
+ This allows ordinary consumer code to catch one type:
209
+
210
+ ```python
211
+ try:
212
+ state = await client.capture_browser_state()
213
+ except ClientError as error:
214
+ raise BrowserStateTransferException("Could not capture state") from error
215
+ ```
216
+
217
+ Specific documented failures remain distinguishable through their parsed model:
218
+
219
+ ```python
220
+ try:
221
+ return await client.download_file(download_id)
222
+ except ApiError as error:
223
+ if isinstance(error.detail, DownloadNotFoundError):
224
+ raise DownloadNotFoundException() from error
225
+ raise
226
+ ```
227
+
228
+ `ApiError` should expose convenience properties such as `code` and `message`
229
+ when its parsed detail contains those fields. Consumer code should not need
230
+ `getattr(error.parsed_body, "code", None)`.
231
+
232
+ For compatibility, the current `parsed_body` and raw `body` attributes may be
233
+ kept as deprecated aliases during a transition. New generated examples and
234
+ documentation should use `detail` and an explicitly named `raw_body` property.
235
+
236
+ ## 4. Streaming binary responses
237
+
238
+ For every operation with a successful binary response, generate both:
239
+
240
+ - the existing convenient method returning `bytes`; and
241
+ - a streaming context-manager variant.
242
+
243
+ Example surface:
244
+
245
+ ```python
246
+ content = await client.download_recording(recording_id)
247
+
248
+ async with client.stream_download_recording(recording_id) as file:
249
+ await storage.put(
250
+ StoredObject(
251
+ content=file.iter_bytes(chunk_size=64 * 1024),
252
+ content_type=file.media_type,
253
+ )
254
+ )
255
+ ```
256
+
257
+ The yielded object should provide at least:
258
+
259
+ ```python
260
+ class BinaryStream:
261
+ media_type: str | None
262
+ content_length: int | None
263
+ filename: str | None
264
+ headers: Mapping[str, str]
265
+
266
+ def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: ...
267
+ ```
268
+
269
+ The stream context manager owns only the individual response lifecycle. Leaving
270
+ the context must close the response, not the injected shared HTTP client.
271
+
272
+ Status matching, content-type validation, parsing of error responses, and
273
+ exception translation must be shared between the buffered and streaming paths.
274
+ The buffered convenience method should be implemented on top of the streaming
275
+ path where practical, so the protocol logic cannot drift between the two.
276
+
277
+ For a success response offering multiple binary media types, `media_type` must
278
+ report the normalized actual response content type. An unsupported content type
279
+ must produce `ResponseValidationError`, not an untyped `ValueError`.
280
+
281
+ ## 5. Large binary request bodies
282
+
283
+ An `application/octet-stream` request must not be limited to `bytes`. Its
284
+ generated input type should support incremental request bodies:
285
+
286
+ ```python
287
+ BinaryContent = bytes | Iterable[bytes] | AsyncIterable[bytes]
288
+ ```
289
+
290
+ The body must be passed directly as request content without Pydantic
291
+ serialization or eager concatenation.
292
+
293
+ ## 6. Multipart and multi-file requests
294
+
295
+ ### File-part abstraction
296
+
297
+ Generate or export a small transport-neutral file-part type:
298
+
299
+ ```python
300
+ @dataclass(frozen=True)
301
+ class UploadFile:
302
+ content: bytes | BinaryIO
303
+ filename: str | None = None
304
+ content_type: str | None = None
305
+ headers: Mapping[str, str] | None = None
306
+ ```
307
+
308
+ The generator converts this type to the tuple representation expected by the
309
+ selected HTTP transport. A plain `bytes` or binary file handle may be accepted
310
+ as shorthand where no metadata is needed.
311
+
312
+ Multipart file handles must never be read eagerly by generated serialization
313
+ code.
314
+
315
+ ### Generated method shape
316
+
317
+ Multipart fields should be represented directly in the operation signature
318
+ rather than being hidden inside a Pydantic body model that requires `bytes`.
319
+ For example, this OpenAPI shape:
320
+
321
+ ```yaml
322
+ type: object
323
+ required: [documents, category]
324
+ properties:
325
+ documents:
326
+ type: array
327
+ items:
328
+ type: string
329
+ format: binary
330
+ category:
331
+ type: string
332
+ ```
333
+
334
+ should produce an API resembling:
335
+
336
+ ```python
337
+ await client.upload_documents(
338
+ documents=[
339
+ UploadFile(open("a.pdf", "rb"), filename="a.pdf"),
340
+ UploadFile(open("b.pdf", "rb"), filename="b.pdf"),
341
+ ],
342
+ category="contracts",
343
+ )
344
+ ```
345
+
346
+ Required multipart fields must remain required parameters. Optional fields must
347
+ use `None` and be omitted from the encoded request when absent.
348
+
349
+ If flattening fields would collide with path, query, header, or generated
350
+ control parameters such as `timeout`, generation must fail with a useful error
351
+ or apply a deterministic, documented name disambiguation rule.
352
+
353
+ ### Multipart encoding
354
+
355
+ The generated code must use a sequence of pairs rather than a dictionary:
356
+
357
+ ```python
358
+ files: list[tuple[str, FilePart]] = []
359
+ files.extend(("documents", document) for document in documents)
360
+ ```
361
+
362
+ This preserves repeated wire names required by multi-file fields. Scalar form
363
+ data should likewise preserve repeated values where the OpenAPI schema permits
364
+ arrays.
365
+
366
+ Multipart boundaries remain the responsibility of the HTTP transport. Generated
367
+ code must not set a bare `Content-Type: multipart/form-data` header because doing
368
+ so would omit the transport-generated boundary.
369
+
370
+ ## 7. Expected browser-provisioner result
371
+
372
+ After regeneration, `browser-provisioner` should no longer need:
373
+
374
+ - the verbose `GeneratedBrowserWorkerClient` name;
375
+ - `cast(Any, self._http)` when constructing generated clients;
376
+ - repeated `(ApiError, HTTPError, ValidationError, ValueError)` exception tuples;
377
+ - `getattr(error.parsed_body, "code", None)`;
378
+ - hand-built URLs and direct `_http.stream()` calls for generated binary
379
+ endpoints; or
380
+ - duplicated status and content-type handling around recording downloads.
381
+
382
+ The application integration should reduce to domain translation around the
383
+ generated client:
384
+
385
+ ```python
386
+ try:
387
+ client = self._client(browser)
388
+ state = await client.capture_browser_state()
389
+ except ClientError as error:
390
+ raise _as_transfer_failure("read browser state from", error) from error
391
+ ```
392
+
393
+ and:
394
+
395
+ ```python
396
+ client = self._client(browser, transfer=True)
397
+ async with client.stream_download_recording(recording_id) as recording:
398
+ await self._storage.put(
399
+ StoredObject(
400
+ key=key,
401
+ content=recording.iter_bytes(chunk_size=64 * 1024),
402
+ content_type=recording.media_type or "application/octet-stream",
403
+ )
404
+ )
405
+ ```
406
+
407
+ ## 8. Tests and acceptance criteria
408
+
409
+ ### Naming
410
+
411
+ - `package_name="generated.browser_worker"` generates
412
+ `BrowserWorkerClient` by default.
413
+ - `package_name="company.payments"` generates `PaymentsClient` by default.
414
+ - `client_name="ProvisionerClient"` overrides the inferred name exactly.
415
+ - An explicit name without a `Client` suffix is accepted verbatim; explicit
416
+ values are never rewritten beyond validation.
417
+ - Invalid and colliding explicit names fail before files are written.
418
+ - Supplying a single client name during multi-tag generation fails with a clear
419
+ explanation.
420
+ - Package imports continue to use the complete dotted `package_name`; changing
421
+ class-name inference must not alter module paths.
422
+
423
+ ### Transport and errors
424
+
425
+ - An `httpx.AsyncClient` satisfies the generated transport protocol.
426
+ - An `httpx2.AsyncClient` satisfies the same protocol without a cast.
427
+ - Request failures are raised as `TransportError` with the original exception as
428
+ `__cause__`.
429
+ - Invalid JSON, invalid typed bodies, invalid response headers, and unsupported
430
+ response content types are raised as `ResponseValidationError`.
431
+ - Documented non-success responses produce `ApiError` containing the parsed
432
+ typed detail.
433
+ - Unexpected status codes produce `ApiError` with `detail is None` and preserve
434
+ the response.
435
+ - All generated operation failures can be caught as `ClientError`.
436
+
437
+ ### Streaming downloads
438
+
439
+ - A binary response can be consumed chunk by chunk without accessing
440
+ `response.content`.
441
+ - The response remains open while inside the stream context and closes on exit,
442
+ including exceptional exits.
443
+ - Error responses received by a streaming operation are parsed through the same
444
+ error path as buffered operations.
445
+ - The buffered binary method still returns `bytes` and uses the same response
446
+ validation rules.
447
+ - Multiple declared binary media types report the actual normalized media type.
448
+
449
+ ### Multipart uploads
450
+
451
+ - A single `bytes` file remains supported.
452
+ - An open binary file can be uploaded without being read eagerly by generated
453
+ code.
454
+ - An array of binary files is encoded as repeated parts with the same wire name.
455
+ - Optional file fields are omitted when `None`.
456
+ - Filenames, per-part content types, and per-part headers are preserved.
457
+ - Multipart boundaries are generated by the HTTP transport.
458
+ - A test uses a file-like object that fails if the generator attempts an eager
459
+ full read, preventing regressions to in-memory buffering.
460
+
461
+ ## 9. Implementation order
462
+
463
+ 1. Separate `client_name` from `package_name`, add the CLI/Python override, and
464
+ switch default inference to the final package segment.
465
+ 2. Introduce the transport and response protocols and remove the concrete
466
+ `httpx.AsyncClient` annotation.
467
+ 3. Add `ClientError`, `TransportError`, and `ResponseValidationError`; update
468
+ generated request and parsing boundaries.
469
+ 4. Generate streaming variants for successful binary responses and implement
470
+ buffered binary methods through the shared response path.
471
+ 5. Extend request-body analysis to distinguish single binary fields from arrays
472
+ of binary fields.
473
+ 6. Introduce `UploadFile`, flatten multipart operation parameters, and encode
474
+ files as repeated pairs.
475
+ 7. Regenerate the `browser-provisioner` client as `BrowserWorkerClient` and
476
+ remove its casts, broad exception tuples, and manual recording download
477
+ transport code.
478
+ 8. Update the README and remove streaming from the list of general limitations.
479
+
480
+ The first three steps address the concrete readability and large-recording
481
+ problems in `browser-provisioner`. Multipart changes should follow as a separate,
482
+ focused change because they alter generated method signatures and therefore
483
+ need an explicit migration note.