Introduce an OpenAPI-first HTTP client driven by the same specification

used for server-side route binding, and refactor documentation and
templates to treat the client as a first-class contract consumer.

Key changes:
- Add OpenAPI-first client module based on httpx
- Document client usage alongside server-side binder usage
- Update mkdocs navigation to include client documentation
- Refactor CRUD and model app templates to call APIs via operationId
  instead of hardcoded paths
- Align package documentation and public API surface with client support
- Clarify server/client dependency split (fastapi vs httpx)

This establishes strict symmetry between OpenAPI-driven server binding
and OpenAPI-driven client invocation, reinforcing OpenAPI as the single
source of truth on both sides of the contract.
This commit is contained in:
2026-01-11 18:41:27 +05:30
parent 31bf1b1b6b
commit a74e3d0d01
7 changed files with 295 additions and 24 deletions

View File

@@ -14,14 +14,22 @@ The tests assume:
from fastapi.testclient import TestClient
from main import app
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
client = TestClient(app)
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(
spec=spec,
base_url="http://testserver",
client=client,
)
def test_list_items_initial():
"""Initial items should be present."""
response = client.get("/items")
response = client.list_items()
assert response.status_code == 200
data = response.json()
@@ -35,7 +43,9 @@ def test_list_items_initial():
def test_get_item():
"""Existing item should be retrievable by ID."""
response = client.get("/items/1")
response = client.get_item(
path_params={"item_id": 1}
)
assert response.status_code == 200
item = response.json()
@@ -51,7 +61,9 @@ def test_create_item():
"price": 0.8,
}
response = client.post("/items", json=payload)
response = client.create_item(
body=payload
)
assert response.status_code == 201
item = response.json()
@@ -60,7 +72,7 @@ def test_create_item():
assert item["price"] == payload["price"]
# Verify it appears in list
list_response = client.get("/items")
list_response = client.list_items()
ids = {i["id"] for i in list_response.json()}
assert item["id"] in ids
@@ -72,7 +84,10 @@ def test_update_item():
"price": 0.6,
}
response = client.put("/items/1", json=payload)
response = client.update_item(
path_params={"item_id": 1},
body=payload,
)
assert response.status_code == 200
item = response.json()
@@ -81,7 +96,9 @@ def test_update_item():
assert item["price"] == payload["price"]
# Verify persisted update
get_response = client.get("/items/1")
get_response = client.get_item(
path_params={"item_id": 1}
)
updated = get_response.json()
assert updated["name"] == payload["name"]
assert updated["price"] == payload["price"]
@@ -89,10 +106,12 @@ def test_update_item():
def test_delete_item():
"""Deleting an item should remove it from the store."""
response = client.delete("/items/2")
response = client.delete_item(
path_params={"item_id": 2}
)
assert response.status_code == 204
# Verify deletion
list_response = client.get("/items")
list_response = client.list_items()
ids = {item["id"] for item in list_response.json()}
assert 2 not in ids