Files
nimbusflow/backend/models/dataclasses.py

179 lines
5.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ------------------------------------------------------------
# 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