50 lines
1.8 KiB
Python
50 lines
1.8 KiB
Python
from __future__ import annotations
|
||
from dataclasses import dataclass, asdict, fields
|
||
from datetime import date, datetime
|
||
from typing import Any, Dict, Tuple, Type, TypeVar, Union
|
||
|
||
Row = Tuple[Any, ...] | Dict[str, Any] # what sqlite3.Row returns
|
||
|
||
T = TypeVar("T", bound="BaseModel")
|
||
|
||
|
||
@dataclass()
|
||
class BaseModel:
|
||
"""A tiny helper that gives every model a common interface."""
|
||
|
||
@classmethod
|
||
def from_row(cls: Type[T], row: Row) -> T:
|
||
"""
|
||
Build a model instance from a sqlite3.Row (or a dict‑like object).
|
||
Column names are matched to the dataclass field names.
|
||
"""
|
||
if isinstance(row, dict):
|
||
data = row
|
||
else: # sqlite3.Row behaves like a mapping, but we guard for safety
|
||
data = dict(row)
|
||
|
||
# Convert raw strings to proper Python types where we know the annotation
|
||
converted: Dict[str, Any] = {}
|
||
for f in fields(cls):
|
||
value = data.get(f.name)
|
||
if value is None:
|
||
converted[f.name] = None
|
||
continue
|
||
|
||
# datetime/date handling – sqlite returns str in ISO format
|
||
if f.type is datetime:
|
||
converted[f.name] = datetime.fromisoformat(value)
|
||
elif f.type is date:
|
||
converted[f.name] = date.fromisoformat(value)
|
||
else:
|
||
converted[f.name] = value
|
||
return cls(**converted) # type: ignore[arg-type]
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""Return a plain dict (useful for INSERT/UPDATE statements)."""
|
||
return asdict(self)
|
||
|
||
def __repr__(self) -> str: # a nicer representation when printing
|
||
field_vals = ", ".join(f"{f.name}={getattr(self, f.name)!r}" for f in fields(self))
|
||
return f"{self.__class__.__name__}({field_vals})"
|