This commit is contained in:
2026-06-16 12:26:47 +05:30
parent 514f6e5f7c
commit eb845c5bf4
7 changed files with 2047 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
"""
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 (mark a schema as a UI resource):
``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
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 vet_app
Create the service in a custom directory:
openapi-first 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.
"""

View File

@@ -0,0 +1,276 @@
"""
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 datetime, timezone
from models import (
Parent, ParentCreate,
Vet, VetCreate,
Treatment, TreatmentCreate,
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,
**payload.model_dump(exclude={"id"}),
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()
parent = _parents[parent_id]
updated = parent.model_copy(
update={
**payload.model_dump(exclude={"id", "metadata"}),
"metadata": {"createdOn": parent.metadata.get("createdOn", now), "updatedOn": now}
if parent.metadata else None,
}
)
_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,
**payload.model_dump(exclude={"id"}),
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()
vet = _vets[vet_id]
updated = vet.model_copy(
update={
**payload.model_dump(exclude={"id", "metadata"}),
"metadata": {"createdOn": vet.metadata.get("createdOn", now), "updatedOn": now}
if vet.metadata else None,
}
)
_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,
**payload.model_dump(exclude={"id"}),
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()
treatment = _treatments[treatment_id]
updated = treatment.model_copy(
update={
**payload.model_dump(exclude={"id", "metadata"}),
"metadata": {"createdOn": treatment.metadata.get("createdOn", now), "updatedOn": now}
if treatment.metadata else None,
}
)
_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()
pet = Pet(
id=_pets_next_id,
**payload.model_dump(exclude={"id"}),
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()
pet = _pets[pet_id]
updated = pet.model_copy(
update={
**payload.model_dump(exclude={"id", "metadata"}),
"metadata": {"createdOn": pet.metadata.get("createdOn", now), "updatedOn": now}
if pet.metadata else None,
}
)
_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,
**payload.model_dump(exclude={"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()
appointment = _appointments[appointment_id]
updated = appointment.model_copy(
update={
**payload.model_dump(exclude={"id", "metadata"}),
"metadata": {"createdOn": appointment.metadata.get("createdOn", now), "updatedOn": now}
if appointment.metadata else None,
}
)
_appointments[appointment_id] = updated
return updated
def delete_appointment(appointment_id: int) -> None:
del _appointments[appointment_id]

View File

@@ -0,0 +1,35 @@
"""
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 openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="Veterinary Clinic Service",
)

View File

@@ -0,0 +1,104 @@
"""
Pydantic domain models for the Veterinary Clinic example.
This module defines Pydantic models representing the five domain
entities used by the veterinary clinic service. These models are
referenced by the OpenAPI specification for request and response
schemas.
The models are declarative and framework-agnostic. They contain no
persistence logic, validation beyond type constraints, or business
behavior.
This module is not part of the ``openapi_first`` library API surface.
It exists solely to support the example application template.
"""
from datetime import date, datetime
from pydantic import BaseModel
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 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
parent_ids: list[int] = []
metadata: Metadata | None = None
class PetCreate(PetBase):
pass
class Pet(PetBase):
id: int
class AppointmentBase(BaseModel):
date: datetime
notes: str | None = None
pet_id: int
vet_id: int
treatment_id: int
metadata: Metadata | None = None
class AppointmentCreate(AppointmentBase):
pass
class Appointment(AppointmentBase):
id: int

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,365 @@
"""
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 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):
"""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]}
def create_parent(payload: ParentCreate, response: Response):
"""Create a parent.
Parameters
----------
payload : ParentCreate
Parent data excluding the ``id`` field.
Returns
-------
Parent
The newly created parent.
"""
parent = _create_parent(payload)
response.status_code = 201
return parent
def get_parent(id: int):
"""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):
"""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):
"""Delete an existing parent.
Parameters
----------
id : int
Identifier of the parent.
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]}
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 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]}
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):
"""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]}
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

View File

@@ -0,0 +1,151 @@
"""
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_params={"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_params={"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.upload_pet_photo(
path_params={"id": pet["id"]},
body={},
)
assert response.status_code == 200
def test_list_appointments():
"""List appointments returns paginated response with filter params."""
response = client.list_appointments(query_params={"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