179 lines
5.7 KiB
Python
179 lines
5.7 KiB
Python
# ------------------------------------------------------------
|
||
# 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 |