14 Commits

Author SHA1 Message Date
7d075b3904 bumped up version
All checks were successful
continuous-integration/drone Build is passing
2026-07-17 12:52:51 +05:30
28c6d9e964 if no ffilterable but autocomplete, thriw error 2026-07-13 04:17:59 +05:30
628c3e97f5 if no ffilterable but autocomplete, thriw error 2026-07-13 04:14:42 +05:30
4bf358734a filterable treatment description 2026-07-13 04:01:50 +05:30
f5b3c2bb11 removed - from phone number 2026-07-13 03:24:05 +05:30
3da419fed5 feat(vet_app): add x-autocomplete annotations to schema fields
- Add x-autocomplete annotations to all qualifying string fields:
  - token: Pet.name, Parent.name, Vet.name, Vet.specialty, Treatment.label, Procedure.name
  - text: Treatment.description, Appointment.notes, Procedure.description
  - email: Parent.email, Vet.email
  - phone: Parent.phone, Vet.phone
2026-07-13 03:22:12 +05:30
a417563ab5 readme and order fixes 2026-07-13 01:36:35 +05:30
1940e33bc7 nested schema fixes 2026-07-12 23:53:42 +05:30
5da0a688a8 allOf and oneOf 2026-07-12 01:19:03 +05:30
f1a7a556fd sse sub resource 2026-07-12 00:51:23 +05:30
4a7a76e330 sse events schema for /calls 2026-06-18 01:52:55 +05:30
083fb6923d sse events sample 2026-06-18 01:21:40 +05:30
69b795f9ca moved procedures field from treatment to appointment and updated schema 2026-06-17 01:25:19 +05:30
61de233745 nested inline schemas 2026-06-17 01:01:35 +05:30
8 changed files with 397 additions and 26 deletions

View File

@@ -24,9 +24,8 @@ library API surface.
OpenAPI x- extension fields demonstrated 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-primary-key`` (REQUIRED) Primary key property name
``x-display-format`` (REQUIRED) Human-readable label template ``x-display-format`` (REQUIRED) Human-readable label template
``x-list-columns`` (REQUIRED) Columns for the datatable ``x-list-columns`` (REQUIRED) Columns for the datatable

View File

