httpxgen 0.2.0__tar.gz → 0.3.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 (84) hide show
  1. {httpxgen-0.2.0 → httpxgen-0.3.0}/.github/workflows/ci.yml +68 -64
  2. {httpxgen-0.2.0 → httpxgen-0.3.0}/PKG-INFO +133 -7
  3. {httpxgen-0.2.0 → httpxgen-0.3.0}/README.md +679 -553
  4. httpxgen-0.3.0/changelog.md +298 -0
  5. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/build.py +222 -195
  6. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/client.py +234 -81
  7. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/models.py +144 -27
  8. httpxgen-0.3.0/httpxgen/generator/naming.py +97 -0
  9. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/normalize.py +692 -665
  10. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/operations.py +367 -110
  11. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/package.py +279 -258
  12. httpxgen-0.3.0/httpxgen/generator/schema.py +226 -0
  13. httpxgen-0.3.0/httpxgen/generator/templates/binary_operation.jinja2 +51 -0
  14. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/client.jinja2 +29 -29
  15. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/exceptions.jinja2 +81 -74
  16. httpxgen-0.3.0/httpxgen/generator/templates/serialization.jinja2 +287 -0
  17. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/shared_init.jinja2 +67 -51
  18. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/transport.jinja2 +155 -112
  19. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates.py +42 -42
  20. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/openapi.py +118 -117
  21. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/output.py +27 -2
  22. httpxgen-0.3.0/httpxgen/references.py +149 -0
  23. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/selection.py +66 -50
  24. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/invoices/client.py +17 -10
  25. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/payments/client.py +60 -30
  26. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/payments/models.py +21 -7
  27. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/shared/__init__.py +16 -0
  28. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/shared/exceptions.py +8 -1
  29. httpxgen-0.3.0/preview/shared/serialization.py +294 -0
  30. httpxgen-0.3.0/preview/shared/transport.py +162 -0
  31. {httpxgen-0.2.0 → httpxgen-0.3.0}/pyproject.toml +1 -1
  32. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_build.py +34 -33
  33. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_client.py +5 -5
  34. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_improvements.py +351 -340
  35. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_normalize.py +3 -5
  36. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_schema.py +94 -92
  37. httpxgen-0.3.0/tests/generator/test_tickets.py +2634 -0
  38. httpxgen-0.3.0/tests/generator/test_typing.py +114 -0
  39. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/test_output.py +50 -2
  40. {httpxgen-0.2.0 → httpxgen-0.3.0}/uv.lock +546 -546
  41. httpxgen-0.2.0/IMPROVEMENT_SPEC.md +0 -483
  42. httpxgen-0.2.0/httpxgen/generator/naming.py +0 -40
  43. httpxgen-0.2.0/httpxgen/generator/schema.py +0 -142
  44. httpxgen-0.2.0/httpxgen/generator/templates/binary_operation.jinja2 +0 -51
  45. httpxgen-0.2.0/httpxgen/generator/templates/serialization.jinja2 +0 -130
  46. httpxgen-0.2.0/preview/shared/serialization.py +0 -137
  47. httpxgen-0.2.0/preview/shared/transport.py +0 -109
  48. {httpxgen-0.2.0 → httpxgen-0.3.0}/.gitignore +0 -0
  49. {httpxgen-0.2.0 → httpxgen-0.3.0}/.python-version +0 -0
  50. {httpxgen-0.2.0 → httpxgen-0.3.0}/CLAUDE.md +0 -0
  51. {httpxgen-0.2.0 → httpxgen-0.3.0}/LICENSE +0 -0
  52. {httpxgen-0.2.0 → httpxgen-0.3.0}/VISION.md +0 -0
  53. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/__init__.py +0 -0
  54. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/cli.py +0 -0
  55. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/__init__.py +0 -0
  56. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/errors.py +0 -0
  57. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/enum.jinja2 +0 -0
  58. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/generated_header.jinja2 +0 -0
  59. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/http_methods.jinja2 +0 -0
  60. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/model.jinja2 +0 -0
  61. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/models.jinja2 +0 -0
  62. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/operation.jinja2 +0 -0
  63. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/generator/templates/package_init.jinja2 +0 -0
  64. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/io.py +0 -0
  65. {httpxgen-0.2.0 → httpxgen-0.3.0}/httpxgen/loading.py +0 -0
  66. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/__init__.py +0 -0
  67. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/invoices/__init__.py +0 -0
  68. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/invoices/models.py +0 -0
  69. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/payments/__init__.py +0 -0
  70. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/py.typed +0 -0
  71. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/shared/http_methods.py +0 -0
  72. {httpxgen-0.2.0 → httpxgen-0.3.0}/preview/shared/models.py +0 -0
  73. {httpxgen-0.2.0 → httpxgen-0.3.0}/scripts/generate-preview.sh +0 -0
  74. {httpxgen-0.2.0 → httpxgen-0.3.0}/specs/api.yml +0 -0
  75. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/conftest.py +0 -0
  76. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_models.py +0 -0
  77. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_naming.py +0 -0
  78. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_operations.py +0 -0
  79. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_package.py +0 -0
  80. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/generator/test_runtime.py +0 -0
  81. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/test_cli.py +0 -0
  82. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/test_loading.py +0 -0
  83. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/test_openapi.py +0 -0
  84. {httpxgen-0.2.0 → httpxgen-0.3.0}/tests/test_selection.py +0 -0
