Compare commits
4 Commits
d912053368
...
richer-ope
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a7a76e330 | |||
| 083fb6923d | |||
| 69b795f9ca | |||
| 61de233745 |
@@ -22,6 +22,7 @@ from models import (
|
|||||||
Parent, ParentCreate,
|
Parent, ParentCreate,
|
||||||
Vet, VetCreate,
|
Vet, VetCreate,
|
||||||
Treatment, TreatmentCreate,
|
Treatment, TreatmentCreate,
|
||||||
|
Procedure, ProcedureNotes,
|
||||||
Pet, PetCreate,
|
Pet, PetCreate,
|
||||||
Appointment, AppointmentCreate,
|
Appointment, AppointmentCreate,
|
||||||
)
|
)
|
||||||
@@ -268,6 +269,7 @@ def create_appointment(payload: AppointmentCreate) -> Appointment:
|
|||||||
id=_appointments_next_id,
|
id=_appointments_next_id,
|
||||||
date=payload.date,
|
date=payload.date,
|
||||||
notes=payload.notes,
|
notes=payload.notes,
|
||||||
|
procedures=payload.procedures,
|
||||||
pet=_pets[payload.pet_id],
|
pet=_pets[payload.pet_id],
|
||||||
vet=_vets[payload.vet_id],
|
vet=_vets[payload.vet_id],
|
||||||
treatment=_treatments[payload.treatment_id],
|
treatment=_treatments[payload.treatment_id],
|
||||||
@@ -287,6 +289,7 @@ def update_appointment(appointment_id: int, payload: AppointmentCreate) -> Appoi
|
|||||||
id=appointment_id,
|
id=appointment_id,
|
||||||
date=payload.date,
|
date=payload.date,
|
||||||
notes=payload.notes if payload.notes is not None else current.notes,
|
notes=payload.notes if payload.notes is not None else current.notes,
|
||||||
|
procedures=payload.procedures,
|
||||||
pet=_pets.get(payload.pet_id, current.pet),
|
pet=_pets.get(payload.pet_id, current.pet),
|
||||||
vet=_vets.get(payload.vet_id, current.vet),
|
vet=_vets.get(payload.vet_id, current.vet),
|
||||||
treatment=_treatments.get(payload.treatment_id, current.treatment),
|
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[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
|
_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[1] = Appointment(id=1, date=datetime(2026, 6, 18, 9, 0, tzinfo=timezone.utc), notes="Annual checkup",
|
||||||
_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)
|
procedures=[Procedure(name="Physical Exam", cost=50.0), Procedure(name="Heart Rate", notes=ProcedureNotes(summary="Normal rhythm"))],
|
||||||
_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)
|
pet=_pets[1], vet=_vets[1], treatment=_treatments[1], 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[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
|
_appointments_next_id = 5
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,29 @@ Example:
|
|||||||
uvicorn main:app
|
uvicorn main:app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from openapi_first.app import OpenAPIFirstApp
|
from openapi_first.app import OpenAPIFirstApp
|
||||||
import routes
|
import routes
|
||||||
|
from sse import start_worker, stop_worker
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app):
|
||||||
|
start_worker()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
stop_worker()
|
||||||
|
|
||||||
|
|
||||||
app = OpenAPIFirstApp(
|
app = OpenAPIFirstApp(
|
||||||
openapi_path="openapi.yaml",
|
openapi_path="openapi.yaml",
|
||||||
routes_module=routes,
|
routes_module=routes,
|
||||||
title="Veterinary Clinic Service",
|
title="Veterinary Clinic Service",
|
||||||
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
@@ -38,6 +38,18 @@ class Vet(VetBase):
|
|||||||
id: int
|
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):
|
class TreatmentBase(BaseModel):
|
||||||
label: str
|
label: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
@@ -74,6 +86,7 @@ class Pet(PetBase):
|
|||||||
class AppointmentBase(BaseModel):
|
class AppointmentBase(BaseModel):
|
||||||
date: datetime
|
date: datetime
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
|
procedures: list[Procedure] = []
|
||||||
metadata: Metadata | None = None
|
metadata: Metadata | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,56 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
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:
|
Parent:
|
||||||
type: object
|
type: object
|
||||||
x-resource: parents
|
x-resource: parents
|
||||||
@@ -234,6 +284,13 @@ components:
|
|||||||
x-order: 2
|
x-order: 2
|
||||||
x-label: "Notes"
|
x-label: "Notes"
|
||||||
x-description: "Any additional 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:
|
pet:
|
||||||
$ref: '#/components/schemas/Pet'
|
$ref: '#/components/schemas/Pet'
|
||||||
x-fk:
|
x-fk:
|
||||||
@@ -327,6 +384,19 @@ components:
|
|||||||
$ref: '#/components/schemas/ErrorBody'
|
$ref: '#/components/schemas/ErrorBody'
|
||||||
|
|
||||||
paths:
|
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:
|
/parents:
|
||||||
get:
|
get:
|
||||||
summary: List parents (paginated)
|
summary: List parents (paginated)
|
||||||
@@ -942,6 +1012,7 @@ paths:
|
|||||||
$ref: '#/components/responses/ValidationError'
|
$ref: '#/components/responses/ValidationError'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/components/responses/InternalServerError'
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
|
||||||
/appointments/{id}:
|
/appointments/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: get_appointment
|
operationId: get_appointment
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ in the OpenAPI specification.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import Response, HTTPException, UploadFile
|
from fastapi import Response, HTTPException, UploadFile
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from sse import subscribe, unsubscribe
|
||||||
|
|
||||||
from models import (
|
from models import (
|
||||||
ParentCreate,
|
ParentCreate,
|
||||||
@@ -363,3 +366,18 @@ def delete_appointment(id: int, response: Response):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPException(status_code=404, detail="Appointment not found")
|
raise HTTPException(status_code=404, detail="Appointment not found")
|
||||||
response.status_code = 204
|
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