@@ -22,6 +22,9 @@ from models import (
Parent, ParentCreate, Parent, ParentCreate,
Vet, VetCreate, Vet, VetCreate,
Treatment, TreatmentCreate, Treatment, TreatmentCreate,
Procedure,
BasicNote, HeartRateNote, DentalNote,
VaccineNote, PreOpNote, SurgeryNote,
Pet, PetCreate, Pet, PetCreate,
Appointment, AppointmentCreate, Appointment, AppointmentCreate,
) )
@@ -268,6 +271,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 +291,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),
@@ -310,15 +315,15 @@ def _seed_data():
global _parents_next_id, _vets_next_id, _treatments_next_id global _parents_next_id, _vets_next_id, _treatments_next_id
global _pets_next_id, _appointments_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[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="555-0102", 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="555-0103", 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="555-0104", metadata=meta) _parents[4] = Parent(id=4, name="Dave Brown", email="dave@example.com", phone="5550104", metadata=meta)
_parents_next_id = 5 _parents_next_id = 5
_vets[1] = Vet(id=1, name="Sarah Connor", specialty="Surgery", email="sarah@clinic.com", phone="555-0201", 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="555-0202", 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="555-0203", 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 _vets_next_id = 4
_treatments[1] = Treatment(id=1, label="Annual Checkup", description="Full physical examination", metadata=meta) _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[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=[
_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) Procedure(name="Physical Exam", cost=50.0, notes=BasicNote(summary="Normal findings", details="Heart rate and temperature within normal range")),
_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) 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 _appointments_next_id = 5

View File

@@ -25,15 +25,25 @@ 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
@asynccontextmanager
async def lifespan(app):
try:
yield
finally:
pass
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(

View File

@@ -1,5 +1,6 @@
from datetime import date, datetime from datetime import date, datetime
from pydantic import BaseModel from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Metadata(BaseModel): class Metadata(BaseModel):
@@ -38,6 +39,57 @@ class Vet(VetBase):
id: int 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): class TreatmentBase(BaseModel):
label: str label: str
description: str | None = None description: str | None = None
@@ -74,6 +126,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

View File

@@ -23,9 +23,189 @@ 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"
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: Parent:
type: object type: object
x-resource: parents
x-primary-key: id x-primary-key: id
x-display-format: "{name}" x-display-format: "{name}"
x-list-columns: [name, email, phone] x-list-columns: [name, email, phone]
@@ -43,6 +223,7 @@ components:
x-description: "Parent's full name" x-description: "Parent's full name"
x-filterable: true x-filterable: true
x-sortable: true x-sortable: true
x-autocomplete: token
email: email:
type: string type: string
format: email format: email
@@ -50,12 +231,14 @@ components:
x-label: "Email" x-label: "Email"
x-description: "Email address" x-description: "Email address"
x-filterable: true x-filterable: true
x-autocomplete: email
phone: phone:
type: string type: string
x-order: 3 x-order: 3
x-label: "Phone" x-label: "Phone"
x-description: "Contact phone number" x-description: "Contact phone number"
x-filterable: true x-filterable: true
x-autocomplete: phone
metadata: metadata:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/Metadata'
x-order: 4 x-order: 4
@@ -64,7 +247,6 @@ components:
Vet: Vet:
type: object type: object
x-resource: vets
x-primary-key: id x-primary-key: id
x-display-format: "Dr. {name}" x-display-format: "Dr. {name}"
x-list-columns: [name, specialty, email, phone] x-list-columns: [name, specialty, email, phone]
@@ -82,12 +264,14 @@ components:
x-description: "Veterinarian's full name" x-description: "Veterinarian's full name"
x-filterable: true x-filterable: true
x-sortable: true x-sortable: true
x-autocomplete: token
specialty: specialty:
type: string type: string
x-order: 2 x-order: 2
x-label: "Specialty" x-label: "Specialty"
x-description: "Area of specialization" x-description: "Area of specialization"
x-filterable: true x-filterable: true
x-autocomplete: token
email: email:
type: string type: string
format: email format: email
@@ -95,6 +279,7 @@ components:
x-label: "Email" x-label: "Email"
x-description: "Email address" x-description: "Email address"
x-filterable: true x-filterable: true
x-autocomplete: email
phone: phone:
type: string type: string
x-order: 4 x-order: 4
@@ -102,13 +287,12 @@ components:
x-description: "Contact phone number" x-description: "Contact phone number"
metadata: metadata:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/Metadata'
x-order: 4 x-order: 5
x-label: "Metadata" x-label: "Metadata"
required: [id, name] required: [id, name]
Treatment: Treatment:
type: object type: object
x-resource: treatments
x-primary-key: id x-primary-key: id
x-display-format: "{label}" x-display-format: "{label}"
x-list-columns: [label, description] x-list-columns: [label, description]
@@ -126,11 +310,14 @@ components:
x-description: "Name of the treatment" x-description: "Name of the treatment"
x-filterable: true x-filterable: true
x-sortable: true x-sortable: true
x-autocomplete: token
description: description:
type: string type: string
x-order: 2 x-order: 2
x-label: "Description" x-label: "Description"
x-description: "Detailed description of the treatment" x-description: "Detailed description of the treatment"
x-filterable: true
x-autocomplete: text
metadata: metadata:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/Metadata'
x-order: 4 x-order: 4
@@ -139,7 +326,6 @@ components:
Pet: Pet:
type: object type: object
x-resource: pets
x-primary-key: id x-primary-key: id
x-display-format: "{name} #{id}" x-display-format: "{name} #{id}"
x-list-columns: [name, species, age, weight, birthDate, parents] x-list-columns: [name, species, age, weight, birthDate, parents]
@@ -157,6 +343,7 @@ components:
x-description: "Name of the pet" x-description: "Name of the pet"
x-filterable: true x-filterable: true
x-sortable: true x-sortable: true
x-autocomplete: token
species: species:
type: string type: string
enum: [dog, cat, bird] enum: [dog, cat, bird]
@@ -204,13 +391,12 @@ components:
x-filterable: true x-filterable: true
metadata: metadata:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/Metadata'
x-order: 4 x-order: 8
x-label: "Metadata" x-label: "Metadata"
required: [id, name, parents] required: [id, name, parents]
Appointment: Appointment:
type: object type: object
x-resource: appointments
x-primary-key: id x-primary-key: id
x-display-format: "Appt #{id} {date}" x-display-format: "Appt #{id} {date}"
x-list-columns: [date, pet, vet, treatment, notes] x-list-columns: [date, pet, vet, treatment, notes]
@@ -234,11 +420,18 @@ 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:
resource: pets resource: pets
x-order: 3 x-order: 4
x-label: "Pet" x-label: "Pet"
x-description: "Select a pet" x-description: "Select a pet"
x-filterable: true x-filterable: true
@@ -247,7 +440,7 @@ components:
x-fk: x-fk:
resource: vets resource: vets
prefetch: true prefetch: true
x-order: 4 x-order: 5
x-label: "Veterinarian" x-label: "Veterinarian"
x-description: "Select a veterinarian" x-description: "Select a veterinarian"
x-filterable: true x-filterable: true
@@ -256,13 +449,13 @@ components:
x-fk: x-fk:
resource: treatments resource: treatments
prefetch: true prefetch: true
x-order: 5 x-order: 6
x-label: "Treatment" x-label: "Treatment"
x-description: "Select a treatment" x-description: "Select a treatment"
x-filterable: true x-filterable: true
metadata: metadata:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/Metadata'
x-order: 4 x-order: 7
x-label: "Metadata" x-label: "Metadata"
required: [id, date, pet, vet, treatment] required: [id, date, pet, vet, treatment]
@@ -875,6 +1068,23 @@ paths:
$ref: '#/components/responses/ValidationError' $ref: '#/components/responses/ValidationError'
'500': '500':
$ref: '#/components/responses/InternalServerError' $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: /appointments:
get: get:
@@ -942,6 +1152,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

View File

@@ -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,
@@ -40,6 +43,7 @@ from data import (
create_pet as _create_pet, create_pet as _create_pet,
update_pet as _update_pet, update_pet as _update_pet,
delete_pet as _delete_pet, delete_pet as _delete_pet,
get_pet as _get_pet,
list_appointments as _list_appointments, list_appointments as _list_appointments,
get_appointment as _get_appointment, get_appointment as _get_appointment,
create_appointment as _create_appointment, create_appointment as _create_appointment,
@@ -363,3 +367,23 @@ 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_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")

View 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)

View File

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "openapi-first" name = "openapi-first"
version = "0.0.4" version = "0.0.5"
description = "Strict OpenAPI-first application bootstrap for FastAPI." description = "Strict OpenAPI-first application bootstrap for FastAPI."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"