@@ -1,64 +1,68 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
-
8
- jobs:
9
- lint:
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v4
13
-
14
- - name: Install uv
15
- uses: astral-sh/setup-uv@v4
16
- with:
17
- python-version: "3.14"
18
-
19
- - name: Install dependencies
20
- run: uv sync --all-groups
21
-
22
- - name: Ruff lint
23
- run: uv run ruff check --output-format=github .
24
-
25
- - name: Ruff format
26
- run: uv run ruff format --check .
27
-
28
- preview:
29
- runs-on: ubuntu-latest
30
- steps:
31
- - uses: actions/checkout@v4
32
-
33
- - name: Install uv
34
- uses: astral-sh/setup-uv@v4
35
- with:
36
- python-version: "3.14"
37
-
38
- - name: Install dependencies
39
- run: uv sync --all-groups
40
-
41
- - name: Regenerate the preview client
42
- run: bash scripts/generate-preview.sh
43
-
44
- - name: Fail if the checked-in preview is stale
45
- run: git diff --exit-code -- preview
46
-
47
- test:
48
- runs-on: ubuntu-latest
49
- strategy:
50
- matrix:
51
- python-version: ["3.12", "3.13", "3.14"]
52
- steps:
53
- - uses: actions/checkout@v4
54
-
55
- - name: Install uv
56
- uses: astral-sh/setup-uv@v4
57
- with:
58
- python-version: ${{ matrix.python-version }}
59
-
60
- - name: Install dependencies
61
- run: uv sync --all-groups
62
-
63
- - name: Run tests
64
- run: uv run pytest
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v4
16
+ with:
17
+ python-version: "3.14"
18
+
19
+ - name: Install dependencies
20
+ run: uv sync --locked --all-groups
21
+
22
+ - name: Ruff lint
23
+ run: uv run ruff check --output-format=github .
24
+
25
+ - name: Ruff format
26
+ run: uv run ruff format --check .
27
+
28
+ preview:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Install uv
34
+ uses: astral-sh/setup-uv@v4
35
+ with:
36
+ python-version: "3.14"
37
+
38
+ - name: Install dependencies
39
+ run: uv sync --locked --all-groups
40
+
41
+ - name: Regenerate the preview client
42
+ run: bash scripts/generate-preview.sh
43
+
44
+ - name: Fail if the checked-in preview is stale
45
+ run: git diff --exit-code -- preview
46
+
47
+ test:
48
+ runs-on: ubuntu-latest
49
+ strategy:
50
+ matrix:
51
+ python-version: ["3.12", "3.13", "3.14"]
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - name: Install uv
56
+ uses: astral-sh/setup-uv@v4
57
+ with:
58
+ python-version: ${{ matrix.python-version }}
59
+
60
+ - name: Install dependencies
61
+ run: uv sync --locked --all-groups
62
+
63
+ - name: Run tests
64
+ run: uv run pytest
65
+
66
+ - name: Type-check generated protocols
67
+ if: matrix.python-version == '3.14'
68
+ run: uv run --frozen --with mypy==2.4.0 --with httpx2==2.13.1 pytest --no-cov tests/generator/test_typing.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: httpxgen
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Generate a typed async httpx client from an OpenAPI document.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -291,7 +291,23 @@ Path parameters carry their spec format — `format: uuid` becomes `UUID`, not `
291
291
  raise ApiError(response.status_code, response.text, response=response)
292
292
  ```
