feat(backend): refactor mono repository

This commit is contained in:
2025-08-27 11:04:56 -04:00
parent d0dbba21fb
commit be1c729220
37 changed files with 2534 additions and 452 deletions

View File

@@ -0,0 +1,42 @@
# ------------------------------------------------------------
# Public interface for the ``myapp.models`` package.
# By reexporting the mostused symbols here callers can simply do:
#
# from myapp.models import Member, Service, ScheduleStatus
#
# This keeps import statements short and hides the internal file layout
# (whether a model lives in ``dataclasses.py`` or elsewhere).
# ------------------------------------------------------------
# Reexport all dataclass models
from .dataclasses import ( # noqa: F401 (reexported names)
AcceptedLog,
Classification,
DeclineLog,
Member,
Schedule,
ScheduledLog,
Service,
ServiceAvailability,
ServiceType,
)
# Reexport any enums that belong to the model layer
from .enums import ScheduleStatus # noqa: F401
# Optional: define what ``from myapp.models import *`` should export.
# This is useful for documentation tools and for IDE autocompletion.
__all__ = [
# Dataclasses
"AcceptedLog",
"Classification",
"DeclineLog",
"Member",
"Schedule",
"ScheduledLog",
"Service",
"ServiceAvailability",
"ServiceType",
# Enums
"ScheduleStatus",
]

View File

@@ -0,0 +1,179 @@
# ------------------------------------------------------------
# Central place for all datamodel definitions.
# ------------------------------------------------------------
from __future__ import annotations
from dataclasses import dataclass, asdict, fields
from datetime import date, datetime
from typing import Any, Dict, Tuple, Type, TypeVar, Union
# ----------------------------------------------------------------------
# Helper types what sqlite3.Row returns (either a tuplelike or a dictlike)
# ----------------------------------------------------------------------
Row = Tuple[Any, ...] | Dict[str, Any]
T = TypeVar("T", bound="BaseModel")
# ----------------------------------------------------------------------
# BaseModel common conversion helpers for every model
# ----------------------------------------------------------------------
class BaseModel:
"""
Minimal base class that knows how to:
* Build an instance from a SQLite row (or any mapping with column names).
* Export itself as a plain ``dict`` suitable for INSERT/UPDATE statements.
* Render a readable ``repr`` for debugging.
"""
@classmethod
def from_row(cls: Type[T], row: Row) -> T:
"""
Convert a ``sqlite3.Row`` (or a dictlike mapping) into a dataclass
instance. Field names are matched to column names; ``None`` values are
preserved verbatim. ``datetime`` and ``date`` columns are parsed from
ISO8601 strings when necessary.
"""
# ``row`` may already be a dict otherwise turn the Row into one.
data = dict(row) if not isinstance(row, dict) else row
converted: Dict[str, Any] = {}
for f in fields(cls):
raw = data.get(f.name)
# Preserve ``None`` exactly asis.
if raw is None:
converted[f.name] = None
continue
# ------------------------------------------------------------------
# 1⃣ datetime handling
# ------------------------------------------------------------------
if f.type is datetime:
# SQLite stores datetimes as ISO strings.
if isinstance(raw, str):
converted[f.name] = datetime.fromisoformat(raw)
else:
converted[f.name] = raw
# ------------------------------------------------------------------
# 2⃣ date handling
# ------------------------------------------------------------------
elif f.type is date:
if isinstance(raw, str):
converted[f.name] = date.fromisoformat(raw)
else:
converted[f.name] = raw
# ------------------------------------------------------------------
# 3⃣ fallback keep whatever we received
# ------------------------------------------------------------------
else:
converted[f.name] = raw
# Instantiate the concrete dataclass.
return cls(**converted) # type: ignore[arg-type]
# ------------------------------------------------------------------
# Convenience helpers
# ------------------------------------------------------------------
def to_dict(self) -> Dict[str, Any]:
"""Return a plain ``dict`` of the dataclass fields (good for INSERTs)."""
return asdict(self)
def __repr__(self) -> str:
"""Readable representation useful during debugging."""
parts = ", ".join(f"{f.name}={getattr(self, f.name)!r}" for f in fields(self))
return f"{self.__class__.__name__}({parts})"
# ----------------------------------------------------------------------
# Concrete models each one is a thin dataclass inheriting from BaseModel.
# ----------------------------------------------------------------------
# ---------- Logging tables ----------
@dataclass
class AcceptedLog(BaseModel):
LogId: int
MemberId: int
ServiceId: int
AcceptedAt: datetime
@dataclass
class DeclineLog(BaseModel):
DeclineId: int
MemberId: int
ServiceId: int
DeclinedAt: datetime
DeclineDate: date # the service day that was declined
Reason: Union[str, None] = None
@dataclass
class ScheduledLog(BaseModel):
LogId: int
MemberId: int
ServiceId: int
ScheduledAt: datetime
ExpiresAt: datetime
# ---------- Core reference data ----------
@dataclass
class Classification(BaseModel):
ClassificationId: int
ClassificationName: str
@dataclass
class ServiceType(BaseModel):
ServiceTypeId: int
TypeName: str
# ---------- Primary domain entities ----------
@dataclass
class Member(BaseModel):
MemberId: int
FirstName: str
LastName: str
Email: Union[str, None] = None
PhoneNumber: Union[str, None] = None
ClassificationId: Union[int, None] = None
Notes: Union[str, None] = None
IsActive: int = 1
LastScheduledAt: Union[datetime, None] = None
LastAcceptedAt: Union[datetime, None] = None
LastDeclinedAt: Union[datetime, None] = None
DeclineStreak: int = 0
@dataclass
class Service(BaseModel):
ServiceId: int
ServiceTypeId: int
ServiceDate: date
@dataclass
class ServiceAvailability(BaseModel):
ServiceAvailabilityId: int
MemberId: int
ServiceTypeId: int
@dataclass
class Schedule(BaseModel):
ScheduleId: int
ServiceId: int
MemberId: int
Status: str # 'pending' | 'accepted' | 'declined'
ScheduledAt: datetime
AcceptedAt: Union[datetime, None] = None
DeclinedAt: Union[datetime, None] = None
ExpiresAt: Union[datetime, None] = None
DeclineReason: Union[str, None] = None

42
backend/models/enums.py Normal file
View File

@@ -0,0 +1,42 @@
# ------------------------------------------------------------
# Centralised enumeration definitions for the datamodel layer.
# Keeping them in one module avoids circular imports and makes
# typechecking / IDE completion straightforward.
# ------------------------------------------------------------
from __future__ import annotations
from enum import StrEnum
from typing import Any, Iterable
class ScheduleStatus(StrEnum):
"""
Canonical status values for a ``Schedule`` row.
Using ``StrEnum`` means the enum members behave like regular strings
(e.g. they can be written directly to SQLite) while still giving us
the safety and autocomplete of an enum.
"""
PENDING = "pending"
ACCEPTED = "accepted"
DECLINED = "declined"
@classmethod
def from_raw(cls, value: Any) -> "ScheduleStatus":
"""
Convert an arbitrary value (often a plain string coming from the DB)
into a ``ScheduleStatus`` member.
Raises
------
ValueError
If the value does not correspond to any defined member.
"""
if isinstance(value, cls):
return value
try:
# ``cls(value)`` works because ``StrEnum`` subclasses ``str``.
return cls(value)
except ValueError as exc:
raise ValueError(f"Invalid ScheduleStatus: {value!r}") from exc