Phase 6: replace app template with the vet sample domain + jobs slice, pipelines and worker

- templates/app now carries the vet server (parents/pets/treatments/vets/appointments)
- added domain/jobs slice with /jobs OpenAPI paths and Job schema (x- extensions)
- added pipelines/shared scaffold (loader, ports, registry) and the
  appointment_reminder family; worker.py dispatches jobs by kind
- wired init.py, config.py worker knobs, .env.example, docker-compose worker
- unit tests for jobs routes, the pipeline family and worker dispatch (12 passing)
This commit is contained in:
2026-10-20 14:49:10 +05:30
parent 6f6f0d6e6d
commit 56aa1322fb
83 changed files with 2946 additions and 610 deletions

View File

@@ -0,0 +1,3 @@
"""# Summary
Appointments domain slice."""

View File

@@ -0,0 +1,68 @@
"""# Summary
Appointments domain — scheduled visits linking a pet, a vet and a treatment.
"""
from pydantic import BaseModel
from domain.parents.models import Metadata
class ProcedureNote(BaseModel):
"""A single procedure note; ``noteType`` selects the shape.
Attributes:
noteType (str): Discriminator (basic, heart_rate, dental, vaccine,
preop, surgery).
summary (str | None): Short summary. Defaults to None.
details (str | None): Longer details. Defaults to None.
"""
noteType: str
summary: str | None = None
details: str | None = None
model_config = {"extra": "ignore"}
class Procedure(BaseModel):
"""A procedure performed during an appointment.
Attributes:
name (str): Procedure name.
description (str | None): Procedure description. Defaults to None.
cost (float | None): Cost of the procedure. Defaults to None.
notes (ProcedureNote | None): Typed procedure note. Defaults to None.
"""
name: str
description: str | None = None
cost: float | None = None
notes: ProcedureNote | None = None
model_config = {"extra": "ignore"}
class Appointment(BaseModel):
"""A scheduled appointment.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
date (str): ISO date-time of the appointment.
notes (str | None): Additional notes. Defaults to None.
procedures (list[Procedure]): Procedures performed. Defaults to empty.
pet (dict | int | None): Embedded pet object or pet id.
vet (dict | int | None): Embedded vet object or vet id.
treatment (dict | int | None): Embedded treatment or treatment id.
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
date: str
notes: str | None = None
procedures: list[Procedure] = []
pet: dict | int | None = None
vet: dict | int | None = None
treatment: dict | int | None = None
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Appointments domain ports."""

View File

@@ -0,0 +1,25 @@
"""# Summary
Appointments persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from domain.appointments.models import Appointment
from infra.db.repos import create_repo
class AppointmentDocument(Appointment, BaseDocument):
"""Persisted appointment document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_appointment_repo():
"""Create the appointment repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``appointments`` collection/store.
"""
return create_repo("appointments", AppointmentDocument)

View File

@@ -0,0 +1,34 @@
"""# Summary
CRUD service wiring for the appointment aggregate via the hexa container.
"""
from hexa import build
from domain.appointments.models import Appointment
from domain.appointments.ports.db import AppointmentDocument
from infra.adapters.service import CrudsServiceAdaptor
class AppointmentCrudsService(CrudsServiceAdaptor[Appointment, AppointmentDocument]):
"""Appointment CRUD service resolving :class:`AppointmentDocument` from a repo.
Notes:
Instances are created by :func:`get_appointment_service`. The ``model``
and ``schema`` class attributes bind the domain model and its document.
"""
model = Appointment
schema = AppointmentDocument
def get_appointment_service(repo) -> AppointmentCrudsService:
"""Build an appointment CRUD service wired to ``repo``.
Args:
repo (Any): Integer-id appointment repository from
:func:`create_appointment_repo`.
Returns:
AppointmentCrudsService: Ready-to-use appointment CRUD service.
"""
return build(AppointmentCrudsService, instances={"repo": repo})

View File

@@ -1,21 +0,0 @@
"""# Summary
Item domain model — the neutral resource used throughout the template.
"""
from pydantic import BaseModel, Field
class Item(BaseModel):
"""A single countable resource.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Item name.
quantity (int): Item quantity. Defaults to 1.
"""
id: int | None = None
name: str
quantity: int = Field(default=1)
model_config = {"extra": "ignore"}