293
293
 
294
- Request bodies are a single typed `body` argument, serialized by alias and without `None` noise:
294
+ Request bodies are a single typed `body` argument, serialized by alias.
295
+ JSON inputs are validated using the declared Pydantic schema before the HTTP
296
+ transport runs, including mappings and model instances changed after creation.
297
+ Invalid inputs raise `RequestValidationError`; Pydantic's normal accepted
298
+ conversions apply before serialization.
299
+ Unsupported runtime `content_type` choices also raise `RequestValidationError`
300
+ before sending a request, with the declared alternatives listed.
301
+ Explicitly supplied nullable fields are sent as JSON null; unset nullable fields
302
+ are omitted, including inside nested models:
303
+
304
+ For `application/merge-patch+json`, all unset model fields are omitted, including
305
+ schema defaults. Explicit values equal to a default are still sent. Ordinary
306
+ JSON create requests continue to include non-null defaults.
307
+
308
+ Omit an optional JSON body by leaving out `body`. Passing `body=None` sends the
309
+ literal JSON payload `null` when the schema permits null, with the selected JSON
310
+ Content-Type. Omission uses a separate generated sentinel.
295
311
 
296
312
  ```python
297
313
  async def create_charge(
@@ -301,9 +317,7 @@ Request bodies are a single typed `body` argument, serialized by alias and witho
301
317
  timeout: float | None = None,
302
318
  ) -> Charge:
303
319
  ...
304
- json_body = TypeAdapter(CreateChargeRequest).dump_python(
305
- body, mode="json", by_alias=True, exclude_none=True
306
- )
320
+ json_body = serialize_json_body(body, CreateChargeRequest)
307
321
  ...
308
322
  ```
309
323
 
@@ -311,10 +325,19 @@ The same direct shape is used for JSON, form, text, and small binary bodies.
311
325
  An `application/octet-stream` body accepts `bytes`, `Iterable[bytes]`, or
312
326
  `AsyncIterable[bytes]` and is passed directly as `content=`.
313
327
 
328
+ Synchronous binary iterables are adapted lazily to async chunks. Exceptions
329
+ raised while consuming an upload iterable are reported as `TransportError`.
330
+
314
331
  Multipart fields are flattened into method arguments. Binary arrays become
315
- repeated parts with the same wire name, while `UploadFile` carries optional
332
+ repeated parts with the same wire name. Parameter and multipart argument names
333
+ that collide with generated locals (such as `path`, `headers`, or `files`) gain
334
+ an `_arg` suffix; their wire names are preserved. `UploadFile` carries optional
316
335
  per-part metadata without reading an open file eagerly:
317
336
 
