Compare commits
4 Commits
d912053368
...
richer-ope
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a7a76e330 | |||
| 083fb6923d | |||
| 69b795f9ca | |||
| 61de233745 |
@@ -22,6 +22,7 @@ from models import (
|
||||
Parent, ParentCreate,
|
||||
Vet, VetCreate,
|
||||
Treatment, TreatmentCreate,
|
||||
Procedure, ProcedureNotes,
|
||||
Pet, PetCreate,
|
||||
Appointment, AppointmentCreate,
|
||||
)
|
||||
@@ -268,6 +269,7 @@ def create_appointment(payload: AppointmentCreate) -> 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],
|
||||
@@ -287,6 +289,7 @@ def update_appointment(appointment_id: int, payload: AppointmentCreate) -> Appoi
|
||||
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),
|
||||
@@ -335,10 +338,18 @@ def _seed_data():
|
||||
_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", 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", 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", 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", pet=_pets[5], vet=_vets[1], treatment=_treatments[4], metadata=meta)
|
||||
_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), Procedure(name="Heart Rate", notes=ProcedureNotes(summary="Normal rhythm"))],
|
||||
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), Procedure(name="Polishing", cost=40.0, notes=ProcedureNotes(summary="High-speed polish"))],
|
||||
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="DHPP Vaccine", cost=35.0), Procedure(name="Rabies Vaccine", cost=45.0)],
|
||||
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="Pre-op Exam", cost=30.0), Procedure(name="Surgery", cost=200.0), Procedure(name="Post-op Care", cost=50.0)],
|
||||
pet=_pets[5], vet=_vets[1], treatment=_treatments[4], metadata=meta)
|
||||
_appointments_next_id = 5
|
||||
|
||||
|
||||
|
||||
@@ -25,15 +25,29 @@ Example:
|
||||
uvicorn main:app
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from openapi_first.app import OpenAPIFirstApp
|
||||
import routes
|
||||
from sse import start_worker, stop_worker
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
start_worker()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stop_worker()
|
||||
|
||||
|
||||
app = OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="Veterinary Clinic Service",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
|
||||
@@ -38,6 +38,18 @@ class Vet(VetBase):
|
||||
id: int
|
||||
|
||||
|
||||
class ProcedureNotes(BaseModel):
|
||||
summary: str | None = None
|
||||
details: str | None = None
|
||||
|
||||
|
||||
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
|
||||
@@ -74,6 +86,7 @@ class Pet(PetBase):
|
||||
class AppointmentBase(BaseModel):
|
||||
date: datetime
|
||||
notes: str | None = None
|
||||
procedures: list[Procedure] = []
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,56 @@ components:
|
||||
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"
|
||||
|
||||
ProcedureNotes:
|
||||
type: object
|
||||
x-display-format: "{summary}"
|
||||
properties:
|
||||
summary:
|
||||
type: string
|
||||
x-order: 1
|
||||
x-label: "Summary"
|
||||
details:
|
||||
type: string
|
||||
x-order: 2
|
||||
x-label: "Details"
|
||||
|
||||
Call:
|
||||
type: object
|
||||
x-resource: calls
|
||||
x-primary-key: _received_at
|
||||
x-display-format: "{sound}"
|
||||
x-list-columns: [sound]
|
||||
properties:
|
||||
sound:
|
||||
type: string
|
||||
enum: [woof, meow, coo]
|
||||
x-label: "Sound"
|
||||
x-order: 1
|
||||
x-filterable: false
|
||||
required: [sound]
|
||||
|
||||
Parent:
|
||||
type: object
|
||||
x-resource: parents
|
||||
@@ -234,6 +284,13 @@ components:
|
||||
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:
|
||||
@@ -327,6 +384,19 @@ components:
|
||||
$ref: '#/components/schemas/ErrorBody'
|
||||
|
||||
paths:
|
||||
/calls:
|
||||
get:
|
||||
summary: Stream random animal sounds via SSE
|
||||
operationId: stream_calls
|
||||
x-sse: true
|
||||
responses:
|
||||
'200':
|
||||
description: SSE stream of random animal sounds
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Call'
|
||||
|
||||
/parents:
|
||||
get:
|
||||
summary: List parents (paginated)
|
||||
@@ -942,6 +1012,7 @@ paths:
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/appointments/{id}:
|
||||
get:
|
||||
operationId: get_appointment
|
||||
|
||||
@@ -11,6 +11,9 @@ in the OpenAPI specification.
|
||||
"""
|
||||
|
||||
from fastapi import Response, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from sse import subscribe, unsubscribe
|
||||
|
||||
from models import (
|
||||
ParentCreate,
|
||||
@@ -363,3 +366,18 @@ def delete_appointment(id: int, response: Response):
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
async def stream_calls():
|
||||
"""Stream random animal sounds via SSE."""
|
||||
q = await subscribe()
|
||||
|
||||
async def event_generator():
|
||||
try:
|
||||
while True:
|
||||
data = await q.get()
|
||||
yield f"data: {data}\n\n"
|
||||
finally:
|
||||
unsubscribe(q)
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
44
openapi_first/templates/vet_app/sse.py
Normal file
44
openapi_first/templates/vet_app/sse.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
SSE broadcast for the animal-sounds worker.
|
||||
|
||||
Not part of the openapi_first library API surface.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import json
|
||||
|
||||
|
||||
_sounds = ["woof", "meow", "coo"]
|
||||
_subscribers: list[asyncio.Queue] = []
|
||||
_worker_task: asyncio.Task | None = None
|
||||
|
||||
|
||||
async def _sound_worker():
|
||||
while True:
|
||||
sound = random.choice(_sounds)
|
||||
data = json.dumps({"sound": sound})
|
||||
for q in _subscribers:
|
||||
await q.put(data)
|
||||
await asyncio.sleep(random.uniform(1, 5))
|
||||
|
||||
|
||||
def start_worker():
|
||||
global _worker_task
|
||||
_worker_task = asyncio.create_task(_sound_worker())
|
||||
|
||||
|
||||
def stop_worker():
|
||||
if _worker_task is not None:
|
||||
_worker_task.cancel()
|
||||
|
||||
|
||||
async def subscribe() -> asyncio.Queue:
|
||||
q: asyncio.Queue = asyncio.Queue()
|
||||
_subscribers.append(q)
|
||||
return q
|
||||
|
||||
|
||||
def unsubscribe(q: asyncio.Queue):
|
||||
if q in _subscribers:
|
||||
_subscribers.remove(q)
|
||||
Reference in New Issue
Block a user