157 lines
4.5 KiB
Markdown
157 lines
4.5 KiB
Markdown
# Use Case 3: The OperationId-Driven Client
|
||
|
||
`OpenAPIClient` is the other side of the contract. It reads the *same* OpenAPI document the server runs on and exposes one callable per `operationId` — so "client" and "server" are two views of one truth.
|
||
|
||
---
|
||
|
||
## 🔍 1. Before You Start
|
||
|
||
The client is `httpx`-based and returns raw `httpx.Response` objects — no magic deserialization, no hidden schema inference:
|
||
|
||
- **No** response Pydantic models
|
||
- **No** implicit URL construction (path params are explicit)
|
||
- **No** hand-written `requests.get(...)` scattered through your code
|
||
|
||
---
|
||
|
||
## 🧬 2. Constructing the Client
|
||
|
||
```python
|
||
from openapi_first.loader import load_openapi
|
||
from openapi_first.client import OpenAPIClient
|
||
|
||
spec = load_openapi("openapi.yaml")
|
||
client = OpenAPIClient(spec)
|
||
```
|
||
|
||
The base URL comes from the spec's `servers` list (first entry) unless you pass `base_url` explicitly:
|
||
|
||
```python
|
||
client = OpenAPIClient(spec, base_url="https://api.internal.myco/v1")
|
||
```
|
||
|
||
You can also hand over a preconfigured `httpx.Client` (custom transport, TLS, retries):
|
||
|
||
```python
|
||
import httpx
|
||
|
||
transport = httpx.HTTPTransport(retries=3)
|
||
client = OpenAPIClient(
|
||
spec,
|
||
client=httpx.Client(transport=transport),
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 💥 3. Fail-Fast at Construction
|
||
|
||
`OpenAPIClient(...)` **raises immediately** if the contract is broken — you find out at startup, not on the first request:
|
||
|
||
| Violation | Error |
|
||
|----------------------------------------|-----------------------------------|
|
||
| Spec has no `servers` entry | `OpenAPIClientError` |
|
||
| Spec has no `paths` | `OpenAPIClientError` |
|
||
| Operation missing `operationId` | `OpenAPIClientError` |
|
||
| Duplicate `operationId` | `OpenAPIClientError` |
|
||
| Operation references unknown parameters| `OpenAPIClientError` |
|
||
|
||
Missing *required parameters* fail at **call time** — pydoclint-grade strictness on the wire.
|
||
|
||
---
|
||
|
||
## 📞 4. Calling Operations
|
||
|
||
Every `operationId` becomes a method. The call signature is uniform across the whole client:
|
||
|
||
```python
|
||
response = client.<operationId>(
|
||
*,
|
||
path_params: dict | None = None,
|
||
query: dict | None = None,
|
||
headers: dict | None = None,
|
||
body: Any | None = None,
|
||
timeout: float | None = None,
|
||
) -> httpx.Response
|
||
```
|
||
|
||
Concrete examples (from the `crud_app` / `health_app` templates):
|
||
|
||
```python
|
||
# No parameters — simplest
|
||
response = client.get_health()
|
||
assert response.status_code == 200
|
||
|
||
# Path parameter
|
||
response = client.get_item(path_params={"item_id": 3})
|
||
|
||
# Query parameters
|
||
response = client.list_items(query={"limit": 10, "offset": 20})
|
||
|
||
# JSON request body
|
||
response = client.create_item(body={"name": "Orange", "price": 0.8})
|
||
|
||
# Custom headers / timeout
|
||
response = client.get_user(
|
||
path_params={"user_id": 1},
|
||
headers={"X-Internal-Key": "..."},
|
||
timeout=30,
|
||
)
|
||
```
|
||
|
||
**Returns:** the raw `httpx.Response`, so `status_code`, `.json()`, `.headers` are all yours to inspect.
|
||
|
||
---
|
||
|
||
## 🧠 5. How Parameters Are Bound
|
||
|
||
For each operation the client knows exactly *where* each parameter belongs:
|
||
|
||
| OpenAPI location | Client kwarg |
|
||
|------------------|-------------------|
|
||
| `in: path` | `path_params[name]` |
|
||
| `in: query` | `query[name]` |
|
||
| `in: header` | `headers[name]` |
|
||
| `requestBody` | `body` |
|
||
|
||
JSON media types are sent as `json=`; other media types as raw `content=`.
|
||
|
||
---
|
||
|
||
## 🔄 6. Server ↔ Client, One Spec
|
||
|
||
```bash
|
||
# One directory, two processes
|
||
uvicorn main:app --port 8000 # server
|
||
python -c "import asyncio; from client_script import run; asyncio.run(run())" # client
|
||
```
|
||
|
||
Or the same client against a remote environment:
|
||
|
||
```python
|
||
client = OpenAPIClient(
|
||
spec,
|
||
base_url="https://staging.internal.myco",
|
||
)
|
||
```
|
||
|
||
The URL is the *only* thing that changes between environments — the contract never does.
|
||
|
||
---
|
||
|
||
## ✍️ 7. OperationId as the API
|
||
|
||
Because the client is operationId-driven:
|
||
|
||
- Adding an operation = adding a method (and vice versa)
|
||
- No operation can be "forgotten" by the client — it's constructed from the spec
|
||
- `operationId` is the one name you remember; the HTTP verb/path is an implementation detail
|
||
|
||
If an `operationId` you call doesn't exist, you get an `AttributeError` at construction-scan time — never a silent 404.
|
||
|
||
---
|
||
|
||
## Related
|
||
|
||
- [01 – Overview](../01_overview.md) · [01 – Quickstart](01_quickstart.md) · [02 – Templates](02_templates.md)
|