View File

@@ -1,18 +0,0 @@
"""# Summary
Optional converters for the item slice — pure functions mapping between domain
types. Add a converter here when a service must reshape data; keep it pure.
"""
from domain.item.models import Item
def to_item(data: dict) -> Item:
"""Build an :class:`Item` from a raw mapping.
Args:
data (dict): Field mapping matching the item model.
Returns:
Item: The constructed item model.
"""
return Item(**data)

View File

@@ -1,25 +0,0 @@
"""# Summary
Items persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from domain.item.models import Item
from infra.db.repos import create_repo
class ItemDocument(Item, BaseDocument):
"""Persisted item document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_item_repo():
"""Create the item repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``items`` collection/store.
"""
return create_repo("items", ItemDocument)

View File

@@ -1,33 +0,0 @@
"""# Summary
CRUD service wiring for the item aggregate via the hexa container.
"""
from hexa import build
from domain.item.models import Item
from domain.item.ports.db import ItemDocument
from infra.adapters.service import CrudsServiceAdaptor
class ItemCrudsService(CrudsServiceAdaptor[Item, ItemDocument]):
"""Item CRUD service resolving :class:`ItemDocument` from a repo.
Notes:
Instances are created by :func:`get_item_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Item
schema = ItemDocument
def get_item_service(repo) -> ItemCrudsService:
"""Build an item CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed item repository from :func:`create_item_repo`.
Returns:
ItemCrudsService: Ready-to-use item CRUD service.
"""
return build(ItemCrudsService, instances={"repo": repo})

View File

@@ -1,29 +0,0 @@
"""# Summary
Job domain model — a pipeline work item consumed by the background worker.
"""
from typing import Literal
from pydantic import BaseModel, Field
class Job(BaseModel):
"""A unit of background work.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Job name.
status (str): Lifecycle status. Defaults to "pending".
retry_count (int): Times the job failed. Defaults to 0.
error_message (str | None): Last failure message. Defaults to None.
payload (dict | None): Arbitrary job payload. Defaults to None.
"""
id: int | None = None
name: str
status: Literal["pending", "processing", "done", "failed"] = "pending"
retry_count: int = Field(default=0)
error_message: str | None = None
payload: dict | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,37 @@
"""# Summary
Jobs domain — the worker's queue. Each job is a unit of pipeline work the
background worker claims and executes.
"""
from typing import Literal
from pydantic import BaseModel
from domain.parents.models import Metadata
class Job(BaseModel):
"""A unit of background work.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Human label for the job.
kind (str): Dispatch key — selects the pipeline (e.g.
``appointment_reminder``).
status (str): Lifecycle status. Defaults to ``pending``.
payload (dict | None): Job-specific input. Defaults to None.
retry_count (int): Failed attempts so far. Defaults to 0.
error_message (str | None): Last failure detail. Defaults to None.
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
name: str
kind: str
status: Literal["pending", "processing", "done", "failed"] = "pending"
payload: dict | None = None
retry_count: int = 0
error_message: str | None = None
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -5,7 +5,8 @@ Jobs persistence ports — integer-id document and repository factory.
from mongo_ops import BaseDocument
from pydantic import Field
from domain.job.models import Job
from config import settings
from domain.jobs.models import Job
from infra.db.repos import create_repo
@@ -22,4 +23,5 @@ def create_job_repo():
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``jobs`` collection/store.
"""
_ = settings # backend chosen centrally by create_repo
return create_repo("jobs", JobDocument)

View File

@@ -4,8 +4,8 @@ CRUD service wiring for the job aggregate via the hexa container.
"""
from hexa import build
from domain.job.models import Job
from domain.job.ports.db import JobDocument
from domain.jobs.models import Job
from domain.jobs.ports.db import JobDocument
from infra.adapters.service import CrudsServiceAdaptor
@@ -25,9 +25,9 @@ def get_job_service(repo) -> JobCrudsService:
"""Build a job CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed job repository from :func:`create_job_repo`.
repo (Any): Integer-id job repository from :func:`create_job_repo`.
Returns:
JobCrudsService: Ready-to-use job CRUD service.
"""
return build(JobCrudsService, instances={"repo": repo})
return build(JobCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,3 @@
"""# Summary
Parents domain slice."""

View File

@@ -0,0 +1,39 @@
"""# Summary
Parents domain — pet owners referenced by :class:`domain.pets.models.Pet`.
"""
from pydantic import BaseModel
class Metadata(BaseModel):
"""Optional presentation metadata carried alongside a resource.
Attributes:
createdOn (str | None): ISO creation timestamp. Defaults to None.
updatedOn (str | None): ISO update timestamp. Defaults to None.
"""
createdOn: str | None = None
updatedOn: str | None = None
model_config = {"extra": "ignore"}
class Parent(BaseModel):
"""A pet owner.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Parent's full name.
email (str): Email address.
phone (str | None): Contact phone number. Defaults to None.
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
name: str
email: str
phone: str | None = None
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Parents domain ports."""

View File

@@ -0,0 +1,27 @@
"""# Summary
Parents persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from config import settings
from domain.parents.models import Parent
from infra.db.repos import create_repo
class ParentDocument(Parent, BaseDocument):
"""Persisted parent document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_parent_repo():
"""Create the parent repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``parents`` collection/store.
"""
_ = settings # backend chosen centrally by create_repo
return create_repo("parents", ParentDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the parent aggregate via the hexa container.
"""
from hexa import build
from domain.parents.models import Parent
from domain.parents.ports.db import ParentDocument
from infra.adapters.service import CrudsServiceAdaptor
class ParentCrudsService(CrudsServiceAdaptor[Parent, ParentDocument]):
"""Parent CRUD service resolving :class:`ParentDocument` from a repo.
Notes:
Instances are created by :func:`get_parent_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Parent
schema = ParentDocument
def get_parent_service(repo) -> ParentCrudsService:
"""Build a parent CRUD service wired to ``repo``.
Args:
repo (Any): Integer-id parent repository from :func:`create_parent_repo`.
Returns:
ParentCrudsService: Ready-to-use parent CRUD service.
"""
return build(ParentCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,3 @@
"""# Summary
Pets domain slice."""

View File

@@ -0,0 +1,37 @@
"""# Summary
Pets domain — animals owned by parents and treated in appointments.
"""
from typing import Literal
from pydantic import BaseModel
from domain.parents.models import Metadata
class Pet(BaseModel):
"""A pet patient.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Name of the pet.
species (str): Type of animal (dog, cat, bird).
age (int | None): Age in years. Defaults to None.
weight (float | None): Weight in kilograms. Defaults to None.
birthDate (str | None): ISO date of birth. Defaults to None.
photo (str | None): Stored photo path after upload. Defaults to None.
parents (list[dict]): Embedded parent objects (or bare ids on input).
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
name: str
species: Literal["dog", "cat", "bird"]
age: int | None = None
weight: float | None = None
birthDate: str | None = None
photo: str | None = None
parents: list[dict] = []
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Pets domain ports."""

View File

@@ -0,0 +1,25 @@
"""# Summary
Pets persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from domain.pets.models import Pet
from infra.db.repos import create_repo
class PetDocument(Pet, BaseDocument):
"""Persisted pet document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_pet_repo():
"""Create the pet repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``pets`` collection/store.
"""
return create_repo("pets", PetDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the pet aggregate via the hexa container.
"""
from hexa import build
from domain.pets.models import Pet
from domain.pets.ports.db import PetDocument
from infra.adapters.service import CrudsServiceAdaptor
class PetCrudsService(CrudsServiceAdaptor[Pet, PetDocument]):
"""Pet CRUD service resolving :class:`PetDocument` from a repo.
Notes:
Instances are created by :func:`get_pet_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Pet
schema = PetDocument
def get_pet_service(repo) -> PetCrudsService:
"""Build a pet CRUD service wired to ``repo``.
Args:
repo (Any): Integer-id pet repository from :func:`create_pet_repo`.
Returns:
PetCrudsService: Ready-to-use pet CRUD service.
"""
return build(PetCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,3 @@
"""# Summary
Treatments domain slice."""

View File

@@ -0,0 +1,25 @@
"""# Summary
Treatments domain — the catalogue of treatments offered by the clinic.
"""
from pydantic import BaseModel
from domain.parents.models import Metadata
class Treatment(BaseModel):
"""A catalogue treatment.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
label (str): Name of the treatment.
description (str | None): Detailed description. Defaults to None.
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
label: str
description: str | None = None
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Treatments domain ports."""

View File

@@ -0,0 +1,25 @@
"""# Summary
Treatments persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from domain.treatments.models import Treatment
from infra.db.repos import create_repo
class TreatmentDocument(Treatment, BaseDocument):
"""Persisted treatment document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_treatment_repo():
"""Create the treatment repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``treatments`` collection/store.
"""
return create_repo("treatments", TreatmentDocument)

View File

@@ -0,0 +1,34 @@
"""# Summary
CRUD service wiring for the treatment aggregate via the hexa container.
"""
from hexa import build
from domain.treatments.models import Treatment
from domain.treatments.ports.db import TreatmentDocument
from infra.adapters.service import CrudsServiceAdaptor
class TreatmentCrudsService(CrudsServiceAdaptor[Treatment, TreatmentDocument]):
"""Treatment CRUD service resolving :class:`TreatmentDocument` from a repo.
Notes:
Instances are created by :func:`get_treatment_service`. The ``model``
and ``schema`` class attributes bind the domain model and its document.
"""
model = Treatment
schema = TreatmentDocument
def get_treatment_service(repo) -> TreatmentCrudsService:
"""Build a treatment CRUD service wired to ``repo``.
Args:
repo (Any): Integer-id treatment repository from
:func:`create_treatment_repo`.
Returns:
TreatmentCrudsService: Ready-to-use treatment CRUD service.
"""
return build(TreatmentCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,3 @@
"""# Summary
Vets domain slice."""

View File

@@ -0,0 +1,29 @@
"""# Summary
Vets domain — veterinarians referenced by appointments.
"""
from pydantic import BaseModel
from domain.parents.models import Metadata
class Vet(BaseModel):
"""A veterinarian.
Attributes:
id (int | None): Server-assigned identifier. Defaults to None.
name (str): Veterinarian's full name.
specialty (str | None): Area of specialization. Defaults to None.
email (str | None): Email address. Defaults to None.
phone (str | None): Contact phone number. Defaults to None.
metadata (Metadata | None): Presentation metadata. Defaults to None.
"""
id: int | None = None
name: str
specialty: str | None = None
email: str | None = None
phone: str | None = None
metadata: Metadata | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,3 @@
"""# Summary
Vets domain ports."""

View File

@@ -0,0 +1,25 @@
"""# Summary
Vets persistence ports — integer-id document and repository factory.
"""
from mongo_ops import BaseDocument
from pydantic import Field
from domain.vets.models import Vet
from infra.db.repos import create_repo
class VetDocument(Vet, BaseDocument):
"""Persisted vet document. The integer ``id`` shadows the ObjectId."""
id: int | None = Field(default=None, validation_alias="_id")
def create_vet_repo():
"""Create the vet repository selected by ``settings.repo_backend``.
Returns:
IntIdMemoryRepository | IntIdMongoRepository: Repository wired to the
``vets`` collection/store.
"""
return create_repo("vets", VetDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the vet aggregate via the hexa container.
"""
from hexa import build
from domain.vets.models import Vet
from domain.vets.ports.db import VetDocument
from infra.adapters.service import CrudsServiceAdaptor
class VetCrudsService(CrudsServiceAdaptor[Vet, VetDocument]):
"""Vet CRUD service resolving :class:`VetDocument` from a repo.
Notes:
Instances are created by :func:`get_vet_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Vet
schema = VetDocument
def get_vet_service(repo) -> VetCrudsService:
"""Build a vet CRUD service wired to ``repo``.
Args:
repo (Any): Integer-id vet repository from :func:`create_vet_repo`.
Returns:
VetCrudsService: Ready-to-use vet CRUD service.
"""
return build(VetCrudsService, instances={"repo": repo})