feat(backend): refactor mono repository
This commit is contained in:
42
backend/models/__init__.py
Normal file
42
backend/models/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# ------------------------------------------------------------
|
||||
# Public interface for the ``myapp.models`` package.
|
||||
# By re‑exporting the most‑used 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).
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Re‑export all dataclass models
|
||||
from .dataclasses import ( # noqa: F401 (re‑exported names)
|
||||
AcceptedLog,
|
||||
Classification,
|
||||
DeclineLog,
|
||||
Member,
|
||||
Schedule,
|
||||
ScheduledLog,
|
||||
Service,
|
||||
ServiceAvailability,
|
||||
ServiceType,
|
||||
)
|
||||
|
||||
# Re‑export 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 auto‑completion.
|
||||
__all__ = [
|
||||
# Dataclasses
|
||||
"AcceptedLog",
|
||||
"Classification",
|
||||
"DeclineLog",
|
||||
"Member",
|
||||
"Schedule",
|
||||
"ScheduledLog",
|
||||
"Service",
|
||||
"ServiceAvailability",
|
||||
"ServiceType",
|
||||
# Enums
|
||||
"ScheduleStatus",
|
||||
]
|
||||
179
backend/models/dataclasses.py
Normal file
179
backend/models/dataclasses.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# ------------------------------------------------------------
|
||||
# Central place for all data‑model 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 tuple‑like or a dict‑like)
|
||||
# ----------------------------------------------------------------------
|
||||
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 dict‑like mapping) into a dataclass
|
||||
instance. Field names are matched to column names; ``None`` values are
|
||||
preserved verbatim. ``datetime`` and ``date`` columns are parsed from
|
||||
ISO‑8601 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 as‑is.
|
||||
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
42
backend/models/enums.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# ------------------------------------------------------------
|
||||
# Centralised enumeration definitions for the data‑model layer.
|
||||
# Keeping them in one module avoids circular imports and makes
|
||||
# type‑checking / 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
|
||||
Reference in New Issue
Block a user