8 Commits
0.0.1 ... 0.0.3

Author SHA1 Message Date
6180443327 v0.0.3 — Introduces an OpenAPI-first HTTP client, updates documentation and templates to use operationId-based calls, and adds httpx as a client-side dependency.
All checks were successful
continuous-integration/drone/tag Build is passing
2026-01-11 18:43:04 +05:30
1b26021725 added explicitly httpx as it's being used in client.py 2026-01-11 18:42:13 +05:30
a74e3d0d01 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.
2026-01-11 18:41:27 +05:30
31bf1b1b6b cleanup requirements.txt 2026-01-11 17:12:03 +05:30
7b4583f305 feat(templates): add CRUD and model CRUD app scaffolds; bump version to 0.0.2
All checks were successful
continuous-integration/drone/tag Build is passing
- add OpenAPI-first CRUD app template with in-memory mock data
- add model-based CRUD app template with Pydantic layer
- include end-to-end tests for both templates
- document explicit runtime status handling in routes
- add Pydantic dependency for model_app support
2026-01-10 18:07:01 +05:30
fc8346fcda added pydantic with pin 2026-01-10 18:05:51 +05:30
40d91bc52b feat(scaffold): add model-backed CRUD service template
Provides a complete OpenAPI-first CRUD example with a Pydantic
model layer, explicit runtime semantics, and integration tests
to support developer onboarding and real-world service structure.
2026-01-10 18:05:39 +05:30
2ac342240b feat(templates): add OpenAPI-first CRUD app scaffold with mock data
- include CRUD OpenAPI spec
- add in-memory mock data store
- implement OpenAPI-bound route handlers
- provide runnable FastAPI bootstrap
- include end-to-end integration test
2026-01-10 17:59:11 +05:30
18 changed files with 927 additions and 11 deletions

View File

@@ -0,0 +1,3 @@
# Client
::: openapi_first.client

View File

@@ -42,6 +42,7 @@ nav:
- OpenAPI-First App: openapi_first/app.md - OpenAPI-First App: openapi_first/app.md
- Route Binder: openapi_first/binder.md - Route Binder: openapi_first/binder.md
- Spec Loaders: openapi_first/loader.md - Spec Loaders: openapi_first/loader.md
- Client: openapi_first/client.md
- CLI: - CLI:
- Home: openapi_first/cli.md - Home: openapi_first/cli.md

View File

