Skip to content

Commit 5f4c5d7

Browse files
author
Anders Brams
committed
feat: better nullable handling, content types extracted from spec
1 parent 6cd0d56 commit 5f4c5d7

17 files changed

Lines changed: 609 additions & 290 deletions

File tree

README.md

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
## Installation
1212

1313
```bash
14-
uv add openapi-python # If you want to define your own HTTP transport (requests, asyncio, ...)
1514
uv add openapi-python[httpx] # Ships with an `httpx` transport
15+
uv add openapi-python # Bring your own transport (requests, asyncio, ...)
1616
```
1717

1818

@@ -21,104 +21,62 @@ uv add openapi-python[httpx] # Ships with an `httpx` transport
2121
Generate a client from an OpenAPI spec in `openapi.json`:
2222

2323
```bash
24-
# Types + HTTP transport
24+
# Types + Protocol + HTTP transport
2525
uv run openapi-python generate --spec ./openapi.json --out ./generated
2626

27-
# Types + custom transport protocol
27+
# Types + Protocol
2828
uv run openapi-python generate --spec ./openapi.json --out ./generated --protocol-only
2929
```
3030

31-
... or programatically:
32-
33-
```python
34-
from pathlib import Path
35-
from openapi_python import GenerationRequest, generate_client
36-
37-
result = generate_client(
38-
GenerationRequest(
39-
spec_source="./openapi.json",
40-
output_dir=Path("./generated"),
41-
package_name="my_client",
42-
overwrite=True,
43-
)
44-
)
45-
```
46-
4731
## Using generated clients
4832

4933
Generated clients expose route-specific callables with typed `params`, `query`, `headers`, `body`, and return values.
5034

51-
With the built-in `httpx` transport:
35+
When using `openapi-python[httpx]`:
5236

5337
```python
54-
from generated.my_client import Client
38+
import httpx
5539

56-
client = Client(base_url="https://api.example.com")
57-
book = client.get("/books/{book_id}")(params={"book_id": 1})
58-
```
40+
from generated.my_client import AsyncClient, Client, DefaultAsyncTransport, DefaultTransport
5941

60-
For async APIs:
42+
sync_http = httpx.Client(
43+
base_url="https://api.example.com",
44+
headers={"authorization": "Bearer token"},
45+
)
46+
async_http = httpx.AsyncClient(
47+
base_url="https://api.example.com",
48+
headers={"authorization": "Bearer token"},
49+
)
6150

62-
```python
63-
from generated.my_client import AsyncClient
51+
client = Client(
52+
transport=DefaultTransport(client=sync_http),
53+
)
54+
async_client = AsyncClient(
55+
transport=DefaultAsyncTransport(client=async_http),
56+
)
6457

65-
async_client = AsyncClient(base_url="https://api.example.com")
66-
book = await async_client.get("/books/{book_id}")(params={"book_id": 1})
58+
book = client.get("/books/{book_id}")(params={"book_id": 1})
59+
async_book = await async_client.get("/books/{book_id}")(params={"book_id": 1})
6760
```
6861

69-
For protocol-only clients, provide your own transport:
62+
When using `openapi-python`, or for `--protocol-only` clients, provide your own transport:
7063

7164
```python
7265
from generated.my_client import Client
7366

74-
client = Client(base_url="https://api.example.com", transport=my_transport)
67+
client = Client(transport=my_transport)
7568
book = client.get("/books/{book_id}")(params={"book_id": 1})
7669
```
7770

71+
See [Custom transport](#custom-transport) on how to build a custom transport.
7872

79-
## Extensibility
80-
81-
`GeneratorExtensions` exposes two safe hooks:
82-
83-
- `normalize_hooks`: transform the normalized model before rendering.
84-
- `render_context_hooks`: transform rendered file content map before writing.
85-
86-
## Transport Decoupling
73+
## Protocols
8774

8875
Generated clients expose a transport protocol. You can plug in your own transport while keeping route-level typing guarantees.
8976

90-
Use `--protocol-only` to generate clients that require a supplied transport and do not emit the built-in `httpx` transport classes. By default, generated clients include `DefaultTransport` and `DefaultAsyncTransport`, which require the `httpx` extra when instantiated.
91-
92-
Protocol typing can be relaxed independently with `--no-routes`, `--no-requests`, and `--no-responses`. Those flags replace the corresponding route literals, request payload types, or response types with broad catch-all types.
93-
94-
### Built-in `httpx` transport
95-
96-
Install the `httpx` extra and generate with the default transport mode:
97-
98-
```bash
99-
uv add "openapi-python[httpx]"
100-
uv run openapi-python generate --spec ./openapi.json --out ./generated --package my_client
101-
```
102-
103-
You can supply preconfigured `httpx` clients:
104-
105-
```python
106-
import httpx
107-
108-
from generated.my_client import AsyncClient, Client, DefaultAsyncTransport, DefaultTransport
109-
110-
sync_http = httpx.Client(headers={"authorization": "Bearer token"})
111-
async_http = httpx.AsyncClient(headers={"authorization": "Bearer token"})
77+
Use `--protocol-only` to generate clients that don't ship with a built-in transport.
11278

113-
client = Client(
114-
base_url="https://api.example.com",
115-
transport=DefaultTransport(client=sync_http),
116-
)
117-
async_client = AsyncClient(
118-
base_url="https://api.example.com",
119-
transport=DefaultAsyncTransport(client=async_http),
120-
)
121-
```
79+
Protocol typing can be relaxed independently with `--no-routes`, `--no-requests`, and `--no-responses`.
12280

12381
### Custom transport
12482

@@ -153,14 +111,19 @@ class RequestsTransport:
153111
params: Mapping[str, object] | None,
154112
query: Mapping[str, object] | None,
155113
headers: Mapping[str, object] | None,
114+
request_media_type: str | None,
156115
body: object | None,
116+
response_media_type: str | None,
157117
) -> object:
118+
request_kwargs = {"json": body}
119+
if request_media_type and request_media_type != "application/json":
120+
request_kwargs = {"data": body}
158121
response = requests.request(
159122
method=method.upper(),
160123
url=f"{base_url.rstrip('/')}{route.format(**(params or {}))}",
161124
params={key: str(value) for key, value in (query or {}).items()} or None,
162125
headers={key: str(value) for key, value in (headers or {}).items()} or None,
163-
json=body,
126+
**request_kwargs,
164127
)
165128
response.raise_for_status()
166129
if response.content:
@@ -169,7 +132,6 @@ class RequestsTransport:
169132

170133

171134
client = Client(
172-
base_url="https://api.example.com",
173135
transport=RequestsTransport(),
174136
)
175137
book = client.get("/books/{book_id}")(params={"book_id": 1})

openapi_python/generate/__init__.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

openapi_python/generate/generator.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

openapi_python/generate/runtime/async_base_client.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

openapi_python/generate/runtime/base_client.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

openapi_python/generator/model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ class OperationDef:
9797
query_required: bool
9898
headers_type: TypeAnnotation
9999
headers_required: bool
100+
request_media_type: str | None
100101
body_type: TypeAnnotation | None
101102
body_required: bool
103+
response_media_type: str | None
102104
response_type: TypeAnnotation
103105

104106

0 commit comments

Comments
 (0)