337
+ Bytes and default `UploadFile` inputs use the filename `upload`; bare handles
338
+ use their basename when available. Set `UploadFile(filename=None, ...)`
339
+ explicitly to send a filename-less part.
340
+
318
341
  ```python
319
342
  await client.upload_documents(
320
343
  documents=[
@@ -332,6 +355,16 @@ call sites to pass fields directly.
332
355
  Successful binary responses also get a streaming context-manager variant. The
333
356
  buffered method remains available for small responses:
334
357
 
358
+ For endpoints with mixed success representations, buffered methods return the
359
+ declared bytes/model/text/None union. Streaming variants yield `BinaryStream`
360
+ for binary content and the parsed value for JSON, text, or empty successes.
361
+
362
+ Binary successes with declared headers return a typed result whose `body` is
363
+ bytes and whose fields contain validated headers. Their streaming variants
364
+ yield a corresponding `...Result...Stream` model with `body: BinaryStream` and
365
+ the same header fields, validated before iteration. Endpoints without declared
366
+ headers retain the direct bytes/`BinaryStream` API.
367
+
335
368
  ```python
336
369
  content = await client.download_recording(recording_id)
337
370
 
@@ -343,6 +376,7 @@ print(recording.media_type, recording.content_length, recording.filename)
343
376
  ```
344
377
 
345
378
  The streaming context owns the individual response only. The generated wrapper
379
+ closes entered responses even when validation or reading is cancelled, and
346
380
  treats its injected HTTP client as borrowed, so closing the wrapper does not
347
381
  close a shared application transport:
348
382
 
@@ -359,10 +393,14 @@ await http_client.aclose()
359
393
  ```
360
394
 
361
395
  All operation failures derive from `ClientError`. `TransportError` retains its
362
- original `cause`, `ResponseValidationError` retains both the response and cause,
396
+ original `cause`, including failures during binary iteration.
397
+ `ResponseValidationError` retains both the response and cause,
363
398
  and `ApiError` exposes a parsed `detail` plus convenient `code` and `message`
364
399
  properties. `body` and `parsed_body` remain compatibility aliases for
365
400
  `raw_body` and `detail`.
401
+ When an error declares headers, `detail` retains its typed result wrapper and
402
+ header fields; `code`/`message` read its typed payload. Arbitrary user payloads
403
+ with a property named `body` are not unwrapped.
366
404
 
367
405
  ### Generated models
368
406
 
@@ -530,6 +568,57 @@ client-check:
530
568
 
531
569
  ## Supported scope
532
570
 
571
+ HTTP header names merge case-insensitively. Configured values override generated
572
+ defaults; operation header parameters override configured values; applied
573
+ security credentials take precedence over both. Each logical header is sent once.
574
+
575
+ Recursive array and `anyOf` aliases use lazy Python 3.12+ type aliases, including
576
+ mutual recursion. Recursive nondiscriminated `oneOf` and reference-only aliases are rejected with
577
+ an actionable error; ordinary aliases and recursive object models stay readable.
578
+
579
+ Exact `oneOf` membership uses JSON types without string/number/boolean coercion.
580
+ Formatted date/UUID strings remain valid, and mathematically integral JSON
581
+ numbers count as integers. Zero or multiple matching branches are rejected.
582
+
583
+ Query names beginning with `_` or `model_` receive a public `field`/`field_`
584
+ prefix in generated signatures and query models. Their wire names and
585
+ validation constraints remain intact.
586
+
587
+ Leading-underscore schema properties also receive a `field` prefix (for example,
588
+ `_id` becomes `field_id`) and keep an explicit wire alias. They are validated
589
+ ordinary model fields, including when additional properties are forbidden.
590
+
591
+ Operation names that collide with generated lifecycle methods or streaming
592
+ companions fail generation before writing files; rename the conflicting
593
+ `operationId` in the specification.
594
+
595
+ Local Path Item references are resolved before schema normalization and tag
596
+ selection. Authored siblings replace the corresponding inherited fields;
597
+ omitted siblings preserve inherited parameters and operations. Cycles fail
598
+ generation with a specific error.
599
+
600
+ References to nullable object components accept null in response bodies, request
601
+ arguments, and nested fields while keeping the object model's required fields
602
+ and constraints. OpenAPI 3.0 `nullable` requires an explicit type.
603
+
604
+ URL-encoded forms honor supported per-field style/explode encodings and JSON
605
+ contentType. Object values use JSON by default; explicit form styles can flatten
606
+ simple objects. Unsupported encoding options (including form allowReserved)
607
+ fail generation.
608
+
609
+ Multipart object fields and object-array items are JSON parts with aliases and
610
+ the declared `encoding.contentType` (JSON/+json), or application/json by default.
611
+ File parts also honor `encoding.contentType`. Unsupported multipart encoding
612
+ options fail generation explicitly.
613
+
614
+ Dot-valued path identifiers (`.` and `..`) are percent-encoded to preserve the
615
+ route boundary instead of allowing URL dot-segment normalization.
616
+
617
+ Path `simple`, `label`, and `matrix` styles support scalar, flat array, and flat
618
+ object values with their style-specific explode delimiters. Data is escaped
619
+ before joining it, including literal commas, label dots, slashes, and percent
620
+ signs. `allowReserved` applies only to query parameters.
621
+
533
622
  httpxgen targets ordinary OpenAPI 3.0 and 3.1 client specifications, not every
