docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index
This commit is contained in:
156
docs/wiki/03_use_cases/03_client.md
Normal file
156
docs/wiki/03_use_cases/03_client.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user