httpxgen 0.2.0__tar.gz → 0.4.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.
- {httpxgen-0.2.0 → httpxgen-0.4.0}/.github/workflows/ci.yml +7 -3
- httpxgen-0.4.0/.github/workflows/release.yml +135 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/PKG-INFO +187 -16
- {httpxgen-0.2.0 → httpxgen-0.4.0}/README.md +186 -15
- {httpxgen-0.2.0 → httpxgen-0.4.0}/VISION.md +1 -1
- httpxgen-0.4.0/changelog.md +324 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/cli.py +94 -94
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/__init__.py +4 -4
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/build.py +46 -24
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/client.py +1057 -894
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/models.py +630 -493
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/naming.py +57 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/normalize.py +160 -104
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/operations.py +863 -579
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/package.py +29 -5
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/schema.py +108 -24
- httpxgen-0.4.0/httpxgen/generator/templates/binary_operation.jinja2 +51 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/client.jinja2 +1 -1
- httpxgen-0.2.0/httpxgen/generator/templates/exceptions.jinja2 → httpxgen-0.4.0/httpxgen/generator/templates/errors.jinja2 +8 -3
- httpxgen-0.4.0/httpxgen/generator/templates/models.jinja2 +37 -0
- httpxgen-0.4.0/httpxgen/generator/templates/serialization.jinja2 +329 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/shared_init.jinja2 +18 -3
- httpxgen-0.4.0/httpxgen/generator/templates/transport.jinja2 +135 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates.py +3 -4
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/openapi.py +19 -2
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/output.py +110 -85
- httpxgen-0.4.0/httpxgen/references.py +162 -0
- httpxgen-0.4.0/httpxgen/selection.py +196 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/__init__.py +93 -93
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/invoices/__init__.py +22 -22
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/invoices/client.py +270 -264
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/invoices/models.py +59 -61
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/payments/__init__.py +62 -62
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/payments/client.py +675 -646
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/payments/models.py +244 -232
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/shared/__init__.py +71 -56
- httpxgen-0.2.0/preview/shared/exceptions.py → httpxgen-0.4.0/preview/shared/errors.py +84 -79
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/shared/models.py +72 -41
- httpxgen-0.4.0/preview/shared/serialization.py +336 -0
- httpxgen-0.4.0/preview/shared/transport.py +141 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/pyproject.toml +66 -66
- {httpxgen-0.2.0 → httpxgen-0.4.0}/specs/api.yml +753 -753
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/conftest.py +25 -25
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_build.py +399 -377
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_client.py +265 -267
- httpxgen-0.4.0/tests/generator/test_cookies.py +161 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_improvements.py +26 -15
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_models.py +203 -190
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_normalize.py +425 -427
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_package.py +61 -61
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_runtime.py +597 -597
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_schema.py +2 -0
- httpxgen-0.4.0/tests/generator/test_tickets.py +2634 -0
- httpxgen-0.4.0/tests/generator/test_typing.py +116 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/test_cli.py +75 -75
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/test_openapi.py +38 -31
- httpxgen-0.4.0/tests/test_openapi_32.py +286 -0
- httpxgen-0.4.0/tests/test_output.py +171 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/test_selection.py +136 -136
- {httpxgen-0.2.0 → httpxgen-0.4.0}/uv.lock +1 -1
- httpxgen-0.2.0/httpxgen/generator/templates/binary_operation.jinja2 +0 -51
- httpxgen-0.2.0/httpxgen/generator/templates/http_methods.jinja2 +0 -12
- httpxgen-0.2.0/httpxgen/generator/templates/models.jinja2 +0 -6
- httpxgen-0.2.0/httpxgen/generator/templates/serialization.jinja2 +0 -130
- httpxgen-0.2.0/httpxgen/generator/templates/transport.jinja2 +0 -112
- httpxgen-0.2.0/httpxgen/selection.py +0 -162
- httpxgen-0.2.0/preview/shared/http_methods.py +0 -17
- httpxgen-0.2.0/preview/shared/serialization.py +0 -137
- httpxgen-0.2.0/preview/shared/transport.py +0 -109
- httpxgen-0.2.0/tests/test_output.py +0 -99
- {httpxgen-0.2.0 → httpxgen-0.4.0}/.gitignore +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/.python-version +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/CLAUDE.md +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/IMPROVEMENT_SPEC.md +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/LICENSE +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/__init__.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/errors.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/enum.jinja2 +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/generated_header.jinja2 +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/model.jinja2 +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/operation.jinja2 +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/generator/templates/package_init.jinja2 +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/io.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/httpxgen/loading.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/preview/py.typed +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/scripts/generate-preview.sh +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_naming.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/generator/test_operations.py +0 -0
- {httpxgen-0.2.0 → httpxgen-0.4.0}/tests/test_loading.py +0 -0
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
python-version: "3.14"
|
|
18
18
|
|
|
19
19
|
- name: Install dependencies
|
|
20
|
-
run: uv sync --all-groups
|
|
20
|
+
run: uv sync --locked --all-groups
|
|
21
21
|
|
|
22
22
|
- name: Ruff lint
|
|
23
23
|
run: uv run ruff check --output-format=github .
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
python-version: "3.14"
|
|
37
37
|
|
|
38
38
|
- name: Install dependencies
|
|
39
|
-
run: uv sync --all-groups
|
|
39
|
+
run: uv sync --locked --all-groups
|
|
40
40
|
|
|
41
41
|
- name: Regenerate the preview client
|
|
42
42
|
run: bash scripts/generate-preview.sh
|
|
@@ -58,7 +58,11 @@ jobs:
|
|
|
58
58
|
python-version: ${{ matrix.python-version }}
|
|
59
59
|
|
|
60
60
|
- name: Install dependencies
|
|
61
|
-
run: uv sync --all-groups
|
|
61
|
+
run: uv sync --locked --all-groups
|
|
62
62
|
|
|
63
63
|
- name: Run tests
|
|
64
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
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: release-${{ github.event.release.tag_name || github.head_ref || github.ref }}
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
name: Build and verify distributions
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v4
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.14"
|
|
29
|
+
|
|
30
|
+
- name: Validate release tag and source
|
|
31
|
+
if: github.event_name == 'release'
|
|
32
|
+
env:
|
|
33
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
34
|
+
run: |
|
|
35
|
+
python - <<'PY'
|
|
36
|
+
import os
|
|
37
|
+
import tomllib
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
41
|
+
expected = f"v{version}"
|
|
42
|
+
if os.environ["RELEASE_TAG"] != expected:
|
|
43
|
+
raise SystemExit(f"Release tag must be {expected}")
|
|
44
|
+
PY
|
|
45
|
+
git merge-base --is-ancestor HEAD origin/main
|
|
46
|
+
|
|
47
|
+
- name: Install locked dependencies
|
|
48
|
+
run: uv sync --locked --all-groups
|
|
49
|
+
|
|
50
|
+
- name: Lint and format
|
|
51
|
+
run: |
|
|
52
|
+
uv run ruff check --output-format=github .
|
|
53
|
+
uv run ruff format --check .
|
|
54
|
+
|
|
55
|
+
- name: Run tests
|
|
56
|
+
run: uv run pytest
|
|
57
|
+
|
|
58
|
+
- name: Type-check generated protocols
|
|
59
|
+
run: uv run --frozen --with mypy==2.4.0 --with httpx2==2.13.1 pytest --no-cov tests/generator/test_typing.py
|
|
60
|
+
|
|
61
|
+
- name: Verify checked-in preview
|
|
62
|
+
run: |
|
|
63
|
+
bash scripts/generate-preview.sh
|
|
64
|
+
git diff --exit-code -- preview
|
|
65
|
+
|
|
66
|
+
- name: Build wheel and source distribution
|
|
67
|
+
run: uv build --no-sources
|
|
68
|
+
|
|
69
|
+
- name: Check distribution metadata
|
|
70
|
+
run: uvx --from twine twine check --strict dist/*
|
|
71
|
+
|
|
72
|
+
- name: Smoke-test the installed wheel outside the checkout
|
|
73
|
+
run: |
|
|
74
|
+
wheel="$(realpath dist/*.whl)"
|
|
75
|
+
cd "$RUNNER_TEMP"
|
|
76
|
+
uv run --isolated --no-project --with "$wheel" httpxgen --help
|
|
77
|
+
uv run --isolated --no-project --with "$wheel" httpxgen \
|
|
78
|
+
"$GITHUB_WORKSPACE/specs/api.yml" "$RUNNER_TEMP/release_smoke" \
|
|
79
|
+
--package-name release_smoke --tag payments --tag invoices
|
|
80
|
+
uv run --isolated --no-project --with "$wheel" httpxgen \
|
|
81
|
+
"$GITHUB_WORKSPACE/specs/api.yml" "$RUNNER_TEMP/release_smoke" \
|
|
82
|
+
--package-name release_smoke --tag payments --tag invoices --check
|
|
83
|
+
|
|
84
|
+
- name: Store verified distributions
|
|
85
|
+
uses: actions/upload-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: distributions
|
|
88
|
+
path: |
|
|
89
|
+
dist/*.whl
|
|
90
|
+
dist/*.tar.gz
|
|
91
|
+
if-no-files-found: error
|
|
92
|
+
|
|
93
|
+
publish:
|
|
94
|
+
name: Publish to PyPI
|
|
95
|
+
if: github.event_name == 'release'
|
|
96
|
+
needs: build
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
environment:
|
|
99
|
+
name: pypi
|
|
100
|
+
url: https://pypi.org/project/httpxgen/
|
|
101
|
+
permissions:
|
|
102
|
+
id-token: write
|
|
103
|
+
steps:
|
|
104
|
+
- name: Download verified distributions
|
|
105
|
+
uses: actions/download-artifact@v4
|
|
106
|
+
with:
|
|
107
|
+
name: distributions
|
|
108
|
+
path: dist
|
|
109
|
+
|
|
110
|
+
- name: Publish with Trusted Publishing
|
|
111
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
112
|
+
|
|
113
|
+
release-assets:
|
|
114
|
+
name: Attach published distributions to GitHub Release
|
|
115
|
+
if: github.event_name == 'release'
|
|
116
|
+
needs: publish
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
permissions:
|
|
119
|
+
contents: write
|
|
120
|
+
steps:
|
|
121
|
+
- name: Download published distributions
|
|
122
|
+
uses: actions/download-artifact@v4
|
|
123
|
+
with:
|
|
124
|
+
name: distributions
|
|
125
|
+
path: dist
|
|
126
|
+
|
|
127
|
+
- name: Calculate checksums
|
|
128
|
+
working-directory: dist
|
|
129
|
+
run: sha256sum -- *.whl *.tar.gz > SHA256SUMS
|
|
130
|
+
|
|
131
|
+
- name: Upload release assets
|
|
132
|
+
env:
|
|
133
|
+
GH_TOKEN: ${{ github.token }}
|
|
134
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
135
|
+
run: gh release upload "$RELEASE_TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: httpxgen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Generate a typed async httpx client from an OpenAPI document.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -38,6 +38,7 @@ the client, check it into your repository, review it, debug it, and ship it.
|
|
|
38
38
|
- [Automation and CI](#automation-and-ci)
|
|
39
39
|
- [Supported scope](#supported-scope)
|
|
40
40
|
- [Development](#development)
|
|
41
|
+
- [Releasing](#releasing)
|
|
41
42
|
- [License](#license)
|
|
42
43
|
|
|
43
44
|
## Why httpxgen?
|
|
@@ -64,11 +65,10 @@ A regular specification becomes a self-contained Python package:
|
|
|
64
65
|
openapi.json ──▶ httpxgen ──▶ payments/
|
|
65
66
|
├── __init__.py
|
|
66
67
|
├── client.py # the public client class
|
|
67
|
-
├── models.py
|
|
68
|
-
├──
|
|
69
|
-
├── http_methods.py # HttpMethods
|
|
68
|
+
├── models.py # API models, HttpMethods, HttpResponse
|
|
69
|
+
├── errors.py # generated client errors
|
|
70
70
|
├── serialization.py # parameter and auth helpers
|
|
71
|
-
├── transport.py #
|
|
71
|
+
├── transport.py # HTTP client protocol and file types
|
|
72
72
|
└── py.typed
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -79,9 +79,8 @@ models and support modules are generated once beside them:
|
|
|
79
79
|
openapi.json ──▶ httpxgen ──▶ api/
|
|
80
80
|
├── __init__.py # clients, ApiError, all models
|
|
81
81
|
├── shared/ # generated once
|
|
82
|
-
│ ├──
|
|
83
|
-
│ ├──
|
|
84
|
-
│ ├── models.py # only models used by both tags
|
|
82
|
+
│ ├── errors.py
|
|
83
|
+
│ ├── models.py # shared models, HttpMethods, HttpResponse
|
|
85
84
|
│ ├── serialization.py
|
|
86
85
|
│ └── transport.py
|
|
87
86
|
├── payments/
|
|
@@ -93,6 +92,10 @@ openapi.json ──▶ httpxgen ──▶ api/
|
|
|
93
92
|
└── py.typed
|
|
94
93
|
```
|
|
95
94
|
|
|
95
|
+
`HttpMethods` and `HttpResponse` live in `models.py` (`shared/models.py` in a
|
|
96
|
+
workspace). Generated error classes live in `errors.py`. These types are also
|
|
97
|
+
available from the package root.
|
|
98
|
+
|
|
96
99
|
## Install
|
|
97
100
|
|
|
98
101
|
Requires Python 3.12 or newer.
|
|
@@ -200,7 +203,7 @@ imports stay short and stable.
|
|
|
200
203
|
`operationId` becomes an idiomatic snake_case method, optional query parameters are only sent when set, and the response is validated into a model:
|
|
201
204
|
|
|
202
205
|
`client.py` holds nothing but the imports and the client class — the serialization
|
|
203
|
-
helpers live in `serialization.py`, the error type in `
|
|
206
|
+
helpers live in `serialization.py`, the error type in `errors.py`:
|
|
204
207
|
|
|
205
208
|
```python
|
|
206
209
|
class ListChargesParams(BaseModel):
|
|
@@ -291,7 +294,23 @@ Path parameters carry their spec format — `format: uuid` becomes `UUID`, not `
|
|
|
291
294
|
raise ApiError(response.status_code, response.text, response=response)
|
|
292
295
|
```
|
|
293
296
|
|
|
294
|
-
Request bodies are a single typed `body` argument, serialized by alias
|
|
297
|
+
Request bodies are a single typed `body` argument, serialized by alias.
|
|
298
|
+
JSON inputs are validated using the declared Pydantic schema before the HTTP
|
|
299
|
+
transport runs, including mappings and model instances changed after creation.
|
|
300
|
+
Invalid inputs raise `RequestValidationError`; Pydantic's normal accepted
|
|
301
|
+
conversions apply before serialization.
|
|
302
|
+
Unsupported runtime `content_type` choices also raise `RequestValidationError`
|
|
303
|
+
before sending a request, with the declared alternatives listed.
|
|
304
|
+
Explicitly supplied nullable fields are sent as JSON null; unset nullable fields
|
|
305
|
+
are omitted, including inside nested models:
|
|
306
|
+
|
|
307
|
+
For `application/merge-patch+json`, all unset model fields are omitted, including
|
|
308
|
+
schema defaults. Explicit values equal to a default are still sent. Ordinary
|
|
309
|
+
JSON create requests continue to include non-null defaults.
|
|
310
|
+
|
|
311
|
+
Omit an optional JSON body by leaving out `body`. Passing `body=None` sends the
|
|
312
|
+
literal JSON payload `null` when the schema permits null, with the selected JSON
|
|
313
|
+
Content-Type. Omission uses a separate generated sentinel.
|
|
295
314
|
|
|
296
315
|
```python
|
|
297
316
|
async def create_charge(
|
|
@@ -301,9 +320,7 @@ Request bodies are a single typed `body` argument, serialized by alias and witho
|
|
|
301
320
|
timeout: float | None = None,
|
|
302
321
|
) -> Charge:
|
|
303
322
|
...
|
|
304
|
-
json_body =
|
|
305
|
-
body, mode="json", by_alias=True, exclude_none=True
|
|
306
|
-
)
|
|
323
|
+
json_body = serialize_json_body(body, CreateChargeRequest)
|
|
307
324
|
...
|
|
308
325
|
```
|
|
309
326
|
|
|
@@ -311,10 +328,19 @@ The same direct shape is used for JSON, form, text, and small binary bodies.
|
|
|
311
328
|
An `application/octet-stream` body accepts `bytes`, `Iterable[bytes]`, or
|
|
312
329
|
`AsyncIterable[bytes]` and is passed directly as `content=`.
|
|
313
330
|
|
|
331
|
+
Synchronous binary iterables are adapted lazily to async chunks. Exceptions
|
|
332
|
+
raised while consuming an upload iterable are reported as `TransportError`.
|
|
333
|
+
|
|
314
334
|
Multipart fields are flattened into method arguments. Binary arrays become
|
|
315
|
-
repeated parts with the same wire name
|
|
335
|
+
repeated parts with the same wire name. Parameter and multipart argument names
|
|
336
|
+
that collide with generated locals (such as `path`, `headers`, or `files`) gain
|
|
337
|
+
an `_arg` suffix; their wire names are preserved. `UploadFile` carries optional
|
|
316
338
|
per-part metadata without reading an open file eagerly:
|
|
317
339
|
|
|
340
|
+
Bytes and default `UploadFile` inputs use the filename `upload`; bare handles
|
|
341
|
+
use their basename when available. Set `UploadFile(filename=None, ...)`
|
|
342
|
+
explicitly to send a filename-less part.
|
|
343
|
+
|
|
318
344
|
```python
|
|
319
345
|
await client.upload_documents(
|
|
320
346
|
documents=[
|
|
@@ -332,6 +358,16 @@ call sites to pass fields directly.
|
|
|
332
358
|
Successful binary responses also get a streaming context-manager variant. The
|
|
333
359
|
buffered method remains available for small responses:
|
|
334
360
|
|
|
361
|
+
For endpoints with mixed success representations, buffered methods return the
|
|
362
|
+
declared bytes/model/text/None union. Streaming variants yield `BinaryStream`
|
|
363
|
+
for binary content and the parsed value for JSON, text, or empty successes.
|
|
364
|
+
|
|
365
|
+
Binary successes with declared headers return a typed result whose `body` is
|
|
366
|
+
bytes and whose fields contain validated headers. Their streaming variants
|
|
367
|
+
yield a corresponding `...Result...Stream` model with `body: BinaryStream` and
|
|
368
|
+
the same header fields, validated before iteration. Endpoints without declared
|
|
369
|
+
headers retain the direct bytes/`BinaryStream` API.
|
|
370
|
+
|
|
335
371
|
```python
|
|
336
372
|
content = await client.download_recording(recording_id)
|
|
337
373
|
|
|
@@ -343,6 +379,7 @@ print(recording.media_type, recording.content_length, recording.filename)
|
|
|
343
379
|
```
|
|
344
380
|
|
|
345
381
|
The streaming context owns the individual response only. The generated wrapper
|
|
382
|
+
closes entered responses even when validation or reading is cancelled, and
|
|
346
383
|
treats its injected HTTP client as borrowed, so closing the wrapper does not
|
|
347
384
|
close a shared application transport:
|
|
348
385
|
|
|
@@ -359,10 +396,14 @@ await http_client.aclose()
|
|
|
359
396
|
```
|
|
360
397
|
|
|
361
398
|
All operation failures derive from `ClientError`. `TransportError` retains its
|
|
362
|
-
original `cause`,
|
|
399
|
+
original `cause`, including failures during binary iteration.
|
|
400
|
+
`ResponseValidationError` retains both the response and cause,
|
|
363
401
|
and `ApiError` exposes a parsed `detail` plus convenient `code` and `message`
|
|
364
402
|
properties. `body` and `parsed_body` remain compatibility aliases for
|
|
365
403
|
`raw_body` and `detail`.
|
|
404
|
+
When an error declares headers, `detail` retains its typed result wrapper and
|
|
405
|
+
header fields; `code`/`message` read its typed payload. Arbitrary user payloads
|
|
406
|
+
with a property named `body` are not unwrapped.
|
|
366
407
|
|
|
367
408
|
### Generated models
|
|
368
409
|
|
|
@@ -530,7 +571,58 @@ client-check:
|
|
|
530
571
|
|
|
531
572
|
## Supported scope
|
|
532
573
|
|
|
533
|
-
|
|
574
|
+
HTTP header names merge case-insensitively. Configured values override generated
|
|
575
|
+
defaults; operation header parameters override configured values; applied
|
|
576
|
+
security credentials take precedence over both. Each logical header is sent once.
|
|
577
|
+
|
|
578
|
+
Recursive array and `anyOf` aliases use lazy Python 3.12+ type aliases, including
|
|
579
|
+
mutual recursion. Recursive nondiscriminated `oneOf` and reference-only aliases are rejected with
|
|
580
|
+
an actionable error; ordinary aliases and recursive object models stay readable.
|
|
581
|
+
|
|
582
|
+
Exact `oneOf` membership uses JSON types without string/number/boolean coercion.
|
|
583
|
+
Formatted date/UUID strings remain valid, and mathematically integral JSON
|
|
584
|
+
numbers count as integers. Zero or multiple matching branches are rejected.
|
|
585
|
+
|
|
586
|
+
Query names beginning with `_` or `model_` receive a public `field`/`field_`
|
|
587
|
+
prefix in generated signatures and query models. Their wire names and
|
|
588
|
+
validation constraints remain intact.
|
|
589
|
+
|
|
590
|
+
Leading-underscore schema properties also receive a `field` prefix (for example,
|
|
591
|
+
`_id` becomes `field_id`) and keep an explicit wire alias. They are validated
|
|
592
|
+
ordinary model fields, including when additional properties are forbidden.
|
|
593
|
+
|
|
594
|
+
Operation names that collide with generated lifecycle methods or streaming
|
|
595
|
+
companions fail generation before writing files; rename the conflicting
|
|
596
|
+
`operationId` in the specification.
|
|
597
|
+
|
|
598
|
+
Local Path Item references are resolved before schema normalization and tag
|
|
599
|
+
selection. Authored siblings replace the corresponding inherited fields;
|
|
600
|
+
omitted siblings preserve inherited parameters and operations. Cycles fail
|
|
601
|
+
generation with a specific error.
|
|
602
|
+
|
|
603
|
+
References to nullable object components accept null in response bodies, request
|
|
604
|
+
arguments, and nested fields while keeping the object model's required fields
|
|
605
|
+
and constraints. OpenAPI 3.0 `nullable` requires an explicit type.
|
|
606
|
+
|
|
607
|
+
URL-encoded forms honor supported per-field style/explode encodings and JSON
|
|
608
|
+
contentType. Object values use JSON by default; explicit form styles can flatten
|
|
609
|
+
simple objects. Unsupported encoding options (including form allowReserved)
|
|
610
|
+
fail generation.
|
|
611
|
+
|
|
612
|
+
Multipart object fields and object-array items are JSON parts with aliases and
|
|
613
|
+
the declared `encoding.contentType` (JSON/+json), or application/json by default.
|
|
614
|
+
File parts also honor `encoding.contentType`. Unsupported multipart encoding
|
|
615
|
+
options fail generation explicitly.
|
|
616
|
+
|
|
617
|
+
Dot-valued path identifiers (`.` and `..`) are percent-encoded to preserve the
|
|
618
|
+
route boundary instead of allowing URL dot-segment normalization.
|
|
619
|
+
|
|
620
|
+
Path `simple`, `label`, and `matrix` styles support scalar, flat array, and flat
|
|
621
|
+
object values with their style-specific explode delimiters. Data is escaped
|
|
622
|
+
before joining it, including literal commas, label dots, slashes, and percent
|
|
623
|
+
signs. `allowReserved` applies only to query parameters.
|
|
624
|
+
|
|
625
|
+
httpxgen targets ordinary OpenAPI 3.0, 3.1 and 3.2 client specifications, not every
|
|
534
626
|
JSON Schema feature. It supports JSON/YAML input, local component references,
|
|
535
627
|
path/query/header/cookie serialization, JSON/form/multipart/binary request
|
|
536
628
|
bodies, JSON/text/binary responses, numeric/default/status-range responses,
|
|
@@ -539,12 +631,56 @@ security schemes, directional request/response models, inline and recursive
|
|
|
539
631
|
Pydantic models, enums, nullable values, discriminated unions, and practical
|
|
540
632
|
`allOf` inheritance.
|
|
541
633
|
|
|
634
|
+
`allOf` supports object-model inheritance. Scalar/array intersections and
|
|
635
|
+
nullable or union bases fail generation with an unsupported-composition error.
|
|
636
|
+
|
|
637
|
+
Required object keys without declared property schemas retain presence checks
|
|
638
|
+
and their additional-property validation, including inherited and directional
|
|
639
|
+
models. Their values remain unconstrained when additional properties are allowed.
|
|
640
|
+
|
|
641
|
+
OpenAPI 3.2 documents are accepted with the same feature set as 3.1, plus
|
|
642
|
+
reusable `components.mediaTypes` references. Streaming media types described
|
|
643
|
+
through `itemSchema` (for example Server-Sent Events or JSON Lines), `QUERY`
|
|
644
|
+
operations, `additionalOperations`, and `querystring` parameters are not
|
|
645
|
+
generated and fail with an explicit error. Mixed REST/streaming documents can
|
|
646
|
+
still produce a REST client: select the REST operations with `--tag` and keep
|
|
647
|
+
the event models with `--schema-tag`. Model selection follows `itemSchema` and
|
|
648
|
+
`contentSchema`, so a payload such as
|
|
649
|
+
`itemSchema.properties.data.contentSchema: {$ref: ...}` keeps its referenced
|
|
650
|
+
models, including nested and recursive references:
|
|
651
|
+
|
|
652
|
+
```sh
|
|
653
|
+
httpxgen openapi.json src/api --tag rest --schema-tag sse
|
|
654
|
+
```
|
|
655
|
+
|
|
542
656
|
Unsupported constructs fail generation where possible. Important remaining
|
|
543
657
|
limitations are external references, callbacks/webhooks, automatic
|
|
544
658
|
pagination, and a synchronous client.
|
|
545
659
|
|
|
546
660
|
## Development
|
|
547
661
|
|
|
662
|
+
String `format: byte` values use standard Base64 in JSON (OpenAPI 3.0 and the
|
|
663
|
+
legacy format in 3.1). Python bytes hold decoded data; Base64 strings are decoded
|
|
664
|
+
with strict alphabet/padding validation. Invalid response strings raise
|
|
665
|
+
`ResponseValidationError`. This does not change `format: binary`, raw binary
|
|
666
|
+
bodies, or multipart file uploads.
|
|
667
|
+
|
|
668
|
+
Parameter `content` supports one JSON/+json representation for query, header,
|
|
669
|
+
and path positions. Values are validated and serialized as one compact JSON
|
|
670
|
+
value with aliases and formatted fields. Required nullable parameters send JSON
|
|
671
|
+
null; optional `None` omits the parameter. JSON cookie content and combinations
|
|
672
|
+
with schema/style/explode/allowReserved are rejected during generation.
|
|
673
|
+
|
|
674
|
+
Query `allowReserved: true` preserves `:/?@!$'()*,;` and valid percent-encoded
|
|
675
|
+
triples in values. It escapes `[]#&=+`, stray percent signs, and other disallowed
|
|
676
|
+
characters to preserve query boundaries and form decoding. Parameter names,
|
|
677
|
+
ordinary parameters, and query API keys retain normal escaping.
|
|
678
|
+
|
|
679
|
+
Regeneration removes obsolete modules carrying the generator header inside the
|
|
680
|
+
output package; `--check` reports them without changing files. Handwritten files
|
|
681
|
+
and unmanaged `__init__.py`/`py.typed` files are preserved. Linked output paths
|
|
682
|
+
are rejected instead of writing outside the package.
|
|
683
|
+
|
|
548
684
|
```sh
|
|
549
685
|
uv sync --all-groups
|
|
550
686
|
uv run pytest
|
|
@@ -552,6 +688,14 @@ uv run ruff check . # lint
|
|
|
552
688
|
uv run ruff format . # format
|
|
553
689
|
```
|
|
554
690
|
|
|
691
|
+
CI also checks generated transport/operation typing with Mypy against HTTPX and
|
|
692
|
+
HTTPX2. Run that check locally with:
|
|
693
|
+
|
|
694
|
+
```sh
|
|
695
|
+
uv run --frozen --with mypy==2.4.0 --with httpx2==2.13.1 \
|
|
696
|
+
pytest --no-cov tests/generator/test_typing.py
|
|
697
|
+
```
|
|
698
|
+
|
|
555
699
|
`preview/` is generated output, checked in so the effect of a change is visible
|
|
556
700
|
in review. Regenerate it whenever generated code changes — CI fails if it is
|
|
557
701
|
stale:
|
|
@@ -560,6 +704,33 @@ stale:
|
|
|
560
704
|
./scripts/generate-preview.sh
|
|
561
705
|
```
|
|
562
706
|
|
|
707
|
+
## Releasing
|
|
708
|
+
|
|
709
|
+
Publishing a GitHub Release runs `.github/workflows/release.yml`, which builds
|
|
710
|
+
and verifies the wheel and source distribution, publishes them to PyPI through
|
|
711
|
+
Trusted Publishing, and attaches the same files plus `SHA256SUMS` to the release.
|
|
712
|
+
Pull requests run the build and verification steps as a publishing dry run.
|
|
713
|
+
|
|
714
|
+
1. Update `project.version` in `pyproject.toml`, run `uv lock` to synchronize
|
|
715
|
+
`uv.lock`, and finalize the version's changelog and migration notes.
|
|
716
|
+
2. Merge the release preparation PR into `main` and wait for green CI.
|
|
717
|
+
3. Create a GitHub Release with tag `v<project.version>` targeting that merged
|
|
718
|
+
commit. A draft does not publish anything; choose **Publish release** to
|
|
719
|
+
start the workflow.
|
|
720
|
+
4. Wait for the **Release** workflow to finish and verify the new version on
|
|
721
|
+
[PyPI](https://pypi.org/project/httpxgen/).
|
|
722
|
+
|
|
723
|
+
The workflow rejects tags that do not match `project.version` or point to a
|
|
724
|
+
commit outside `main`. The PyPI Trusted Publisher must name repository
|
|
725
|
+
`mathisarends/httpxgen`, workflow `release.yml`, and environment `pypi`.
|
|
726
|
+
The publishing job uses that GitHub environment and `id-token: write`; no PyPI
|
|
727
|
+
API token is required. Any configured environment approval must be completed
|
|
728
|
+
before the publishing job can run.
|
|
729
|
+
|
|
730
|
+
The tagged commit must contain the release workflow. For the first automated
|
|
731
|
+
release, merge this workflow before creating the tag. If reusing an existing
|
|
732
|
+
draft, update its target commit and release notes before publishing it.
|
|
733
|
+
|
|
563
734
|
## License
|
|
564
735
|
|
|
565
736
|
MIT
|