Compare commits
14 Commits
d912053368
...
0.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d075b3904 | |||
| 28c6d9e964 | |||
| 628c3e97f5 | |||
| 4bf358734a | |||
| f5b3c2bb11 | |||
| 3da419fed5 | |||
| a417563ab5 | |||
| 1940e33bc7 | |||
| 5da0a688a8 | |||
| f1a7a556fd | |||
| 4a7a76e330 | |||
| 083fb6923d | |||
| 69b795f9ca | |||
| 61de233745 |
@@ -24,9 +24,8 @@ library API surface.
|
||||
OpenAPI x- extension fields demonstrated
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Schema-level extensions (mark a schema as a UI resource):
|
||||
Schema-level extensions (display metadata for resource endpoints):
|
||||
|
||||
``x-resource`` (REQUIRED) Maps schema to URL path segment
|
||||
``x-primary-key`` (REQUIRED) Primary key property name
|
||||
``x-display-format`` (REQUIRED) Human-readable label template
|
||||
``x-list-columns`` (REQUIRED) Columns for the datatable
|
||||
|
||||
@@ -22,6 +22,9 @@ from models import (
|
||||
Parent, ParentCreate,
|
||||
Vet, VetCreate,
|
||||
Treatment, TreatmentCreate,
|
||||
Procedure,
|
||||
BasicNote, HeartRateNote, DentalNote,
|
||||
VaccineNote, PreOpNote, SurgeryNote,
|
||||
Pet, PetCreate,
|
||||
Appointment, AppointmentCreate,
|
||||
)
|
||||
@@ -268,6 +271,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 +291,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),
|
||||
@@ -310,15 +315,15 @@ def _seed_data():
|
||||
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="555-0101", metadata=meta)
|
||||
_parents[2] = Parent(id=2, name="Bob Smith", email="bob@example.com", phone="555-0102", metadata=meta)
|
||||
_parents[3] = Parent(id=3, name="Carol Williams", email="carol@example.com", phone="555-0103", metadata=meta)
|
||||
_parents[4] = Parent(id=4, name="Dave Brown", email="dave@example.com", phone="555-0104", metadata=meta)
|
||||
_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="555-0201", metadata=meta)
|
||||
_vets[2] = Vet(id=2, name="James Wilson", specialty="Dentistry", email="james@clinic.com", phone="555-0202", metadata=meta)
|
||||
_vets[3] = Vet(id=3, name="Emily Davis", specialty="General Practice", email="emily@clinic.com", phone="555-0203", metadata=meta)
|
||||
_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)
|
||||
@@ -335,10 +340,31 @@ 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, 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
|
||||
|
||||
|
||||
|
||||
@@ -25,15 +25,25 @@ 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(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
from pydantic import BaseModel
|
||||
from typing import Annotated, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Metadata(BaseModel):
|
||||
@@ -38,6 +39,57 @@ 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
|
||||
@@ -74,6 +126,7 @@ class Pet(PetBase):
|
||||
class AppointmentBase(BaseModel):
|
||||
date: datetime
|
||||
notes: str | None = None
|
||||
procedures: list[Procedure] = []
|
||||
metadata: Metadata | None = None
|
||||
|
||||
|
||||
|
||||
@@ -23,9 +23,189 @@ 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"
|
||||
|
||||
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-resource: parents
|
||||
x-primary-key: id
|
||||
x-display-format: "{name}"
|
||||
x-list-columns: [name, email, phone]
|
||||
@@ -43,6 +223,7 @@ components:
|
||||
x-description: "Parent's full name"
|
||||
x-filterable: true
|
||||
x-sortable: true
|
||||
x-autocomplete: token
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
@@ -50,12 +231,14 @@ components:
|
||||
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
|
||||
@@ -64,7 +247,6 @@ components:
|
||||
|
||||
Vet:
|
||||
type: object
|
||||
x-resource: vets
|
||||
x-primary-key: id
|
||||
x-display-format: "Dr. {name}"
|
||||
x-list-columns: [name, specialty, email, phone]
|
||||
@@ -82,12 +264,14 @@ components:
|
||||
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
|
||||
@@ -95,6 +279,7 @@ components:
|
||||
x-label: "Email"
|
||||
x-description: "Email address"
|
||||
x-filterable: true
|
||||
x-autocomplete: email
|
||||
phone:
|
||||
type: string
|
||||
x-order: 4
|
||||
@@ -102,13 +287,12 @@ components:
|
||||
x-description: "Contact phone number"
|
||||
metadata:
|
||||
$ref: '#/components/schemas/Metadata'
|
||||
x-order: 4
|
||||
x-order: 5
|
||||
x-label: "Metadata"
|
||||
required: [id, name]
|
||||
|
||||
Treatment:
|
||||
type: object
|
||||
x-resource: treatments
|
||||
x-primary-key: id
|
||||
x-display-format: "{label}"
|
||||
x-list-columns: [label, description]
|
||||
@@ -126,11 +310,14 @@ components:
|
||||
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
|
||||
@@ -139,7 +326,6 @@ components:
|
||||
|
||||
Pet:
|
||||
type: object
|
||||
x-resource: pets
|
||||
x-primary-key: id
|
||||
x-display-format: "{name} – #{id}"
|
||||
x-list-columns: [name, species, age, weight, birthDate, parents]
|
||||
@@ -157,6 +343,7 @@ components:
|
||||
x-description: "Name of the pet"
|
||||
x-filterable: true
|
||||
x-sortable: true
|
||||
x-autocomplete: token
|
||||
species:
|
||||
type: string
|
||||
enum: [dog, cat, bird]
|
||||
@@ -204,13 +391,12 @@ components:
|
||||
x-filterable: true
|
||||
metadata:
|
||||
$ref: '#/components/schemas/Metadata'
|
||||
x-order: 4
|
||||
x-order: 8
|
||||
x-label: "Metadata"
|
||||
required: [id, name, parents]
|
||||
|
||||
Appointment:
|
||||
type: object
|
||||
x-resource: appointments
|
||||
x-primary-key: id
|
||||
x-display-format: "Appt #{id} – {date}"
|
||||
x-list-columns: [date, pet, vet, treatment, notes]
|
||||
@@ -234,11 +420,18 @@ 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:
|
||||
resource: pets
|
||||
x-order: 3
|
||||
x-order: 4
|
||||
x-label: "Pet"
|
||||
x-description: "Select a pet"
|
||||
x-filterable: true
|
||||
@@ -247,7 +440,7 @@ components:
|
||||
x-fk:
|
||||
resource: vets
|
||||
prefetch: true
|
||||
x-order: 4
|
||||
x-order: 5
|
||||
x-label: "Veterinarian"
|
||||
x-description: "Select a veterinarian"
|
||||
x-filterable: true
|
||||
@@ -256,13 +449,13 @@ components:
|
||||
x-fk:
|
||||
resource: treatments
|
||||
prefetch: true
|
||||
x-order: 5
|
||||
x-order: 6
|
||||
x-label: "Treatment"
|
||||
x-description: "Select a treatment"
|
||||
x-filterable: true
|
||||
metadata:
|
||||
$ref: '#/components/schemas/Metadata'
|
||||
x-order: 4
|
||||
x-order: 7
|
||||
x-label: "Metadata"
|
||||
required: [id, date, pet, vet, treatment]
|
||||
|
||||
@@ -875,6 +1068,23 @@ paths:
|
||||
$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:
|
||||
@@ -942,6 +1152,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,
|
||||
@@ -40,6 +43,7 @@ from data import (
|
||||
create_pet as _create_pet,
|
||||
update_pet as _update_pet,
|
||||
delete_pet as _delete_pet,
|
||||
get_pet as _get_pet,
|
||||
list_appointments as _list_appointments,
|
||||
get_appointment as _get_appointment,
|
||||
create_appointment as _create_appointment,
|
||||
@@ -363,3 +367,23 @@ 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_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")
|
||||
|
||||
48
openapi_first/templates/vet_app/sse.py
Normal file
48
openapi_first/templates/vet_app/sse.py
Normal file
@@ -0,0 +1,48 @@
|
||||
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)
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "openapi-first"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
description = "Strict OpenAPI-first application bootstrap for FastAPI."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user