@@ -17,11 +17,12 @@ convenience facades.
Architecture Overview Architecture Overview
---------------------------------------------------------------------- ----------------------------------------------------------------------
The library is structured around three core responsibilities: The library is structured around four core responsibilities:
- loader: load and validate OpenAPI 3.x specifications (JSON/YAML) - loader: load and validate OpenAPI 3.x specifications (JSON/YAML)
- binder: bind OpenAPI operations to FastAPI routes via operationId - binder: bind OpenAPI operations to FastAPI routes via operationId
- app: OpenAPI-first FastAPI application bootstrap - app: OpenAPI-first FastAPI application bootstrap
- client: OpenAPI-first HTTP client driven by the same specification
- errors: explicit error hierarchy for contract violations - errors: explicit error hierarchy for contract violations
The package root acts as a **namespace**, not a facade. Consumers are The package root acts as a **namespace**, not a facade. Consumers are
@@ -40,7 +41,8 @@ Or with Poetry:
poetry add openapi-first poetry add openapi-first
Runtime dependencies are intentionally minimal: Runtime dependencies are intentionally minimal:
- fastapi - fastapi (server-side)
- httpx (client-side)
- openapi-spec-validator - openapi-spec-validator
- pyyaml (optional, for YAML specs) - pyyaml (optional, for YAML specs)
@@ -48,7 +50,7 @@ The ASGI server (e.g., uvicorn) is an application-level dependency and is
not bundled with this library. not bundled with this library.
---------------------------------------------------------------------- ----------------------------------------------------------------------
Basic Usage Server-Side Usage (OpenAPI → FastAPI)
---------------------------------------------------------------------- ----------------------------------------------------------------------
Minimal OpenAPI-first FastAPI application: Minimal OpenAPI-first FastAPI application:
@@ -57,7 +59,7 @@ Minimal OpenAPI-first FastAPI application:
import my_service.routes as routes import my_service.routes as routes
api = app.OpenAPIFirstApp( api = app.OpenAPIFirstApp(
openapi_path="openapi.json", openapi_path="openapi.yaml",
routes_module=routes, routes_module=routes,
title="My Service", title="My Service",
version="1.0.0", version="1.0.0",
@@ -81,6 +83,56 @@ OpenAPI snippet:
"200": "200":
description: OK description: OK
The binder guarantees:
- Every OpenAPI operationId has exactly one handler
- No undocumented routes exist
- All mismatches fail at application startup
----------------------------------------------------------------------
Client-Side Usage (OpenAPI → HTTP Client)
----------------------------------------------------------------------
The same OpenAPI specification can be used to construct a strict,
operationId-driven HTTP client.
Client construction:
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
Calling operations (operationId is the API):
response = client.get_health()
assert response.status_code == 200
assert response.json() == {"status": "ok"}
Path parameters must match the OpenAPI specification exactly:
response = client.get_item(
path_params={"item_id": 1}
)
Request bodies are passed explicitly:
response = client.create_item(
body={"name": "Orange", "price": 0.8}
)
Client guarantees:
- One callable per OpenAPI operationId
- No hardcoded URLs or HTTP methods in user code
- Path and body parameters must match the spec exactly
- Invalid or incomplete OpenAPI specs fail at client construction time
- No schema inference or mutation is performed
The client is transport-level only and returns `httpx.Response`
objects directly. Response interpretation and validation are left to
the consumer or higher-level layers.
---------------------------------------------------------------------- ----------------------------------------------------------------------
Extensibility Model Extensibility Model
---------------------------------------------------------------------- ----------------------------------------------------------------------
@@ -107,6 +159,7 @@ The supported public API consists of the following top-level modules:
- openapi_first.app - openapi_first.app
- openapi_first.binder - openapi_first.binder
- openapi_first.loader - openapi_first.loader
- openapi_first.client
- openapi_first.errors - openapi_first.errors
Classes and functions should be imported explicitly from these modules. Classes and functions should be imported explicitly from these modules.
@@ -118,8 +171,8 @@ Design Guarantees
- OpenAPI is the single source of truth - OpenAPI is the single source of truth
- No undocumented routes can exist - No undocumented routes can exist
- No OpenAPI operation can exist without a handler - No OpenAPI operation can exist without a handler or client callable
- All contract violations fail at application startup - All contract violations fail at application startup or client creation
- No hidden FastAPI magic or implicit behavior - No hidden FastAPI magic or implicit behavior
- Deterministic, testable application assembly - Deterministic, testable application assembly
- CI-friendly failure modes - CI-friendly failure modes
@@ -131,11 +184,13 @@ enforcement over convenience shortcuts.
from . import app from . import app
from . import binder from . import binder
from . import loader from . import loader
from . import client
from . import errors from . import errors
__all__ = [ __all__ = [
"app", "app",
"binder", "binder",
"loader", "loader",
"client",
"errors", "errors",
] ]

176
openapi_first/client.py Normal file
View File

@@ -0,0 +1,176 @@
from typing import Any, Callable, Dict, Optional
from urllib.parse import urljoin
import httpx
from .errors import OpenAPIFirstError
class OpenAPIClientError(OpenAPIFirstError):
"""Raised when an OpenAPI client operation fails."""
class OpenAPIClient:
"""
OpenAPI-first HTTP client (httpx-based).
- One callable per operationId
- Explicit parameters (path, query, headers, body)
- No implicit schema inference or mutation
"""
def __init__(
self,
spec: dict[str, Any],
base_url: Optional[str] = None,
client: Optional[httpx.Client] = None,
) -> None:
self.spec = spec
self.base_url = base_url or self._resolve_base_url(spec)
self.client = client or httpx.Client(base_url=self.base_url)
self._operations: Dict[str, Callable[..., httpx.Response]] = {}
self._build_operations()
# ------------------------------------------------------------------ #
# Public API
# ------------------------------------------------------------------ #
def __getattr__(self, name: str) -> Callable[..., httpx.Response]:
try:
return self._operations[name]
except KeyError:
raise AttributeError(f"No such operationId: {name}") from None
def operations(self) -> Dict[str, Callable[..., httpx.Response]]:
return dict(self._operations)
# ------------------------------------------------------------------ #
# Internal mechanics
# ------------------------------------------------------------------ #
def _resolve_base_url(self, spec: dict[str, Any]) -> str:
servers = spec.get("servers")
if not servers:
raise OpenAPIClientError("No servers defined in OpenAPI spec")
url = servers[0].get("url")
if not url:
raise OpenAPIClientError("Server entry missing 'url'")
return url.rstrip("/") + "/"
def _build_operations(self) -> None:
paths = self.spec.get("paths", {})
if not paths:
raise OpenAPIClientError("OpenAPI spec contains no paths")
for path, path_item in paths.items():
for method, operation in path_item.items():
if method.lower() not in {
"get", "post", "put", "patch", "delete", "head", "options"
}:
continue
operation_id = operation.get("operationId")
if not operation_id:
raise OpenAPIClientError(
f"Missing operationId for {method.upper()} {path}"
)
if operation_id in self._operations:
raise OpenAPIClientError(
f"Duplicate operationId detected: {operation_id}"
)
self._operations[operation_id] = self._make_operation(
method=method.upper(),
path=path,
operation=operation,
)
def _make_operation(
self,
*,
method: str,
path: str,
operation: dict[str, Any],
) -> Callable[..., httpx.Response]:
request_body = operation.get("requestBody")
def call(
*,
path_params: Optional[dict[str, Any]] = None,
query: Optional[dict[str, Any]] = None,
headers: Optional[dict[str, str]] = None,
body: Optional[Any] = None,
timeout: Optional[float] = None,
) -> httpx.Response:
url = self._build_url(path, path_params or {})
req_headers = headers.copy() if headers else {}
json_data = None
content = None
if request_body is not None:
if body is None:
raise OpenAPIClientError(
f"Request body required for operation {operation['operationId']}"
)
media_types = request_body.get("content", {})
if "application/json" in media_types:
json_data = body
req_headers.setdefault("Content-Type", "application/json")
else:
content = body
response = self.client.request(
method=method,
url=url,
params=query,
headers=req_headers,
json=json_data,
content=content,
timeout=timeout,
)
return response
call.__name__ = operation["operationId"]
call.__doc__ = self._build_docstring(method, path, operation)
return call
def _build_url(self, path: str, path_params: dict[str, Any]) -> str:
try:
formatted_path = path.format(**path_params)
except KeyError as exc:
raise OpenAPIClientError(
f"Missing path parameter: {exc.args[0]}"
) from exc
return urljoin(self.base_url, formatted_path.lstrip("/"))
def _build_docstring(
self,
method: str,
path: str,
operation: dict[str, Any],
) -> str:
lines = [
f"{method} {path}",
"",
operation.get("summary", ""),
"",
"Parameters:",
" path_params: dict | None",
" query: dict | None",
" headers: dict | None",
" body: Any | None",
"",
"Returns:",
" httpx.Response",
]
return "\n".join(lines)

View File

@@ -25,8 +25,6 @@ This module intentionally does NOT:
- Perform request/response validation at runtime - Perform request/response validation at runtime
""" """
from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any

View File

@@ -0,0 +1,41 @@
"""
In-memory mock data store for CRUD example.
This module intentionally avoids persistence and concurrency guarantees.
It is suitable for demos, tests, and scaffolding only.
"""
from typing import Dict
_items: Dict[int, dict] = {
1: {"id": 1, "name": "Apple", "price": 0.5},
2: {"id": 2, "name": "Banana", "price": 0.3},
}
_next_id = 3
def list_items():
return list(_items.values())
def get_item(item_id: int):
return _items[item_id]
def create_item(payload: dict):
global _next_id
item = {"id": _next_id, **payload}
_items[_next_id] = item
_next_id += 1
return item
def update_item(item_id: int, payload: dict):
item = {"id": item_id, **payload}
_items[item_id] = item
return item
def delete_item(item_id: int):
del _items[item_id]

View File

@@ -0,0 +1,8 @@
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="CRUD Example Service",
)

View File

@@ -0,0 +1,115 @@
openapi: 3.0.3
info:
title: CRUD Example Service
version: "1.0.0"
description: Minimal OpenAPI-first CRUD service with in-memory mock data.
paths:
/items:
get:
operationId: list_items
summary: List all items
responses:
"200":
description: List of items
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Item"
post:
operationId: create_item
summary: Create a new item
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemCreate"
responses:
"201":
description: Item created
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
/items/{item_id}:
get:
operationId: get_item
summary: Get item by ID
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Item found
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
put:
operationId: update_item
summary: Update an item
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemCreate"
responses:
"200":
description: Item updated
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
delete:
operationId: delete_item
summary: Delete an item
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Item deleted
components:
schemas:
Item:
type: object
properties:
id:
type: integer
name:
type: string
price:
type: number
format: float
required: [id, name, price]
ItemCreate:
type: object
properties:
name:
type: string
price:
type: number
format: float
required: [name, price]

View File

@@ -0,0 +1,50 @@
"""
CRUD route handlers bound via OpenAPI operationId.
These handlers explicitly control HTTP status codes to ensure
runtime behavior matches the OpenAPI contract.
"""
from fastapi import Response, HTTPException
from data import (
list_items as _list_items,
get_item as _get_item,
create_item as _create_item,
update_item as _update_item,
delete_item as _delete_item,
)
def list_items():
return _list_items()
def get_item(item_id: int):
try:
return _get_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
def create_item(payload: dict, response: Response):
item = _create_item(payload)
response.status_code = 201
return item
def update_item(item_id: int, payload: dict):
try:
return _update_item(item_id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
def delete_item(item_id: int, response: Response):
try:
_delete_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
response.status_code = 204
return None

View File

@@ -0,0 +1,116 @@
"""
End-to-end tests for the OpenAPI-first CRUD example app.
These tests validate that all CRUD operations behave correctly
against the in-memory mock data store.
The tests assume:
- OpenAPI-first route binding
- In-memory storage (no persistence guarantees)
- Deterministic behavior in a single process
"""
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.list_items()
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 2
ids = {item["id"] for item in data}
assert 1 in ids
assert 2 in ids
def test_get_item():
"""Existing item should be retrievable by ID."""
response = client.get_item(
path_params={"item_id": 1}
)
assert response.status_code == 200
item = response.json()
assert item["id"] == 1
assert "name" in item
assert "price" in item
def test_create_item():
"""Creating a new item should return the created entity."""
payload = {
"name": "Orange",
"price": 0.8,
}
response = client.create_item(
body=payload
)
assert response.status_code == 201
item = response.json()
assert "id" in item
assert item["name"] == payload["name"]
assert item["price"] == payload["price"]
# Verify it appears in list
list_response = client.list_items()
ids = {i["id"] for i in list_response.json()}
assert item["id"] in ids
def test_update_item():
"""Updating an item should replace its values."""
payload = {
"name": "Green Apple",
"price": 0.6,
}
response = client.update_item(
path_params={"item_id": 1},
body=payload,
)
assert response.status_code == 200
item = response.json()
assert item["id"] == 1
assert item["name"] == payload["name"]
assert item["price"] == payload["price"]
# Verify persisted update
get_response = client.get_item(
path_params={"item_id": 1}
)
updated = get_response.json()
assert updated["name"] == payload["name"]
assert updated["price"] == payload["price"]
def test_delete_item():
"""Deleting an item should remove it from the store."""
response = client.delete_item(
path_params={"item_id": 2}
)
assert response.status_code == 204
# Verify deletion
list_response = client.list_items()
ids = {item["id"] for item in list_response.json()}
assert 2 not in ids

View File

@@ -0,0 +1,44 @@
"""
In-memory data store using Pydantic models.
This module is NOT thread-safe and is intended for demos and scaffolds only.
"""
from typing import Dict
from models import Item, ItemCreate
_items: Dict[int, Item] = {
1: Item(id=1, name="Apple", price=0.5),
2: Item(id=2, name="Banana", price=0.3),
}
_next_id = 3
def list_items() -> list[Item]:
return list(_items.values())
def get_item(item_id: int) -> Item:
return _items[item_id]
def create_item(payload: ItemCreate) -> Item:
global _next_id
item = Item(id=_next_id, **payload.model_dump())
_items[_next_id] = item
_next_id += 1
return item
def update_item(item_id: int, payload: ItemCreate) -> Item:
if item_id not in _items:
raise KeyError(item_id)
item = Item(id=item_id, **payload.model_dump())
_items[item_id] = item
return item
def delete_item(item_id: int) -> None:
del _items[item_id]

View File

@@ -0,0 +1,8 @@
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="Model CRUD Example Service",
)

View File

@@ -0,0 +1,18 @@
"""
Pydantic domain models for the CRUD example.
"""
from pydantic import BaseModel
class ItemBase(BaseModel):
name: str
price: float
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int

View File

@@ -0,0 +1,116 @@
openapi: 3.0.3
info:
title: Model CRUD Example Service
version: "1.0.0"
description: OpenAPI-first CRUD service with Pydantic models.
paths:
/items:
get:
operationId: list_items
responses:
"200":
description: List of items
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Item"
post:
operationId: create_item
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemCreate"
responses:
"201":
description: Item created
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
/items/{item_id}:
get:
operationId: get_item
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Item found
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
description: Item not found
put:
operationId: update_item
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemCreate"
responses:
"200":
description: Item updated
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
description: Item not found
delete:
operationId: delete_item
parameters:
- name: item_id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Item deleted
"404":
description: Item not found
components:
schemas:
Item:
type: object
required: [id, name, price]
properties:
id:
type: integer
name:
type: string
price:
type: number
format: float
ItemCreate:
type: object
required: [name, price]
properties:
name:
type: string
price:
type: number
format: float

View File

@@ -0,0 +1,48 @@
"""
CRUD route handlers bound via OpenAPI operationId.
"""
from fastapi import Response, HTTPException
from models import ItemCreate
from data import (
list_items as _list_items,
get_item as _get_item,
create_item as _create_item,
update_item as _update_item,
delete_item as _delete_item,
)
def list_items():
return _list_items()
def get_item(item_id: int):
try:
return _get_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
def create_item(payload: ItemCreate, response: Response):
item = _create_item(payload)
response.status_code = 201
return item
def update_item(item_id: int, payload: ItemCreate):
try:
return _update_item(item_id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
def delete_item(item_id: int, response: Response):
try:
_delete_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
response.status_code = 204
return None

View File

@@ -0,0 +1,117 @@
"""
End-to-end tests for the OpenAPI-first model CRUD example app.
These tests validate that all CRUD operations behave correctly
against the in-memory mock data store using Pydantic models.
The tests assume:
- OpenAPI-first route binding
- Pydantic model validation
- In-memory storage (no persistence guarantees)
- Deterministic behavior in a single process
"""
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.list_items()
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 2
ids = {item["id"] for item in data}
assert 1 in ids
assert 2 in ids
def test_get_item():
"""Existing item should be retrievable by ID."""
response = client.get_item(
path_params={"item_id": 1}
)
assert response.status_code == 200
item = response.json()
assert item["id"] == 1
assert isinstance(item["name"], str)
assert isinstance(item["price"], float)
def test_create_item():
"""Creating a new item should return the created entity."""
payload = {
"name": "Orange",
"price": 0.8,
}
response = client.create_item(
body=payload
)
assert response.status_code == 201
item = response.json()
assert "id" in item
assert item["name"] == payload["name"]
assert item["price"] == payload["price"]
# Verify it appears in list
list_response = client.list_items()
ids = {i["id"] for i in list_response.json()}
assert item["id"] in ids
def test_update_item():
"""Updating an item should replace its values."""
payload = {
"name": "Green Apple",
"price": 0.6,
}
response = client.update_item(
path_params={"item_id": 1},
body=payload,
)
assert response.status_code == 200
item = response.json()
assert item["id"] == 1
assert item["name"] == payload["name"]
assert item["price"] == payload["price"]
# Verify persisted update
get_response = client.get_item(
path_params={"item_id": 1}
)
updated = get_response.json()
assert updated["name"] == payload["name"]
assert updated["price"] == payload["price"]
def test_delete_item():
"""Deleting an item should remove it from the store."""
response = client.delete_item(
path_params={"item_id": 2}
)
assert response.status_code == 204
# Verify deletion
list_response = client.list_items()
ids = {item["id"] for item in list_response.json()}
assert 2 not in ids

View File

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "openapi-first" name = "openapi-first"
version = "0.0.1" version = "0.0.3"
description = "Strict OpenAPI-first application bootstrap for FastAPI." description = "Strict OpenAPI-first application bootstrap for FastAPI."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@@ -1,7 +1,9 @@
fastapi==0.128.0, fastapi==0.128.0,
openapi-spec-validator==0.7.2, openapi-spec-validator==0.7.2
pyyaml==6.0.3, pyyaml==6.0.3
uvicorn==0.40.0 uvicorn==0.40.0
pydantic==2.12.5
httpx==0.28.1
# Test Packages # Test Packages
pytest==7.4.0 pytest==7.4.0