534
623
  JSON Schema feature. It supports JSON/YAML input, local component references,
535
624
  path/query/header/cookie serialization, JSON/form/multipart/binary request
@@ -539,12 +628,41 @@ security schemes, directional request/response models, inline and recursive
539
628
  Pydantic models, enums, nullable values, discriminated unions, and practical
540
629
  `allOf` inheritance.
541
630
 
631
+ `allOf` supports object-model inheritance. Scalar/array intersections and
632
+ nullable or union bases fail generation with an unsupported-composition error.
633
+
634
+ Required object keys without declared property schemas retain presence checks
635
+ and their additional-property validation, including inherited and directional
636
+ models. Their values remain unconstrained when additional properties are allowed.
637
+
542
638
  Unsupported constructs fail generation where possible. Important remaining
543
639
  limitations are external references, callbacks/webhooks, automatic
544
640
  pagination, and a synchronous client.
545
641
 
546
642
  ## Development
547
643
 
644
+ String `format: byte` values use standard Base64 in JSON (OpenAPI 3.0 and the
645
+ legacy format in 3.1). Python bytes hold decoded data; Base64 strings are decoded
646
+ with strict alphabet/padding validation. Invalid response strings raise
647
+ `ResponseValidationError`. This does not change `format: binary`, raw binary
648
+ bodies, or multipart file uploads.
649
+
650
+ Parameter `content` supports one JSON/+json representation for query, header,
651
+ and path positions. Values are validated and serialized as one compact JSON
652
+ value with aliases and formatted fields. Required nullable parameters send JSON
653
+ null; optional `None` omits the parameter. JSON cookie content and combinations
654
+ with schema/style/explode/allowReserved are rejected during generation.
655
+
656
+ Query `allowReserved: true` preserves `:/?@!$'()*,;` and valid percent-encoded
657
+ triples in values. It escapes `[]#&=+`, stray percent signs, and other disallowed
658
+ characters to preserve query boundaries and form decoding. Parameter names,
659
+ ordinary parameters, and query API keys retain normal escaping.
660
+
661
+ Regeneration removes obsolete modules carrying the generator header inside the
662
+ output package; `--check` reports them without changing files. Handwritten files
663
+ and unmanaged `__init__.py`/`py.typed` files are preserved. Linked output paths
664
+ are rejected instead of writing outside the package.
665
+
548
666
  ```sh
549
667
  uv sync --all-groups
550
668
  uv run pytest
@@ -552,6 +670,14 @@ uv run ruff check . # lint
552
670
  uv run ruff format . # format
553
671
  ```
554
672
 
673
+ CI also checks generated transport/operation typing with Mypy against HTTPX and
674
+ HTTPX2. Run that check locally with:
675
+
676
+ ```sh
677
+ uv run --frozen --with mypy==2.4.0 --with httpx2==2.13.1 \
678
+ pytest --no-cov tests/generator/test_typing.py
679
+ ```
680
+
555
681
  `preview/` is generated output, checked in so the effect of a change is visible
556
682
  in review. Regenerate it whenever generated code changes — CI fails if it is
557
683
  stale: