Files
openapi-first/docs/wiki/03_use_cases/03_client.md

157 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)