vet app
This commit is contained in:
97
openapi_first/templates/vet_app/__init__.py
Normal file
97
openapi_first/templates/vet_app/__init__.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
OpenAPI-first Veterinary Clinic application template.
|
||||
|
||||
This package contains a complete, runnable example of an OpenAPI-first
|
||||
veterinary clinic management service. It demonstrates all ``x-`` extension
|
||||
fields consumed by the ``react-openapi`` admin panel renderer.
|
||||
|
||||
The application manages five resources:
|
||||
|
||||
- **Parents** — pet owners with contact details
|
||||
- **Vets** — veterinarians with specializations
|
||||
- **Treatments** — medical procedure catalog
|
||||
- **Pets** — animals with species, age, weight, and photos
|
||||
- **Appointments** — scheduled visits linking pets, vets, and treatments
|
||||
|
||||
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||
in the OpenAPI specification (``openapi.yaml``). Every operation has an
|
||||
explicit ``operationId`` that maps to a Python handler in ``routes.py``.
|
||||
|
||||
This file is a copyable template. It is not part of the ``openapi_first``
|
||||
library API surface.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
OpenAPI x- extension fields demonstrated
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Schema-level extensions (mark a schema as a UI resource):
|
||||
|
||||
``x-resource`` (REQUIRED) Maps schema to URL path segment
|
||||
``x-primary-key`` (REQUIRED) Primary key property name
|
||||
``x-display-format`` (REQUIRED) Human-readable label template
|
||||
``x-list-columns`` (REQUIRED) Columns for the datatable
|
||||
|
||||
Property-level extensions (control UI rendering):
|
||||
|
||||
``x-label`` (REQUIRED) Human-readable field label
|
||||
``x-order`` (REQUIRED) Field ordering in forms/detail
|
||||
``x-description`` (optional) Helper text below form fields
|
||||
``x-hidden`` (optional) Visibility in form / list / detail
|
||||
``x-filterable`` (optional) Allows column filtering
|
||||
``x-sortable`` (optional) Allows column sorting
|
||||
``x-fk`` (optional) Foreign key — renders as dropdown
|
||||
``x-fk.resource`` (REQUIRED for FK) Target resource name
|
||||
``x-fk.prefetch`` (optional) Preload all FK options on mount
|
||||
``x-ui-type`` (optional) Custom UI type (e.g. image upload)
|
||||
``x-upload-url`` (optional) Upload endpoint for binary fields
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Scaffolding via CLI
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Create a new vet clinic service using the bundled template:
|
||||
|
||||
openapi-first vet_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first vet_app my-vet-clinic
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Client Usage Example
|
||||
----------------------------------------------------------------------
|
||||
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
# List pets with pagination
|
||||
response = client.list_pets(query_params={"limit": 10, "offset": 0})
|
||||
|
||||
# Create a pet with FK references
|
||||
response = client.create_pet(
|
||||
body={"name": "Fido", "species": "dog", "parents": [1, 2]}
|
||||
)
|
||||
|
||||
# Upload a pet photo
|
||||
response = client.upload_pet_photo(
|
||||
path_params={"id": 1},
|
||||
body={"file": open("photo.jpg", "rb")},
|
||||
)
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Non-Goals
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This template is intentionally minimal and is NOT:
|
||||
- production-ready
|
||||
- persistent or concurrency-safe
|
||||
- a reference architecture for data storage
|
||||
|
||||
It exists solely as a copyable example for learning, testing, and
|
||||
bootstrapping OpenAPI-first services.
|
||||
|
||||
This package is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
276
openapi_first/templates/vet_app/data.py
Normal file
276
openapi_first/templates/vet_app/data.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
In-memory data store for the Veterinary Clinic example.
|
||||
|
||||
This module is NOT thread-safe and is intended for demos and scaffolds only.
|
||||
|
||||
It provides minimal, process-local data stores for the five veterinary
|
||||
clinic entities. Each store exposes standard CRUD operations backed by
|
||||
a simple dictionary.
|
||||
|
||||
This module intentionally avoids:
|
||||
- persistence
|
||||
- concurrency guarantees
|
||||
- transactional semantics
|
||||
- validation beyond what Pydantic provides
|
||||
|
||||
This module is not part of the ``openapi_first`` library API surface.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from models import (
|
||||
Parent, ParentCreate,
|
||||
Vet, VetCreate,
|
||||
Treatment, TreatmentCreate,
|
||||
Pet, PetCreate,
|
||||
Appointment, AppointmentCreate,
|
||||
)
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_parents: dict[int, Parent] = {}
|
||||
_parents_next_id = 1
|
||||
|
||||
|
||||
def list_parents() -> list[Parent]:
|
||||
return list(_parents.values())
|
||||
|
||||
|
||||
def get_parent(parent_id: int) -> Parent:
|
||||
return _parents[parent_id]
|
||||
|
||||
|
||||
def create_parent(payload: ParentCreate) -> Parent:
|
||||
global _parents_next_id
|
||||
now = _now()
|
||||
parent = Parent(
|
||||
id=_parents_next_id,
|
||||
**payload.model_dump(exclude={"id"}),
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_parents[_parents_next_id] = parent
|
||||
_parents_next_id += 1
|
||||
return parent
|
||||
|
||||
|
||||
def update_parent(parent_id: int, payload: ParentCreate) -> Parent:
|
||||
if parent_id not in _parents:
|
||||
raise KeyError(parent_id)
|
||||
now = _now()
|
||||
parent = _parents[parent_id]
|
||||
updated = parent.model_copy(
|
||||
update={
|
||||
**payload.model_dump(exclude={"id", "metadata"}),
|
||||
"metadata": {"createdOn": parent.metadata.get("createdOn", now), "updatedOn": now}
|
||||
if parent.metadata else None,
|
||||
}
|
||||
)
|
||||
_parents[parent_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_parent(parent_id: int) -> None:
|
||||
del _parents[parent_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_vets: dict[int, Vet] = {}
|
||||
_vets_next_id = 1
|
||||
|
||||
|
||||
def list_vets() -> list[Vet]:
|
||||
return list(_vets.values())
|
||||
|
||||
|
||||
def get_vet(vet_id: int) -> Vet:
|
||||
return _vets[vet_id]
|
||||
|
||||
|
||||
def create_vet(payload: VetCreate) -> Vet:
|
||||
global _vets_next_id
|
||||
now = _now()
|
||||
vet = Vet(
|
||||
id=_vets_next_id,
|
||||
**payload.model_dump(exclude={"id"}),
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_vets[_vets_next_id] = vet
|
||||
_vets_next_id += 1
|
||||
return vet
|
||||
|
||||
|
||||
def update_vet(vet_id: int, payload: VetCreate) -> Vet:
|
||||
if vet_id not in _vets:
|
||||
raise KeyError(vet_id)
|
||||
now = _now()
|
||||
vet = _vets[vet_id]
|
||||
updated = vet.model_copy(
|
||||
update={
|
||||
**payload.model_dump(exclude={"id", "metadata"}),
|
||||
"metadata": {"createdOn": vet.metadata.get("createdOn", now), "updatedOn": now}
|
||||
if vet.metadata else None,
|
||||
}
|
||||
)
|
||||
_vets[vet_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_vet(vet_id: int) -> None:
|
||||
del _vets[vet_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Treatments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_treatments: dict[int, Treatment] = {}
|
||||
_treatments_next_id = 1
|
||||
|
||||
|
||||
def list_treatments() -> list[Treatment]:
|
||||
return list(_treatments.values())
|
||||
|
||||
|
||||
def get_treatment(treatment_id: int) -> Treatment:
|
||||
return _treatments[treatment_id]
|
||||
|
||||
|
||||
def create_treatment(payload: TreatmentCreate) -> Treatment:
|
||||
global _treatments_next_id
|
||||
now = _now()
|
||||
treatment = Treatment(
|
||||
id=_treatments_next_id,
|
||||
**payload.model_dump(exclude={"id"}),
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_treatments[_treatments_next_id] = treatment
|
||||
_treatments_next_id += 1
|
||||
return treatment
|
||||
|
||||
|
||||
def update_treatment(treatment_id: int, payload: TreatmentCreate) -> Treatment:
|
||||
if treatment_id not in _treatments:
|
||||
raise KeyError(treatment_id)
|
||||
now = _now()
|
||||
treatment = _treatments[treatment_id]
|
||||
updated = treatment.model_copy(
|
||||
update={
|
||||
**payload.model_dump(exclude={"id", "metadata"}),
|
||||
"metadata": {"createdOn": treatment.metadata.get("createdOn", now), "updatedOn": now}
|
||||
if treatment.metadata else None,
|
||||
}
|
||||
)
|
||||
_treatments[treatment_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_treatment(treatment_id: int) -> None:
|
||||
del _treatments[treatment_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_pets: dict[int, Pet] = {}
|
||||
_pets_next_id = 1
|
||||
|
||||
|
||||
def list_pets() -> list[Pet]:
|
||||
return list(_pets.values())
|
||||
|
||||
|
||||
def get_pet(pet_id: int) -> Pet:
|
||||
return _pets[pet_id]
|
||||
|
||||
|
||||
def create_pet(payload: PetCreate) -> Pet:
|
||||
global _pets_next_id
|
||||
now = _now()
|
||||
pet = Pet(
|
||||
id=_pets_next_id,
|
||||
**payload.model_dump(exclude={"id"}),
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_pets[_pets_next_id] = pet
|
||||
_pets_next_id += 1
|
||||
return pet
|
||||
|
||||
|
||||
def update_pet(pet_id: int, payload: PetCreate) -> Pet:
|
||||
if pet_id not in _pets:
|
||||
raise KeyError(pet_id)
|
||||
now = _now()
|
||||
pet = _pets[pet_id]
|
||||
updated = pet.model_copy(
|
||||
update={
|
||||
**payload.model_dump(exclude={"id", "metadata"}),
|
||||
"metadata": {"createdOn": pet.metadata.get("createdOn", now), "updatedOn": now}
|
||||
if pet.metadata else None,
|
||||
}
|
||||
)
|
||||
_pets[pet_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_pet(pet_id: int) -> None:
|
||||
del _pets[pet_id]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appointments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_appointments: dict[int, Appointment] = {}
|
||||
_appointments_next_id = 1
|
||||
|
||||
|
||||
def list_appointments() -> list[Appointment]:
|
||||
return list(_appointments.values())
|
||||
|
||||
|
||||
def get_appointment(appointment_id: int) -> Appointment:
|
||||
return _appointments[appointment_id]
|
||||
|
||||
|
||||
def create_appointment(payload: AppointmentCreate) -> Appointment:
|
||||
global _appointments_next_id
|
||||
now = _now()
|
||||
appointment = Appointment(
|
||||
id=_appointments_next_id,
|
||||
**payload.model_dump(exclude={"id"}),
|
||||
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
|
||||
)
|
||||
_appointments[_appointments_next_id] = appointment
|
||||
_appointments_next_id += 1
|
||||
return appointment
|
||||
|
||||
|
||||
def update_appointment(appointment_id: int, payload: AppointmentCreate) -> Appointment:
|
||||
if appointment_id not in _appointments:
|
||||
raise KeyError(appointment_id)
|
||||
now = _now()
|
||||
appointment = _appointments[appointment_id]
|
||||
updated = appointment.model_copy(
|
||||
update={
|
||||
**payload.model_dump(exclude={"id", "metadata"}),
|
||||
"metadata": {"createdOn": appointment.metadata.get("createdOn", now), "updatedOn": now}
|
||||
if appointment.metadata else None,
|
||||
}
|
||||
)
|
||||
_appointments[appointment_id] = updated
|
||||
return updated
|
||||
|
||||
|
||||
def delete_appointment(appointment_id: int) -> None:
|
||||
del _appointments[appointment_id]
|
||||
35
openapi_first/templates/vet_app/main.py
Normal file
35
openapi_first/templates/vet_app/main.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Application entry point for an OpenAPI-first Veterinary Clinic service.
|
||||
|
||||
This module constructs a FastAPI application exclusively from an
|
||||
OpenAPI specification and a handler namespace, without using
|
||||
decorator-driven routing.
|
||||
|
||||
All HTTP routes, methods, request/response schemas, and operation
|
||||
bindings are defined in the OpenAPI document referenced by
|
||||
``openapi_path``. Python callables defined in the ``routes`` module are
|
||||
bound to OpenAPI operations strictly via ``operationId``.
|
||||
|
||||
This module contains no routing logic, persistence concerns, or
|
||||
framework configuration beyond application assembly.
|
||||
|
||||
Design guarantees:
|
||||
- OpenAPI is the single source of truth
|
||||
- No undocumented routes can exist
|
||||
- Every OpenAPI operationId must resolve to exactly one handler
|
||||
- All contract violations fail at application startup
|
||||
|
||||
This file is intended to be used as the ASGI entry point.
|
||||
|
||||
Example:
|
||||
uvicorn main:app
|
||||
"""
|
||||
|
||||
from openapi_first.app import OpenAPIFirstApp
|
||||
import routes
|
||||
|
||||
app = OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="Veterinary Clinic Service",
|
||||
)
|
||||
104
openapi_first/templates/vet_app/models.py
Normal file
104
openapi_first/templates/vet_app/models.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Pydantic domain models for the Veterinary Clinic example.
|
||||
|
||||
This module defines Pydantic models representing the five domain
|
||||
entities used by the veterinary clinic service. These models are
|
||||
referenced by the OpenAPI specification for request and response
|
||||
schemas.
|
||||
|
||||
The models are declarative and framework-agnostic. They contain no
|
||||
persistence logic, validation beyond type constraints, or business
|
||||
behavior.
|
||||
|
||||
This module is not part of the ``openapi_first`` library API surface.
|
||||
It exists solely to support the example application template.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Metadata(BaseModel):
|
||||
createdOn: datetime | None = None
|
||||
updatedOn: datetime | None = None
|
||||
|
||||
|
||||
class ParentBase(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
phone: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class ParentCreate(ParentBase):
|
||||
pass
|
||||
|
||||
|
||||
class Parent(ParentBase):
|
||||
id: int
|
||||
|
||||
|
||||
class VetBase(BaseModel):
|
||||
name: str
|
||||
specialty: str | None = None
|
||||
email: str
|
||||
phone: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class VetCreate(VetBase):
|
||||
pass
|
||||
|
||||
|
||||
class Vet(VetBase):
|
||||
id: int
|
||||
|
||||
|
||||
class TreatmentBase(BaseModel):
|
||||
label: str
|
||||
description: str | None = None
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class TreatmentCreate(TreatmentBase):
|
||||
pass
|
||||
|
||||
|
||||
class Treatment(TreatmentBase):
|
||||
id: int
|
||||
|
||||
|
||||
class PetBase(BaseModel):
|
||||
name: str
|
||||
species: str
|
||||
age: int | None = None
|
||||
weight: float | None = None
|
||||
birthDate: date | None = None
|
||||
photo: str | None = None
|
||||
parent_ids: list[int] = []
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class PetCreate(PetBase):
|
||||
pass
|
||||
|
||||
|
||||
class Pet(PetBase):
|
||||
id: int
|
||||
|
||||
|
||||
class AppointmentBase(BaseModel):
|
||||
date: datetime
|
||||
notes: str | None = None
|
||||
pet_id: int
|
||||
vet_id: int
|
||||
treatment_id: int
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
class AppointmentCreate(AppointmentBase):
|
||||
pass
|
||||
|
||||
|
||||
class Appointment(AppointmentBase):
|
||||
id: int
|
||||
1019
openapi_first/templates/vet_app/openapi.yaml
Normal file
1019
openapi_first/templates/vet_app/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
365
openapi_first/templates/vet_app/routes.py
Normal file
365
openapi_first/templates/vet_app/routes.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Veterinary Clinic route handlers bound via OpenAPI operationId.
|
||||
|
||||
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||
behavior matches the OpenAPI contract. Domain models defined using
|
||||
Pydantic are used for request and response payloads.
|
||||
|
||||
No routing decorators, path definitions, or implicit framework behavior
|
||||
appear in this module. All routing, HTTP methods, and schemas are defined
|
||||
in the OpenAPI specification.
|
||||
"""
|
||||
|
||||
from fastapi import Response, HTTPException, UploadFile
|
||||
|
||||
from models import (
|
||||
ParentCreate,
|
||||
VetCreate,
|
||||
TreatmentCreate,
|
||||
PetCreate,
|
||||
AppointmentCreate,
|
||||
)
|
||||
from data import (
|
||||
list_parents as _list_parents,
|
||||
get_parent as _get_parent,
|
||||
create_parent as _create_parent,
|
||||
update_parent as _update_parent,
|
||||
delete_parent as _delete_parent,
|
||||
list_vets as _list_vets,
|
||||
get_vet as _get_vet,
|
||||
create_vet as _create_vet,
|
||||
update_vet as _update_vet,
|
||||
delete_vet as _delete_vet,
|
||||
list_treatments as _list_treatments,
|
||||
get_treatment as _get_treatment,
|
||||
create_treatment as _create_treatment,
|
||||
update_treatment as _update_treatment,
|
||||
delete_treatment as _delete_treatment,
|
||||
list_pets as _list_pets,
|
||||
get_pet as _get_pet,
|
||||
create_pet as _create_pet,
|
||||
update_pet as _update_pet,
|
||||
delete_pet as _delete_pet,
|
||||
list_appointments as _list_appointments,
|
||||
get_appointment as _get_appointment,
|
||||
create_appointment as _create_appointment,
|
||||
update_appointment as _update_appointment,
|
||||
delete_appointment as _delete_appointment,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_parents(limit: int = 20, offset: int = 0):
|
||||
"""List parents (paginated).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
limit : int
|
||||
Maximum number of records to return.
|
||||
offset : int
|
||||
Number of records to skip.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Paginated response with ``total`` and ``items``.
|
||||
"""
|
||||
items = _list_parents()
|
||||
return {"total": len(items), "items": items[offset:offset + limit]}
|
||||
|
||||
|
||||
def create_parent(payload: ParentCreate, response: Response):
|
||||
"""Create a parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : ParentCreate
|
||||
Parent data excluding the ``id`` field.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The newly created parent.
|
||||
"""
|
||||
parent = _create_parent(payload)
|
||||
response.status_code = 201
|
||||
return parent
|
||||
|
||||
|
||||
def get_parent(id: int):
|
||||
"""Retrieve a single parent by ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The requested parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
return _get_parent(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def update_parent(id: int, payload: ParentCreate):
|
||||
"""Update an existing parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
payload : ParentCreate
|
||||
Updated parent data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Parent
|
||||
The updated parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
return _update_parent(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def delete_parent(id: int, response: Response):
|
||||
"""Delete an existing parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
404 if the parent does not exist.
|
||||
"""
|
||||
try:
|
||||
_delete_parent(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_vets(limit: int = 20, offset: int = 0):
|
||||
"""List vets (paginated)."""
|
||||
items = _list_vets()
|
||||
return {"total": len(items), "items": items[offset:offset + limit]}
|
||||
|
||||
|
||||
def create_vet(payload: VetCreate, response: Response):
|
||||
"""Create a vet."""
|
||||
vet = _create_vet(payload)
|
||||
response.status_code = 201
|
||||
return vet
|
||||
|
||||
|
||||
def get_vet(id: int):
|
||||
"""Retrieve a single vet by ID."""
|
||||
try:
|
||||
return _get_vet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
|
||||
|
||||
def update_vet(id: int, payload: VetCreate):
|
||||
"""Update an existing vet."""
|
||||
try:
|
||||
return _update_vet(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
|
||||
|
||||
def delete_vet(id: int, response: Response):
|
||||
"""Delete an existing vet."""
|
||||
try:
|
||||
_delete_vet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Vet not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Treatments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_treatments():
|
||||
"""List treatments (catalogue).
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Treatment]
|
||||
A list of treatment domain objects.
|
||||
"""
|
||||
return _list_treatments()
|
||||
|
||||
|
||||
def create_treatment(payload: TreatmentCreate, response: Response):
|
||||
"""Add a treatment (admin only)."""
|
||||
treatment = _create_treatment(payload)
|
||||
response.status_code = 201
|
||||
return treatment
|
||||
|
||||
|
||||
def get_treatment(id: int):
|
||||
"""Retrieve a single treatment by ID."""
|
||||
try:
|
||||
return _get_treatment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
|
||||
|
||||
def update_treatment(id: int, payload: TreatmentCreate):
|
||||
"""Update an existing treatment."""
|
||||
try:
|
||||
return _update_treatment(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
|
||||
|
||||
def delete_treatment(id: int, response: Response):
|
||||
"""Delete an existing treatment."""
|
||||
try:
|
||||
_delete_treatment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Treatment not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_pets(limit: int = 20, offset: int = 0):
|
||||
"""List pets (paginated)."""
|
||||
items = _list_pets()
|
||||
return {"total": len(items), "items": items[offset:offset + limit]}
|
||||
|
||||
|
||||
def create_pet(payload: PetCreate, response: Response):
|
||||
"""Create a pet."""
|
||||
pet = _create_pet(payload)
|
||||
response.status_code = 201
|
||||
return pet
|
||||
|
||||
|
||||
def get_pet(id: int):
|
||||
"""Retrieve a single pet by ID."""
|
||||
try:
|
||||
return _get_pet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
|
||||
|
||||
def update_pet(id: int, payload: PetCreate):
|
||||
"""Update an existing pet."""
|
||||
try:
|
||||
return _update_pet(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
|
||||
|
||||
def delete_pet(id: int, response: Response):
|
||||
"""Delete an existing pet."""
|
||||
try:
|
||||
_delete_pet(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Pet not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
def upload_pet_photo(id: int, file: UploadFile):
|
||||
"""Upload a pet photo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the pet.
|
||||
file : UploadFile
|
||||
Image file to upload.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A confirmation with the pet ID.
|
||||
"""
|
||||
_ = file # In a real app, save to disk / object store
|
||||
return {"id": id, "status": "photo_uploaded"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appointments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_appointments(limit: int = 20, offset: int = 0, date: str = None, vet: int = None, pet: int = None):
|
||||
"""List appointments (paginated, filterable)."""
|
||||
items = _list_appointments()
|
||||
|
||||
# Basic in-memory filtering
|
||||
if date:
|
||||
items = [a for a in items if a.date.startswith(date)]
|
||||
if vet is not None:
|
||||
items = [a for a in items if a.vet_id == vet]
|
||||
if pet is not None:
|
||||
items = [a for a in items if a.pet_id == pet]
|
||||
|
||||
return {"total": len(items), "items": items[offset:offset + limit]}
|
||||
|
||||
|
||||
def create_appointment(payload: AppointmentCreate, response: Response):
|
||||
"""Create an appointment."""
|
||||
appointment = _create_appointment(payload)
|
||||
response.status_code = 201
|
||||
return appointment
|
||||
|
||||
|
||||
def get_appointment(id: int):
|
||||
"""Retrieve a single appointment by ID."""
|
||||
try:
|
||||
return _get_appointment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
|
||||
|
||||
def update_appointment(id: int, payload: AppointmentCreate):
|
||||
"""Update an existing appointment."""
|
||||
try:
|
||||
return _update_appointment(id, payload)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
|
||||
|
||||
def delete_appointment(id: int, response: Response):
|
||||
"""Delete an existing appointment."""
|
||||
try:
|
||||
_delete_appointment(id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
response.status_code = 204
|
||||
151
openapi_first/templates/vet_app/test_vet_app.py
Normal file
151
openapi_first/templates/vet_app/test_vet_app.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
End-to-end tests for the OpenAPI-first Veterinary Clinic example app.
|
||||
|
||||
These tests validate that all CRUD operations behave correctly
|
||||
against the in-memory mock data store using Pydantic models.
|
||||
"""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
|
||||
test_client = TestClient(app)
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(
|
||||
spec=spec,
|
||||
base_url="http://testserver",
|
||||
client=test_client,
|
||||
)
|
||||
|
||||
|
||||
def test_list_parents():
|
||||
"""List parents returns paginated response."""
|
||||
response = client.list_parents(query_params={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_create_parent():
|
||||
"""Creating a parent returns 201 with the created entity."""
|
||||
payload = {"name": "Alice", "email": "alice@example.com"}
|
||||
response = client.create_parent(body=payload)
|
||||
assert response.status_code == 201
|
||||
parent = response.json()
|
||||
assert parent["name"] == "Alice"
|
||||
assert "id" in parent
|
||||
|
||||
|
||||
def test_get_parent():
|
||||
"""Get parent by ID returns the entity."""
|
||||
parent = client.create_parent(body={"name": "Bob", "email": "bob@example.com"}).json()
|
||||
response = client.get_parent(path_params={"id": parent["id"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Bob"
|
||||
|
||||
|
||||
def test_update_parent():
|
||||
"""Update parent replaces its values."""
|
||||
parent = client.create_parent(body={"name": "Carol", "email": "carol@example.com"}).json()
|
||||
payload = {"name": "Carol Smith", "email": "carol.smith@example.com"}
|
||||
response = client.update_parent(path_params={"id": parent["id"]}, body=payload)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Carol Smith"
|
||||
|
||||
|
||||
def test_delete_parent():
|
||||
"""Delete parent returns 204 and removes the entity."""
|
||||
parent = client.create_parent(body={"name": "Dave", "email": "dave@example.com"}).json()
|
||||
response = client.delete_parent(path_params={"id": parent["id"]})
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_list_vets():
|
||||
"""List vets returns paginated response."""
|
||||
response = client.list_vets(query_params={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_create_vet():
|
||||
"""Creating a vet returns 201."""
|
||||
payload = {"name": "Dr. Smith", "specialty": "Surgery", "email": "smith@clinic.com"}
|
||||
response = client.create_vet(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Dr. Smith"
|
||||
|
||||
|
||||
def test_list_treatments():
|
||||
"""List treatments returns an array."""
|
||||
response = client.list_treatments()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
|
||||
def test_create_treatment():
|
||||
"""Creating a treatment returns 201."""
|
||||
payload = {"label": "Vaccination", "description": "Annual vaccination"}
|
||||
response = client.create_treatment(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["label"] == "Vaccination"
|
||||
|
||||
|
||||
def test_create_pet():
|
||||
"""Creating a pet links FK references."""
|
||||
parent = client.create_parent(body={"name": "Owner", "email": "owner@example.com"}).json()
|
||||
payload = {"name": "Fido", "species": "dog", "parent_ids": [parent["id"]]}
|
||||
response = client.create_pet(body=payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Fido"
|
||||
|
||||
|
||||
def test_upload_pet_photo():
|
||||
"""Upload pet photo returns 200."""
|
||||
pet = client.create_pet(body={"name": "PhotoPet", "species": "cat", "parent_ids": []}).json()
|
||||
response = client.upload_pet_photo(
|
||||
path_params={"id": pet["id"]},
|
||||
body={},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_list_appointments():
|
||||
"""List appointments returns paginated response with filter params."""
|
||||
response = client.list_appointments(query_params={"limit": 10, "offset": 0})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total" in data
|
||||
assert "items" in data
|
||||
|
||||
|
||||
def test_full_appointment_lifecycle():
|
||||
"""Create a parent, vet, treatment, pet, then an appointment."""
|
||||
parent = client.create_parent(body={"name": "Eve", "email": "eve@example.com"}).json()
|
||||
vet = client.create_vet(body={"name": "Dr. Jones", "specialty": "Dentistry", "email": "jones@clinic.com"}).json()
|
||||
treatment = client.create_treatment(body={"label": "Cleaning", "description": "Teeth cleaning"}).json()
|
||||
pet = client.create_pet(body={"name": "Max", "species": "dog", "parent_ids": [parent["id"]]}).json()
|
||||
|
||||
payload = {
|
||||
"date": "2025-06-01T10:00:00",
|
||||
"pet_id": pet["id"],
|
||||
"vet_id": vet["id"],
|
||||
"treatment_id": treatment["id"],
|
||||
}
|
||||
response = client.create_appointment(body=payload)
|
||||
assert response.status_code == 201
|
||||
appointment = response.json()
|
||||
assert appointment["pet_id"] == pet["id"]
|
||||
|
||||
# Fetch it back
|
||||
get_resp = client.get_appointment(path_params={"id": appointment["id"]})
|
||||
assert get_resp.status_code == 200
|
||||
|
||||
# Delete it
|
||||
del_resp = client.delete_appointment(path_params={"id": appointment["id"]})
|
||||
assert del_resp.status_code == 204
|
||||
Reference in New Issue
Block a user