httpxgen 0.1.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.1.0/.github/workflows/ci.yml +64 -0
- httpxgen-0.1.0/.gitignore +39 -0
- httpxgen-0.1.0/.python-version +1 -0
- httpxgen-0.1.0/CLAUDE.md +6 -0
- httpxgen-0.1.0/LICENSE +21 -0
- httpxgen-0.1.0/PKG-INFO +445 -0
- httpxgen-0.1.0/README.md +433 -0
- httpxgen-0.1.0/VISION.md +47 -0
- httpxgen-0.1.0/httpxgen/__init__.py +12 -0
- httpxgen-0.1.0/httpxgen/cli.py +78 -0
- httpxgen-0.1.0/httpxgen/generator/__init__.py +4 -0
- httpxgen-0.1.0/httpxgen/generator/build.py +162 -0
- httpxgen-0.1.0/httpxgen/generator/client.py +729 -0
- httpxgen-0.1.0/httpxgen/generator/errors.py +2 -0
- httpxgen-0.1.0/httpxgen/generator/models.py +493 -0
- httpxgen-0.1.0/httpxgen/generator/naming.py +40 -0
- httpxgen-0.1.0/httpxgen/generator/normalize.py +665 -0
- httpxgen-0.1.0/httpxgen/generator/operations.py +599 -0
- httpxgen-0.1.0/httpxgen/generator/package.py +202 -0
- httpxgen-0.1.0/httpxgen/generator/schema.py +142 -0
- httpxgen-0.1.0/httpxgen/generator/templates.py +332 -0
- httpxgen-0.1.0/httpxgen/io.py +5 -0
- httpxgen-0.1.0/httpxgen/loading.py +31 -0
- httpxgen-0.1.0/httpxgen/openapi.py +117 -0
- httpxgen-0.1.0/httpxgen/output.py +66 -0
- httpxgen-0.1.0/httpxgen/selection.py +162 -0
- httpxgen-0.1.0/preview/__init__.py +84 -0
- httpxgen-0.1.0/preview/invoices/__init__.py +22 -0
- httpxgen-0.1.0/preview/invoices/client.py +184 -0
- httpxgen-0.1.0/preview/invoices/models.py +61 -0
- httpxgen-0.1.0/preview/payments/__init__.py +62 -0
- httpxgen-0.1.0/preview/payments/client.py +432 -0
- httpxgen-0.1.0/preview/payments/models.py +232 -0
- httpxgen-0.1.0/preview/py.typed +0 -0
- httpxgen-0.1.0/preview/shared/__init__.py +28 -0
- httpxgen-0.1.0/preview/shared/exceptions.py +20 -0
- httpxgen-0.1.0/preview/shared/http_methods.py +17 -0
- httpxgen-0.1.0/preview/shared/models.py +41 -0
- httpxgen-0.1.0/preview/shared/serialization.py +137 -0
- httpxgen-0.1.0/pyproject.toml +65 -0
- httpxgen-0.1.0/scripts/generate-preview.sh +10 -0
- httpxgen-0.1.0/specs/api.yml +753 -0
- httpxgen-0.1.0/tests/conftest.py +25 -0
- httpxgen-0.1.0/tests/generator/test_build.py +352 -0
- httpxgen-0.1.0/tests/generator/test_client.py +265 -0
- httpxgen-0.1.0/tests/generator/test_models.py +190 -0
- httpxgen-0.1.0/tests/generator/test_naming.py +38 -0
- httpxgen-0.1.0/tests/generator/test_normalize.py +427 -0
- httpxgen-0.1.0/tests/generator/test_operations.py +406 -0
- httpxgen-0.1.0/tests/generator/test_package.py +62 -0
- httpxgen-0.1.0/tests/generator/test_runtime.py +600 -0
- httpxgen-0.1.0/tests/generator/test_schema.py +92 -0
- httpxgen-0.1.0/tests/test_cli.py +51 -0
- httpxgen-0.1.0/tests/test_loading.py +39 -0
- httpxgen-0.1.0/tests/test_openapi.py +31 -0
- httpxgen-0.1.0/tests/test_output.py +98 -0
- httpxgen-0.1.0/tests/test_selection.py +136 -0
- httpxgen-0.1.0/uv.lock +546 -0
|
@@ -0,0 +1,64 @@
|
|
|
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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# uv
|
|
18
|
+
.uv/
|
|
19
|
+
|
|
20
|
+
# Testing / coverage
|
|
21
|
+
.tmp/
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
coverage.xml
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
.nox/
|
|
29
|
+
|
|
30
|
+
# Type checkers
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
.pyright/
|
|
33
|
+
.ruff_cache/
|
|
34
|
+
|
|
35
|
+
# Editors / OS
|
|
36
|
+
.vscode/
|
|
37
|
+
.idea/
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
httpxgen-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# httpxgen
|
|
2
|
+
|
|
3
|
+
## Conventions
|
|
4
|
+
|
|
5
|
+
- In `__init__.py` files, use relative imports (e.g. `from .generator import ...`). Everywhere else, use absolute imports (`from httpxgen.generator import ...`).
|
|
6
|
+
- No blanket module-level docstrings/comments at the top of files (e.g. `"""Renders a typed async httpx client..."""`).
|
httpxgen-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mathisarends
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
httpxgen-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: httpxgen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate a typed async httpx client from an OpenAPI document.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# httpxgen
|
|
14
|
+
|
|
15
|
+
Generate a **typed async [httpx](https://www.python-httpx.org/) client** from an OpenAPI document — no runtime layer, no reflection, no magic.
|
|
16
|
+
|
|
17
|
+
httpxgen emits plain Python you could have written by hand: `async def` methods with real parameter names, [Pydantic](https://docs.pydantic.dev/) models for every schema, discriminated unions for `oneOf`, `StrEnum` for enums, and `UUID` / `datetime` where the spec says so. Check the output into your repo, read it, click through it in your editor.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
openapi.json ──▶ httpxgen ──▶ payments/
|
|
21
|
+
├── __init__.py
|
|
22
|
+
├── client.py # the public client class
|
|
23
|
+
├── models.py
|
|
24
|
+
├── exceptions.py # ApiError
|
|
25
|
+
├── http_methods.py # HttpMethods
|
|
26
|
+
├── serialization.py # parameter and auth helpers
|
|
27
|
+
└── py.typed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Split a large document along its tags and the support modules are generated once,
|
|
31
|
+
beside the clients that share them:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
openapi.json ──▶ httpxgen ──▶ api/
|
|
35
|
+
├── __init__.py # clients, ApiError, all models
|
|
36
|
+
├── shared/ # generated once
|
|
37
|
+
│ ├── exceptions.py
|
|
38
|
+
│ ├── http_methods.py
|
|
39
|
+
│ ├── models.py # only models used by both tags
|
|
40
|
+
│ └── serialization.py
|
|
41
|
+
├── payments/
|
|
42
|
+
│ ├── client.py
|
|
43
|
+
│ └── models.py # models only payments uses
|
|
44
|
+
├── invoices/
|
|
45
|
+
│ ├── client.py
|
|
46
|
+
│ └── models.py
|
|
47
|
+
└── py.typed
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
With [uv](https://docs.astral.sh/uv/):
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv tool install httpxgen # standalone CLI
|
|
56
|
+
uv add --dev httpxgen # dev dependency of your project
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
With pip:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
pip install httpxgen
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or run it without installing anything:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
uvx httpxgen openapi.json src/payments
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
httpxgen is only needed at build time — the generated package depends on `httpx` and `pydantic` alone.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
httpxgen openapi.yaml src/payments --package-name payments
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Generated 6 file(s) in package src/payments.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
import httpx
|
|
87
|
+
|
|
88
|
+
from payments import ApiError, CardPaymentMethod, CreateChargeRequest, Money, PaymentsClient
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def main() -> None:
|
|
92
|
+
async with PaymentsClient(
|
|
93
|
+
httpx.AsyncClient(),
|
|
94
|
+
"https://payments.example.com/api",
|
|
95
|
+
credentials={"bearerAuth": "your-token"},
|
|
96
|
+
) as client:
|
|
97
|
+
page = await client.list_charges(status="succeeded", page_size=50)
|
|
98
|
+
for charge in page.items:
|
|
99
|
+
print(charge.id, charge.amount.amount_cents, charge.status)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
charge = await client.create_charge(
|
|
103
|
+
CreateChargeRequest(
|
|
104
|
+
amount=Money(amount_cents=4200, currency="EUR"),
|
|
105
|
+
payment_method=CardPaymentMethod(...),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
except ApiError as error:
|
|
109
|
+
print(error.status_code, error.body)
|
|
110
|
+
if error.parsed_body is not None:
|
|
111
|
+
print(error.parsed_body)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## What the generated code looks like
|
|
118
|
+
|
|
119
|
+
### From this spec
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
/charges:
|
|
123
|
+
get:
|
|
124
|
+
operationId: listCharges
|
|
125
|
+
parameters:
|
|
126
|
+
- name: status
|
|
127
|
+
in: query
|
|
128
|
+
schema: { $ref: "#/components/schemas/ChargeStatus" }
|
|
129
|
+
- name: cursor
|
|
130
|
+
in: query
|
|
131
|
+
schema: { type: string }
|
|
132
|
+
- name: page_size
|
|
133
|
+
in: query
|
|
134
|
+
schema: { type: integer, default: 25, minimum: 1, maximum: 200 }
|
|
135
|
+
responses:
|
|
136
|
+
"200":
|
|
137
|
+
content:
|
|
138
|
+
application/json:
|
|
139
|
+
schema: { $ref: "#/components/schemas/ChargePage" }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### You get this client
|
|
143
|
+
|
|
144
|
+
`operationId` becomes an idiomatic snake_case method, optional query parameters are only sent when set, and the response is validated into a model:
|
|
145
|
+
|
|
146
|
+
`client.py` holds nothing but the imports and the client class — the serialization
|
|
147
|
+
helpers live in `serialization.py`, the error type in `exceptions.py`:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
class ListChargesParams(BaseModel):
|
|
151
|
+
status: ChargeStatus | None = None
|
|
152
|
+
cursor: str | None = None
|
|
153
|
+
page_size: int = Field(25, ge=1, le=200)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PaymentsClient:
|
|
157
|
+
async def list_charges(
|
|
158
|
+
self,
|
|
159
|
+
status: ChargeStatus | None = None,
|
|
160
|
+
cursor: str | None = None,
|
|
161
|
+
page_size: int = 25,
|
|
162
|
+
*,
|
|
163
|
+
timeout: float | None = None,
|
|
164
|
+
) -> ChargePage:
|
|
165
|
+
path = "/charges"
|
|
166
|
+
|
|
167
|
+
params = ListChargesParams(
|
|
168
|
+
status=status,
|
|
169
|
+
cursor=cursor,
|
|
170
|
+
page_size=page_size,
|
|
171
|
+
)
|
|
172
|
+
query: list[tuple[str, str]] = []
|
|
173
|
+
if params.status is not None:
|
|
174
|
+
query.extend(serialize_query("status", params.status))
|
|
175
|
+
if params.cursor is not None:
|
|
176
|
+
query.extend(serialize_query("cursor", params.cursor))
|
|
177
|
+
query.extend(serialize_query("page_size", params.page_size))
|
|
178
|
+
|
|
179
|
+
headers = dict(self._headers)
|
|
180
|
+
headers.setdefault("Accept", "application/json")
|
|
181
|
+
|
|
182
|
+
apply_security(self._credentials, [("bearerAuth",)], headers, query, {})
|
|
183
|
+
|
|
184
|
+
response = await self._client.request(
|
|
185
|
+
method=HttpMethods.GET,
|
|
186
|
+
url=f"{self._base_url}{path}",
|
|
187
|
+
params=query,
|
|
188
|
+
headers=headers,
|
|
189
|
+
timeout=self._timeout if timeout is None else timeout,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if response.status_code == 200:
|
|
193
|
+
return ChargePage.model_validate(response.json())
|
|
194
|
+
|
|
195
|
+
raise ApiError(response.status_code, response.text, response=response)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Path parameters carry their spec format — `format: uuid` becomes `UUID`, not `str`:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
async def get_customer(
|
|
202
|
+
self,
|
|
203
|
+
customer_id: UUID,
|
|
204
|
+
*,
|
|
205
|
+
timeout: float | None = None,
|
|
206
|
+
) -> Customer:
|
|
207
|
+
path = "/customers/{customerId}"
|
|
208
|
+
path = path.replace("{customerId}", serialize_path("customerId", customer_id))
|
|
209
|
+
|
|
210
|
+
headers = dict(self._headers)
|
|
211
|
+
headers.setdefault("Accept", "application/json")
|
|
212
|
+
|
|
213
|
+
response = await self._client.request(
|
|
214
|
+
method=HttpMethods.GET,
|
|
215
|
+
url=f"{self._base_url}{path}",
|
|
216
|
+
headers=headers,
|
|
217
|
+
timeout=self._timeout if timeout is None else timeout,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if response.status_code == 200:
|
|
221
|
+
return Customer.model_validate(response.json())
|
|
222
|
+
if response.status_code == 404:
|
|
223
|
+
parsed_body = ApiErrorModel.model_validate(response.json())
|
|
224
|
+
raise ApiError(response.status_code, response.text, parsed_body, response)
|
|
225
|
+
|
|
226
|
+
raise ApiError(response.status_code, response.text, response=response)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Request bodies are a single typed `body` argument, serialized by alias and without `None` noise:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
async def create_charge(
|
|
233
|
+
self,
|
|
234
|
+
body: CreateChargeRequest,
|
|
235
|
+
*,
|
|
236
|
+
timeout: float | None = None,
|
|
237
|
+
) -> Charge:
|
|
238
|
+
...
|
|
239
|
+
json_body = TypeAdapter(CreateChargeRequest).dump_python(
|
|
240
|
+
body, mode="json", by_alias=True, exclude_none=True
|
|
241
|
+
)
|
|
242
|
+
...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The same direct shape is used for other ordinary body encodings:
|
|
246
|
+
`application/x-www-form-urlencoded` is passed as `data=`, multipart object
|
|
247
|
+
fields are separated into `data=` and `files=`, and binary payloads use
|
|
248
|
+
`content=`. Multipart boundaries remain under `httpx`'s control.
|
|
249
|
+
|
|
250
|
+
The client is an async context manager, so `httpx` connections are closed for you:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
http_client = httpx.AsyncClient()
|
|
254
|
+
async with PaymentsClient(
|
|
255
|
+
http_client,
|
|
256
|
+
"https://payments.example.com/api",
|
|
257
|
+
credentials={"bearerAuth": "your-token"},
|
|
258
|
+
) as client:
|
|
259
|
+
...
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### And these models
|
|
263
|
+
|
|
264
|
+
`allOf` becomes inheritance, `oneOf` + `discriminator` becomes a Pydantic discriminated union, string enums become `StrEnum`, and `minimum` / `maxLength` survive as `Field(...)` constraints:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
class ChargeStatus(StrEnum):
|
|
268
|
+
PENDING = "pending"
|
|
269
|
+
SUCCEEDED = "succeeded"
|
|
270
|
+
FAILED = "failed"
|
|
271
|
+
REFUNDED = "refunded"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class Money(BaseModel):
|
|
275
|
+
amount_cents: int
|
|
276
|
+
currency: str = Field(min_length=3, max_length=3)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class CardPaymentMethod(BaseModel):
|
|
280
|
+
type: Literal[PaymentMethodType.CARD]
|
|
281
|
+
card_number: str
|
|
282
|
+
exp_month: int = Field(ge=1, le=12)
|
|
283
|
+
exp_year: int
|
|
284
|
+
billing_address: Address | None = None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
PaymentMethod = Annotated[
|
|
288
|
+
CardPaymentMethod | BankTransferPaymentMethod,
|
|
289
|
+
Field(discriminator="type"),
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class BaseEntity(BaseModel):
|
|
294
|
+
id: str
|
|
295
|
+
created_at: datetime
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class Charge(BaseEntity): # allOf: BaseEntity + own properties
|
|
299
|
+
amount: Money
|
|
300
|
+
status: ChargeStatus
|
|
301
|
+
payment_method: PaymentMethod # discriminated at parse time
|
|
302
|
+
metadata: dict[str, str] | None = None
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Everything is re-exported from the package root, so consumers import from one place:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from payments import ApiError, Charge, ChargeStatus, Money, PaymentsClient
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## CLI
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
httpxgen OPENAPI OUTPUT [--package-name NAME] [--tag TAG] [--schema-tag TAG] [--check]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| Argument | Meaning |
|
|
318
|
+
| --- | --- |
|
|
319
|
+
| `OPENAPI` | OpenAPI JSON or YAML file |
|
|
320
|
+
| `OUTPUT` | target package directory, or the root holding one package per tag when several `--tag` are given (created if missing) |
|
|
321
|
+
| `--package-name` | import name and client class prefix; defaults to the output directory name |
|
|
322
|
+
| `--tag TAG` | generate only operations carrying this tag; repeat it for one package per tag |
|
|
323
|
+
| `--schema-tag TAG` | keep schemas referenced by this tag without generating its operations; repeatable |
|
|
324
|
+
| `--check` | write nothing; exit non-zero when the checked-in output is stale |
|
|
325
|
+
|
|
326
|
+
Carve a focused client out of a large spec:
|
|
327
|
+
|
|
328
|
+
```sh
|
|
329
|
+
httpxgen openapi.json src/billing \
|
|
330
|
+
--package-name billing \
|
|
331
|
+
--tag charges \
|
|
332
|
+
--schema-tag webhooks
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Repeat `--tag` and you get one client package per tag. `ApiError`, `HttpMethods`,
|
|
336
|
+
and the serialization helpers are generated once in `shared/`, and every model lands in
|
|
337
|
+
the package that uses it — `shared/models.py` holds only what more than one tag
|
|
338
|
+
references. Generated modules import each other absolutely, so the output reads
|
|
339
|
+
the same wherever you open it:
|
|
340
|
+
|
|
341
|
+
```sh
|
|
342
|
+
httpxgen specs/api.yml src/api --package-name api --tag payments --tag invoices
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# src/api/invoices/client.py
|
|
347
|
+
from api.invoices.models import CreateInvoiceRequest, Invoice, InvoicePage
|
|
348
|
+
from api.shared import ApiError, HttpMethods, apply_security, serialize_path
|
|
349
|
+
from api.shared.models import ApiErrorModel
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from api import ApiError, InvoicesClient, Money, PaymentsClient
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Every managed file starts with a `# Generated by httpxgen. DO NOT EDIT.` header. Files without it are never overwritten — httpxgen aborts instead.
|
|
357
|
+
|
|
358
|
+
## Use it from a shell script
|
|
359
|
+
|
|
360
|
+
Generated code is checked in, so a tiny script is usually all the automation you need.
|
|
361
|
+
|
|
362
|
+
`scripts/generate-client.sh`:
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
#!/usr/bin/env sh
|
|
366
|
+
set -eu
|
|
367
|
+
|
|
368
|
+
SPEC_URL="https://payments.example.com/api/openapi.json"
|
|
369
|
+
OUT="src/payments"
|
|
370
|
+
|
|
371
|
+
curl -fsSL "$SPEC_URL" -o openapi.json
|
|
372
|
+
uvx httpxgen openapi.json "$OUT" --package-name payments
|
|
373
|
+
|
|
374
|
+
echo "client regenerated in $OUT"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
chmod +x scripts/generate-client.sh
|
|
379
|
+
./scripts/generate-client.sh
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Use the `--check` variant in CI so a drifting spec fails the build instead of surprising you at runtime:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
# .github/workflows/client.yml
|
|
386
|
+
- name: Verify generated client is current
|
|
387
|
+
run: |
|
|
388
|
+
curl -fsSL "$SPEC_URL" -o openapi.json
|
|
389
|
+
uvx httpxgen openapi.json src/payments --package-name payments --check
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
Generated HTTP client is current.
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Or wire it into a `Makefile`:
|
|
397
|
+
|
|
398
|
+
```make
|
|
399
|
+
.PHONY: client client-check
|
|
400
|
+
|
|
401
|
+
client:
|
|
402
|
+
uvx httpxgen openapi.json src/payments --package-name payments
|
|
403
|
+
|
|
404
|
+
client-check:
|
|
405
|
+
uvx httpxgen openapi.json src/payments --package-name payments --check
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Scope
|
|
409
|
+
|
|
410
|
+
httpxgen targets ordinary OpenAPI 3.0 and 3.1 client specifications, not every
|
|
411
|
+
JSON Schema feature. It supports JSON/YAML input, local component references,
|
|
412
|
+
path/query/header/cookie serialization, JSON/form/multipart/binary request
|
|
413
|
+
bodies, JSON/text/binary responses, numeric/default/status-range responses,
|
|
414
|
+
typed error bodies, common
|
|
415
|
+
security schemes, directional request/response models, inline and recursive
|
|
416
|
+
Pydantic models, enums, nullable values, discriminated unions, and practical
|
|
417
|
+
`allOf` inheritance.
|
|
418
|
+
|
|
419
|
+
Unsupported constructs fail generation where possible. Important remaining
|
|
420
|
+
limitations are external references, streaming, callbacks/webhooks, automatic
|
|
421
|
+
pagination, and a synchronous client.
|
|
422
|
+
|
|
423
|
+
See [`MISSING_IMPL.md`](MISSING_IMPL.md) for the prioritized checklist and the
|
|
424
|
+
test requirements for each future step.
|
|
425
|
+
|
|
426
|
+
## Development
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
uv sync --all-groups
|
|
430
|
+
uv run pytest
|
|
431
|
+
uv run ruff check . # lint
|
|
432
|
+
uv run ruff format . # format
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`preview/` is generated output, checked in so the effect of a change is visible
|
|
436
|
+
in review. Regenerate it whenever generated code changes — CI fails if it is
|
|
437
|
+
stale:
|
|
438
|
+
|
|
439
|
+
```sh
|
|
440
|
+
./scripts/generate-preview.sh
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## License
|
|
444
|
+
|
|
445
|
+
MIT
|