diff --git a/openapi_first/templates/vet_app/__init__.py b/openapi_first/templates/vet_app/__init__.py deleted file mode 100644 index 934273a..0000000 --- a/openapi_first/templates/vet_app/__init__.py +++ /dev/null @@ -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. -""" diff --git a/openapi_first/templates/vet_app/data.py b/openapi_first/templates/vet_app/data.py deleted file mode 100644 index b48082c..0000000 --- a/openapi_first/templates/vet_app/data.py +++ /dev/null @@ -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() diff --git a/openapi_first/templates/vet_app/main.py b/openapi_first/templates/vet_app/main.py deleted file mode 100644 index 8bab330..0000000 --- a/openapi_first/templates/vet_app/main.py +++ /dev/null @@ -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=["*"], -) diff --git a/openapi_first/templates/vet_app/models.py b/openapi_first/templates/vet_app/models.py deleted file mode 100644 index fe98dd0..0000000 --- a/openapi_first/templates/vet_app/models.py +++ /dev/null @@ -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 diff --git a/openapi_first/templates/vet_app/openapi.yaml b/openapi_first/templates/vet_app/openapi.yaml deleted file mode 100644 index ee0a29c..0000000 --- a/openapi_first/templates/vet_app/openapi.yaml +++ /dev/null @@ -1,1230 +0,0 @@ -openapi: 3.1.0 - -info: - title: Veterinary Clinic API - version: 1.0.0 - description: | - Veterinary clinic management API demonstrating OpenAPI x- extension - fields for the react-openapi admin panel renderer. - -servers: - - url: http://localhost:8000 - -components: - schemas: - Metadata: - type: object - x-display-format: "created:{createdOn}|updated:{updatedOn}" - properties: - createdOn: - type: string - format: date-time - updatedOn: - type: string - format: date-time - - Procedure: - type: object - x-display-format: "{name} — ${cost}" - properties: - name: - type: string - x-order: 1 - x-label: "Procedure Name" - description: - type: string - x-order: 2 - x-label: "Description" - cost: - type: number - format: float - x-order: 3 - x-label: "Cost" - notes: - $ref: '#/components/schemas/ProcedureNotes' - x-order: 4 - x-label: "Notes" - - ProcedureNoteBase: - type: object - properties: - summary: - type: string - x-order: 1 - x-label: "Summary" - details: - type: string - x-order: 2 - x-label: "Details" - - ProcedureNotes: - x-display-format: "{summary}" - oneOf: - - $ref: '#/components/schemas/BasicNote' - - $ref: '#/components/schemas/HeartRateNote' - - $ref: '#/components/schemas/DentalNote' - - $ref: '#/components/schemas/VaccineNote' - - $ref: '#/components/schemas/PreOpNote' - - $ref: '#/components/schemas/SurgeryNote' - discriminator: - propertyName: noteType - mapping: - basic: BasicNote - heart_rate: HeartRateNote - dental: DentalNote - vaccine: VaccineNote - preop: PreOpNote - surgery: SurgeryNote - - BasicNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [basic] - x-order: 0 - x-label: "Type" - required: [noteType] - - HeartRateNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [heart_rate] - x-order: 0 - x-label: "Type" - bpm: - type: integer - x-order: 3 - x-label: "Heart Rate (bpm)" - required: [noteType, bpm] - - DentalNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [dental] - x-order: 0 - x-label: "Type" - procedureType: - type: string - enum: [scaling, polishing] - x-order: 3 - x-label: "Procedure" - teeth: - type: string - enum: [all, upper_only, lower_only] - x-order: 4 - x-label: "Teeth" - required: [noteType, procedureType] - - VaccineNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [vaccine] - x-order: 0 - x-label: "Type" - medicine: - type: string - enum: [Rabies, DHPP, Tricat, Deworming] - x-order: 3 - x-label: "Medicine" - leg: - type: string - enum: [front_left, front_right, hind_left, hind_right] - x-order: 4 - x-label: "Injection Leg" - required: [noteType, medicine, leg] - - PreOpNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [preop] - x-order: 0 - x-label: "Type" - heartRate: - type: integer - x-order: 3 - x-label: "Heart Rate (bpm)" - temperature: - type: number - format: float - x-order: 4 - x-label: "Temperature (°C)" - required: [noteType, heartRate, temperature] - - SurgeryNote: - allOf: - - $ref: '#/components/schemas/ProcedureNoteBase' - - type: object - properties: - noteType: - type: string - enum: [surgery] - x-order: 0 - x-label: "Type" - surgeryType: - type: string - enum: [neuter, spay] - x-order: 3 - x-label: "Surgery Type" - complications: - type: string - x-order: 4 - x-label: "Complications" - required: [noteType, surgeryType] - - Action: - type: object - x-primary-key: _received_at - x-display-format: "{action}" - x-list-columns: [action] - properties: - action: - type: string - enum: [wagging_tail, blep, stretching, showing_belly, barking, panting, purring, kneading, head_tilt, chirping, wing_flap] - x-label: "Action" - x-order: 1 - x-filterable: false - required: [action] - - Parent: - type: object - x-primary-key: id - x-display-format: "{name}" - x-list-columns: [name, email, phone] - properties: - id: - type: integer - readOnly: true - x-order: 0 - x-hidden: { form: true, list: true } - x-label: "ID" - name: - type: string - x-order: 1 - x-label: "Name" - x-description: "Parent's full name" - x-filterable: true - x-sortable: true - x-autocomplete: token - email: - type: string - format: email - x-order: 2 - x-label: "Email" - x-description: "Email address" - x-filterable: true - x-autocomplete: email - phone: - type: string - x-order: 3 - x-label: "Phone" - x-description: "Contact phone number" - x-filterable: true - x-autocomplete: phone - metadata: - $ref: '#/components/schemas/Metadata' - x-order: 4 - x-label: "Metadata" - required: [id, name, email] - - Vet: - type: object - x-primary-key: id - x-display-format: "Dr. {name}" - x-list-columns: [name, specialty, email, phone] - properties: - id: - type: integer - readOnly: true - x-order: 0 - x-hidden: { form: true, list: true } - x-label: "ID" - name: - type: string - x-order: 1 - x-label: "Name" - x-description: "Veterinarian's full name" - x-filterable: true - x-sortable: true - x-autocomplete: token - specialty: - type: string - x-order: 2 - x-label: "Specialty" - x-description: "Area of specialization" - x-filterable: true - x-autocomplete: token - email: - type: string - format: email - x-order: 3 - x-label: "Email" - x-description: "Email address" - x-filterable: true - x-autocomplete: email - phone: - type: string - x-order: 4 - x-label: "Phone" - x-description: "Contact phone number" - metadata: - $ref: '#/components/schemas/Metadata' - x-order: 5 - x-label: "Metadata" - required: [id, name] - - Treatment: - type: object - x-primary-key: id - x-display-format: "{label}" - x-list-columns: [label, description] - properties: - id: - type: integer - readOnly: true - x-order: 0 - x-hidden: { form: true, list: true } - x-label: "ID" - label: - type: string - x-order: 1 - x-label: "Treatment" - x-description: "Name of the treatment" - x-filterable: true - x-sortable: true - x-autocomplete: token - description: - type: string - x-order: 2 - x-label: "Description" - x-description: "Detailed description of the treatment" - x-filterable: true - x-autocomplete: text - metadata: - $ref: '#/components/schemas/Metadata' - x-order: 4 - x-label: "Metadata" - required: [id, label] - - Pet: - type: object - x-primary-key: id - x-display-format: "{name} – #{id}" - x-list-columns: [name, species, age, weight, birthDate, parents] - properties: - id: - type: integer - readOnly: true - x-order: 0 - x-hidden: { form: true, list: true } - x-label: "ID" - name: - type: string - x-order: 1 - x-label: "Pet Name" - x-description: "Name of the pet" - x-filterable: true - x-sortable: true - x-autocomplete: token - species: - type: string - enum: [dog, cat, bird] - x-order: 2 - x-label: "Species" - x-description: "Type of animal" - x-filterable: true - age: - type: integer - x-order: 3 - x-label: "Age" - x-description: "Age in years" - x-filterable: true - x-sortable: true - weight: - type: number - format: float - x-order: 4 - x-label: "Weight" - x-description: "Weight in kilograms" - birthDate: - type: string - format: date - x-order: 5 - x-label: "Date of Birth" - x-description: "Pet's birth date" - x-filterable: true - photo: - type: string - format: binary - x-ui-type: image - x-upload-url: /pets/{id}/photo - x-order: 6 - x-label: "Photo" - x-description: "Upload a photo of the pet" - parents: - type: array - items: - $ref: '#/components/schemas/Parent' - x-fk: - resource: parents - x-order: 7 - x-label: "Parents" - x-description: "Pet's owners" - x-filterable: true - metadata: - $ref: '#/components/schemas/Metadata' - x-order: 8 - x-label: "Metadata" - required: [id, name, parents] - - Appointment: - type: object - x-primary-key: id - x-display-format: "Appt #{id} – {date}" - x-list-columns: [date, pet, vet, treatment, notes] - properties: - id: - type: integer - readOnly: true - x-order: 0 - x-hidden: { form: true, list: true } - x-label: "ID" - date: - type: string - format: date-time - x-order: 1 - x-label: "Date & Time" - x-description: "Appointment date and time" - x-filterable: true - x-sortable: true - notes: - type: string - x-order: 2 - x-label: "Notes" - x-description: "Any additional notes" - procedures: - type: array - items: - $ref: '#/components/schemas/Procedure' - x-order: 3 - x-label: "Procedures" - x-description: "List of procedures performed during this appointment" - pet: - $ref: '#/components/schemas/Pet' - x-fk: - resource: pets - x-order: 4 - x-label: "Pet" - x-description: "Select a pet" - x-filterable: true - vet: - $ref: '#/components/schemas/Vet' - x-fk: - resource: vets - prefetch: true - x-order: 5 - x-label: "Veterinarian" - x-description: "Select a veterinarian" - x-filterable: true - treatment: - $ref: '#/components/schemas/Treatment' - x-fk: - resource: treatments - prefetch: true - x-order: 6 - x-label: "Treatment" - x-description: "Select a treatment" - x-filterable: true - metadata: - $ref: '#/components/schemas/Metadata' - x-order: 7 - x-label: "Metadata" - required: [id, date, pet, vet, treatment] - - ErrorBody: - type: object - properties: - detail: - type: string - required: [detail] - - HTTPValidationError: - type: object - properties: - detail: - type: array - items: - $ref: '#/components/schemas/ValidationError' - - ValidationError: - type: object - properties: - loc: - type: array - items: - type: string - msg: - type: string - type: - type: string - required: [loc, msg, type] - - responses: - Unauthorized: - description: Not authenticated - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorBody' - Forbidden: - description: Insufficient permissions - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorBody' - NotFound: - description: Resource not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorBody' - ValidationError: - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - InternalServerError: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorBody' - -paths: - /parents: - get: - summary: List parents (paginated) - operationId: list_parents - parameters: - - in: query - name: limit - schema: {type: integer, default: 20} - - in: query - name: offset - schema: {type: integer, default: 0} - responses: - '200': - description: Paginated list of parents - content: - application/json: - schema: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/Parent' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Create a parent - operationId: create_parent - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Parent' - responses: - '201': - description: Parent created - content: - application/json: - schema: - $ref: '#/components/schemas/Parent' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - /parents/{id}: - get: - operationId: get_parent - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: Single parent - content: - application/json: - schema: - $ref: '#/components/schemas/Parent' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - put: - operationId: update_parent - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Parent' - responses: - '200': - description: Parent updated - content: - application/json: - schema: - $ref: '#/components/schemas/Parent' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: delete_parent - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '204': - description: Parent deleted - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - - /vets: - get: - summary: List vets (paginated) - operationId: list_vets - parameters: - - in: query - name: limit - schema: {type: integer, default: 20} - - in: query - name: offset - schema: {type: integer, default: 0} - responses: - '200': - description: Paginated list of vets - content: - application/json: - schema: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/Vet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Create a vet - operationId: create_vet - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Vet' - responses: - '201': - description: Vet created - content: - application/json: - schema: - $ref: '#/components/schemas/Vet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - /vets/{id}: - get: - operationId: get_vet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: Single vet - content: - application/json: - schema: - $ref: '#/components/schemas/Vet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - put: - operationId: update_vet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Vet' - responses: - '200': - description: Vet updated - content: - application/json: - schema: - $ref: '#/components/schemas/Vet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: delete_vet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '204': - description: Vet deleted - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - - /treatments: - get: - summary: List treatments (catalogue) - operationId: list_treatments - responses: - '200': - description: List of treatments - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Treatment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Add a treatment (admin only) - operationId: create_treatment - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Treatment' - responses: - '201': - description: Treatment added - content: - application/json: - schema: - $ref: '#/components/schemas/Treatment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - /treatments/{id}: - get: - operationId: get_treatment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: Single treatment - content: - application/json: - schema: - $ref: '#/components/schemas/Treatment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - put: - operationId: update_treatment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Treatment' - responses: - '200': - description: Treatment updated - content: - application/json: - schema: - $ref: '#/components/schemas/Treatment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: delete_treatment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '204': - description: Treatment deleted - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - - /pets: - get: - summary: List pets (paginated) - operationId: list_pets - parameters: - - in: query - name: limit - schema: {type: integer, default: 20} - - in: query - name: offset - schema: {type: integer, default: 0} - responses: - '200': - description: Paginated list of pets - content: - application/json: - schema: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/Pet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Create a pet - operationId: create_pet - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - responses: - '201': - description: Pet created - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - /pets/{id}: - get: - operationId: get_pet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: Single pet - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - put: - operationId: update_pet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - responses: - '200': - description: Pet updated - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: delete_pet - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '204': - description: Pet deleted - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Upload pet photo - operationId: upload_pet_photo - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - responses: - '200': - description: Photo uploaded - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - /pets/{id}/actions: - get: - summary: Stream animal actions via SSE, scoped to a pet's species - operationId: stream_actions - x-sse: true - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: SSE stream of random animal actions (behaviors) - content: - text/event-stream: - schema: - $ref: '#/components/schemas/Action' - - /appointments: - get: - summary: List appointments (paginated, filterable) - operationId: list_appointments - parameters: - - in: query - name: limit - schema: {type: integer, default: 20} - - in: query - name: offset - schema: {type: integer, default: 0} - - in: query - name: date - schema: {type: string, format: date} - - in: query - name: vet - schema: {type: integer} - - in: query - name: pet - schema: {type: integer} - responses: - '200': - description: Paginated list of appointments - content: - application/json: - schema: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/Appointment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - summary: Create an appointment - operationId: create_appointment - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Appointment' - responses: - '201': - description: Appointment created - content: - application/json: - schema: - $ref: '#/components/schemas/Appointment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - - /appointments/{id}: - get: - operationId: get_appointment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '200': - description: Single appointment - content: - application/json: - schema: - $ref: '#/components/schemas/Appointment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - put: - operationId: update_appointment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Appointment' - responses: - '200': - description: Appointment updated - content: - application/json: - schema: - $ref: '#/components/schemas/Appointment' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: delete_appointment - parameters: - - name: id - in: path - required: true - schema: {type: integer} - responses: - '204': - description: Appointment deleted - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/ValidationError' - '500': - $ref: '#/components/responses/InternalServerError' diff --git a/openapi_first/templates/vet_app/routes.py b/openapi_first/templates/vet_app/routes.py deleted file mode 100644 index 0e29201..0000000 --- a/openapi_first/templates/vet_app/routes.py +++ /dev/null @@ -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") diff --git a/openapi_first/templates/vet_app/sse.py b/openapi_first/templates/vet_app/sse.py deleted file mode 100644 index c688165..0000000 --- a/openapi_first/templates/vet_app/sse.py +++ /dev/null @@ -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) diff --git a/openapi_first/templates/vet_app/test_vet_app.py b/openapi_first/templates/vet_app/test_vet_app.py deleted file mode 100644 index 33474ec..0000000 --- a/openapi_first/templates/vet_app/test_vet_app.py +++ /dev/null @@ -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