Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab407df39d | |||
| f7e5a6b4af |
@@ -56,6 +56,7 @@ extra_css:
|
|||||||
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
|
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
|
- docforge-linkfix
|
||||||
site_name: openapi_first
|
site_name: openapi_first
|
||||||
docs_dir: wiki
|
docs_dir: wiki
|
||||||
site_dir: ../site/wiki
|
site_dir: ../site/wiki
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
"""
|
|
||||||
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 (display metadata for resource endpoints):
|
|
||||||
|
|
||||||
``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 scaffold vet_app
|
|
||||||
|
|
||||||
Create the service in a custom directory:
|
|
||||||
|
|
||||||
openapi-first scaffold 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.
|
|
||||||
"""
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
"""
|
|
||||||
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 date, datetime, timezone
|
|
||||||
|
|
||||||
from models import (
|
|
||||||
Parent, ParentCreate,
|
|
||||||
Vet, VetCreate,
|
|
||||||
Treatment, TreatmentCreate,
|
|
||||||
Procedure,
|
|
||||||
BasicNote, HeartRateNote, DentalNote,
|
|
||||||
VaccineNote, PreOpNote, SurgeryNote,
|
|
||||||
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,
|
|
||||||
name=payload.name,
|
|
||||||
email=payload.email,
|
|
||||||
phone=payload.phone,
|
|
||||||
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()
|
|
||||||
current = _parents[parent_id]
|
|
||||||
updated = Parent(
|
|
||||||
id=parent_id,
|
|
||||||
name=payload.name,
|
|
||||||
email=payload.email,
|
|
||||||
phone=payload.phone if payload.phone is not None else current.phone,
|
|
||||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
|
||||||
)
|
|
||||||
_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,
|
|
||||||
name=payload.name,
|
|
||||||
specialty=payload.specialty,
|
|
||||||
email=payload.email,
|
|
||||||
phone=payload.phone,
|
|
||||||
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()
|
|
||||||
current = _vets[vet_id]
|
|
||||||
updated = Vet(
|
|
||||||
id=vet_id,
|
|
||||||
name=payload.name,
|
|
||||||
specialty=payload.specialty if payload.specialty is not None else current.specialty,
|
|
||||||
email=payload.email,
|
|
||||||
phone=payload.phone if payload.phone is not None else current.phone,
|
|
||||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
|
||||||
)
|
|
||||||
_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,
|
|
||||||
label=payload.label,
|
|
||||||
description=payload.description,
|
|
||||||
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()
|
|
||||||
current = _treatments[treatment_id]
|
|
||||||
updated = Treatment(
|
|
||||||
id=treatment_id,
|
|
||||||
label=payload.label,
|
|
||||||
description=payload.description if payload.description is not None else current.description,
|
|
||||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
|
||||||
)
|
|
||||||
_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()
|
|
||||||
parents = [_parents[pid] for pid in payload.parent_ids]
|
|
||||||
pet = Pet(
|
|
||||||
id=_pets_next_id,
|
|
||||||
name=payload.name,
|
|
||||||
species=payload.species,
|
|
||||||
age=payload.age,
|
|
||||||
weight=payload.weight,
|
|
||||||
birthDate=payload.birthDate,
|
|
||||||
photo=payload.photo,
|
|
||||||
parents=parents,
|
|
||||||
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()
|
|
||||||
parents = [_parents[pid] for pid in payload.parent_ids]
|
|
||||||
current = _pets[pet_id]
|
|
||||||
updated = Pet(
|
|
||||||
id=pet_id,
|
|
||||||
name=payload.name,
|
|
||||||
species=payload.species,
|
|
||||||
age=payload.age if payload.age is not None else current.age,
|
|
||||||
weight=payload.weight if payload.weight is not None else current.weight,
|
|
||||||
birthDate=payload.birthDate if payload.birthDate is not None else current.birthDate,
|
|
||||||
photo=payload.photo if payload.photo is not None else current.photo,
|
|
||||||
parents=parents,
|
|
||||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
|
||||||
)
|
|
||||||
_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,
|
|
||||||
date=payload.date,
|
|
||||||
notes=payload.notes,
|
|
||||||
procedures=payload.procedures,
|
|
||||||
pet=_pets[payload.pet_id],
|
|
||||||
vet=_vets[payload.vet_id],
|
|
||||||
treatment=_treatments[payload.treatment_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()
|
|
||||||
current = _appointments[appointment_id]
|
|
||||||
updated = Appointment(
|
|
||||||
id=appointment_id,
|
|
||||||
date=payload.date,
|
|
||||||
notes=payload.notes if payload.notes is not None else current.notes,
|
|
||||||
procedures=payload.procedures,
|
|
||||||
pet=_pets.get(payload.pet_id, current.pet),
|
|
||||||
vet=_vets.get(payload.vet_id, current.vet),
|
|
||||||
treatment=_treatments.get(payload.treatment_id, current.treatment),
|
|
||||||
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
|
|
||||||
)
|
|
||||||
_appointments[appointment_id] = updated
|
|
||||||
return updated
|
|
||||||
|
|
||||||
|
|
||||||
def delete_appointment(appointment_id: int) -> None:
|
|
||||||
del _appointments[appointment_id]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Seed data — populate stores so the UI isn't empty on startup
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _seed_data():
|
|
||||||
now = _now()
|
|
||||||
meta = {"createdOn": now, "updatedOn": now}
|
|
||||||
global _parents_next_id, _vets_next_id, _treatments_next_id
|
|
||||||
global _pets_next_id, _appointments_next_id
|
|
||||||
|
|
||||||
_parents[1] = Parent(id=1, name="Alice Johnson", email="alice@example.com", phone="5550101", metadata=meta)
|
|
||||||
_parents[2] = Parent(id=2, name="Bob Smith", email="bob@example.com", phone="5550102", metadata=meta)
|
|
||||||
_parents[3] = Parent(id=3, name="Carol Williams", email="carol@example.com", phone="5550103", metadata=meta)
|
|
||||||
_parents[4] = Parent(id=4, name="Dave Brown", email="dave@example.com", phone="5550104", metadata=meta)
|
|
||||||
_parents_next_id = 5
|
|
||||||
|
|
||||||
_vets[1] = Vet(id=1, name="Sarah Connor", specialty="Surgery", email="sarah@clinic.com", phone="5550201", metadata=meta)
|
|
||||||
_vets[2] = Vet(id=2, name="James Wilson", specialty="Dentistry", email="james@clinic.com", phone="5550202", metadata=meta)
|
|
||||||
_vets[3] = Vet(id=3, name="Emily Davis", specialty="General Practice", email="emily@clinic.com", phone="5550203", metadata=meta)
|
|
||||||
_vets_next_id = 4
|
|
||||||
|
|
||||||
_treatments[1] = Treatment(id=1, label="Annual Checkup", description="Full physical examination", metadata=meta)
|
|
||||||
_treatments[2] = Treatment(id=2, label="Vaccination", description="Core vaccines for common diseases", metadata=meta)
|
|
||||||
_treatments[3] = Treatment(id=3, label="Dental Cleaning", description="Scaling, polishing, and oral exam", metadata=meta)
|
|
||||||
_treatments[4] = Treatment(id=4, label="Spay/Neuter", description="Surgical sterilization", metadata=meta)
|
|
||||||
_treatments[5] = Treatment(id=5, label="Blood Panel", description="Complete blood count and chemistry", metadata=meta)
|
|
||||||
_treatments_next_id = 6
|
|
||||||
|
|
||||||
_pets[1] = Pet(id=1, name="Max", species="dog", age=4, weight=25.5, birthDate=date(2022, 3, 15), parents=[_parents[1]], metadata=meta)
|
|
||||||
_pets[2] = Pet(id=2, name="Luna", species="cat", age=2, weight=4.2, birthDate=date(2024, 1, 10), parents=[_parents[1], _parents[2]], metadata=meta)
|
|
||||||
_pets[3] = Pet(id=3, name="Charlie", species="dog", age=7, weight=18.0, birthDate=date(2019, 8, 22), parents=[_parents[2]], metadata=meta)
|
|
||||||
_pets[4] = Pet(id=4, name="Bella", species="bird", age=1, weight=0.3, birthDate=date(2025, 5, 1), parents=[_parents[3]], metadata=meta)
|
|
||||||
_pets[5] = Pet(id=5, name="Rocky", species="dog", age=3, weight=30.0, birthDate=date(2023, 11, 5), parents=[_parents[4]], metadata=meta)
|
|
||||||
_pets_next_id = 6
|
|
||||||
|
|
||||||
_appointments[1] = Appointment(id=1, date=datetime(2026, 6, 18, 9, 0, tzinfo=timezone.utc), notes="Annual checkup",
|
|
||||||
procedures=[
|
|
||||||
Procedure(name="Physical Exam", cost=50.0, notes=BasicNote(summary="Normal findings", details="Heart rate and temperature within normal range")),
|
|
||||||
Procedure(name="Heart Rate", notes=HeartRateNote(summary="Normal rhythm", bpm=65)),
|
|
||||||
],
|
|
||||||
pet=_pets[1], vet=_vets[1], treatment=_treatments[1], metadata=meta)
|
|
||||||
_appointments[2] = Appointment(id=2, date=datetime(2026, 6, 18, 10, 30, tzinfo=timezone.utc), notes="Dental cleaning",
|
|
||||||
procedures=[
|
|
||||||
Procedure(name="Scaling", cost=80.0, notes=DentalNote(summary="Moderate tartar removed", procedureType="scaling", teeth="all")),
|
|
||||||
Procedure(name="Polishing", cost=40.0, notes=DentalNote(summary="High-speed polish applied", procedureType="polishing", teeth="all")),
|
|
||||||
],
|
|
||||||
pet=_pets[2], vet=_vets[2], treatment=_treatments[3], metadata=meta)
|
|
||||||
_appointments[3] = Appointment(id=3, date=datetime(2026, 6, 19, 11, 0, tzinfo=timezone.utc), notes="Vaccination booster",
|
|
||||||
procedures=[
|
|
||||||
Procedure(name="Vaccine", cost=35.0, notes=VaccineNote(summary="Administered", medicine="DHPP", leg="hind_left")),
|
|
||||||
Procedure(name="Vaccine", cost=45.0, notes=VaccineNote(summary="Administered", medicine="Rabies", leg="hind_right")),
|
|
||||||
],
|
|
||||||
pet=_pets[3], vet=_vets[3], treatment=_treatments[2], metadata=meta)
|
|
||||||
_appointments[4] = Appointment(id=4, date=datetime(2026, 6, 20, 14, 0, tzinfo=timezone.utc), notes="Follow-up after surgery",
|
|
||||||
procedures=[
|
|
||||||
Procedure(name="PreOp", cost=30.0, notes=PreOpNote(summary="Pre-op clearance", heartRate=80, temperature=38.5)),
|
|
||||||
Procedure(name="Neuter", cost=200.0, notes=SurgeryNote(summary="Surgery completed", surgeryType="neuter", complications="None")),
|
|
||||||
Procedure(name="PostOp", cost=50.0, notes=BasicNote(summary="Recovering well", details="Eating and drinking normally")),
|
|
||||||
],
|
|
||||||
pet=_pets[5], vet=_vets[1], treatment=_treatments[4], metadata=meta)
|
|
||||||
_appointments_next_id = 5
|
|
||||||
|
|
||||||
|
|
||||||
_seed_data()
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
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 contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
from openapi_first.app import OpenAPIFirstApp
|
|
||||||
import routes
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app):
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = OpenAPIFirstApp(
|
|
||||||
openapi_path="openapi.yaml",
|
|
||||||
routes_module=routes,
|
|
||||||
title="Veterinary Clinic Service",
|
|
||||||
lifespan=lifespan,
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["http://localhost:5173"],
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
from datetime import date, datetime
|
|
||||||
from typing import Annotated, Literal
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
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 ProcedureNoteBase(BaseModel):
|
|
||||||
summary: str | None = None
|
|
||||||
details: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class BasicNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["basic"] = "basic"
|
|
||||||
|
|
||||||
|
|
||||||
class HeartRateNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["heart_rate"] = "heart_rate"
|
|
||||||
bpm: int
|
|
||||||
|
|
||||||
|
|
||||||
class DentalNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["dental"] = "dental"
|
|
||||||
procedureType: Literal["scaling", "polishing"]
|
|
||||||
teeth: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class VaccineNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["vaccine"] = "vaccine"
|
|
||||||
medicine: Literal["Rabies", "DHPP", "Tricat", "Deworming"]
|
|
||||||
leg: Literal["front_left", "front_right", "hind_left", "hind_right"]
|
|
||||||
|
|
||||||
|
|
||||||
class PreOpNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["preop"] = "preop"
|
|
||||||
heartRate: int
|
|
||||||
temperature: float
|
|
||||||
|
|
||||||
|
|
||||||
class SurgeryNote(ProcedureNoteBase):
|
|
||||||
noteType: Literal["surgery"] = "surgery"
|
|
||||||
surgeryType: Literal["neuter", "spay"]
|
|
||||||
complications: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
ProcedureNotes = Annotated[
|
|
||||||
BasicNote | HeartRateNote | DentalNote | VaccineNote | PreOpNote | SurgeryNote,
|
|
||||||
Field(discriminator="noteType"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Procedure(BaseModel):
|
|
||||||
name: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
cost: float | None = None
|
|
||||||
notes: ProcedureNotes | None = None
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
metadata: Metadata | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class PetCreate(PetBase):
|
|
||||||
parent_ids: list[int] = []
|
|
||||||
|
|
||||||
|
|
||||||
class Pet(PetBase):
|
|
||||||
id: int
|
|
||||||
parents: list[Parent] = []
|
|
||||||
|
|
||||||
|
|
||||||
class AppointmentBase(BaseModel):
|
|
||||||
date: datetime
|
|
||||||
notes: str | None = None
|
|
||||||
procedures: list[Procedure] = []
|
|
||||||
metadata: Metadata | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class AppointmentCreate(AppointmentBase):
|
|
||||||
pet_id: int
|
|
||||||
vet_id: int
|
|
||||||
treatment_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class Appointment(AppointmentBase):
|
|
||||||
id: int
|
|
||||||
pet: Pet
|
|
||||||
vet: Vet
|
|
||||||
treatment: Treatment
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,392 +0,0 @@
|
|||||||
"""
|
|
||||||
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 fastapi.responses import StreamingResponse
|
|
||||||
|
|
||||||
from sse import subscribe, unsubscribe
|
|
||||||
|
|
||||||
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) -> dict:
|
|
||||||
"""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] if limit else items[offset:]}
|
|
||||||
|
|
||||||
|
|
||||||
def create_parent(payload: ParentCreate, response: Response) -> Parent:
|
|
||||||
"""Create a parent.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
payload : ParentCreate
|
|
||||||
Parent data excluding the ``id`` field.
|
|
||||||
response : Response
|
|
||||||
Response object used to set the HTTP status code.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Parent
|
|
||||||
The newly created parent.
|
|
||||||
"""
|
|
||||||
parent = _create_parent(payload)
|
|
||||||
response.status_code = 201
|
|
||||||
return parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_parent(id: int) -> Parent:
|
|
||||||
"""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) -> Parent:
|
|
||||||
"""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) -> None:
|
|
||||||
"""Delete an existing parent.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
id : int
|
|
||||||
Identifier of the parent.
|
|
||||||
response : Response
|
|
||||||
Response object used to set the HTTP status code.
|
|
||||||
|
|
||||||
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] if limit else items[offset:]}
|
|
||||||
|
|
||||||
|
|
||||||
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[Treatment]:
|
|
||||||
"""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] if limit else items[offset:]}
|
|
||||||
|
|
||||||
|
|
||||||
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) -> dict:
|
|
||||||
"""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] if limit else items[offset:]}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_actions(id: int):
|
|
||||||
"""Stream animal actions via SSE, scoped to a pet's species."""
|
|
||||||
try:
|
|
||||||
pet = _get_pet(id)
|
|
||||||
except KeyError:
|
|
||||||
raise HTTPException(status_code=404, detail="Pet not found")
|
|
||||||
species = pet.species
|
|
||||||
q = await subscribe(id, species)
|
|
||||||
|
|
||||||
async def event_generator():
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = await q.get()
|
|
||||||
yield f"data: {data}\n\n"
|
|
||||||
finally:
|
|
||||||
unsubscribe(id, q)
|
|
||||||
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import random
|
|
||||||
import json
|
|
||||||
|
|
||||||
_behaviors_by_species = {
|
|
||||||
"dog": ["wagging_tail", "stretching", "showing_belly", "barking", "panting"],
|
|
||||||
"cat": ["blep", "stretching", "showing_belly", "purring", "kneading"],
|
|
||||||
"bird": ["head_tilt", "stretching", "chirping", "wing_flap"],
|
|
||||||
}
|
|
||||||
_subscribers: dict[int, list[asyncio.Queue]] = {}
|
|
||||||
_worker_tasks: dict[int, asyncio.Task] = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def _behavior_worker(pet_id: int, species: str):
|
|
||||||
behaviors = _behaviors_by_species.get(species, ["wagging_tail"])
|
|
||||||
while True:
|
|
||||||
action = random.choice(behaviors)
|
|
||||||
data = json.dumps({"action": action})
|
|
||||||
queues = _subscribers.get(pet_id, [])
|
|
||||||
for q in queues:
|
|
||||||
await q.put(data)
|
|
||||||
await asyncio.sleep(random.uniform(1, 5))
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_worker(pet_id: int, species: str):
|
|
||||||
if pet_id not in _worker_tasks or _worker_tasks[pet_id].done():
|
|
||||||
_subscribers.setdefault(pet_id, [])
|
|
||||||
_worker_tasks[pet_id] = asyncio.create_task(
|
|
||||||
_behavior_worker(pet_id, species)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def subscribe(pet_id: int, species: str) -> asyncio.Queue:
|
|
||||||
q: asyncio.Queue = asyncio.Queue()
|
|
||||||
_subscribers.setdefault(pet_id, []).append(q)
|
|
||||||
_ensure_worker(pet_id, species)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def unsubscribe(pet_id: int, q: asyncio.Queue):
|
|
||||||
queues = _subscribers.get(pet_id, [])
|
|
||||||
if q in queues:
|
|
||||||
queues.remove(q)
|
|
||||||
if not queues:
|
|
||||||
task = _worker_tasks.pop(pet_id, None)
|
|
||||||
if task and not task.done():
|
|
||||||
task.cancel()
|
|
||||||
_subscribers.pop(pet_id, None)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"""
|
|
||||||
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={"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={"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.client.post(
|
|
||||||
f"http://testserver/pets/{pet['id']}",
|
|
||||||
files={"file": ("test.jpg", b"fake-image-data", "image/jpeg")},
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
def test_list_appointments():
|
|
||||||
"""List appointments returns paginated response with filter params."""
|
|
||||||
response = client.list_appointments(query={"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