# ------------------------------------------------------------ # 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