CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s
Reading all 28 models against each other: 54 `x.isoformat() if x else None` / `x.isoformat()` expressions in 23 to_dict methods, in two guarded/unguarded wordings, become iso() from models/base.py — uniform, and a row read before flush serialises as null instead of raising. Rulebook / RulebookTopic / Rule carried byte-identical copies of TimestampMixin's two columns; InvitationToken / PasswordResetToken / NoteUsageEvent carried CreatedAtMixin's — all six now use the mixin. AppLog and RetrievalLog keep their explicit created_at, commented: their composite index orders on `created_at.desc()`, which needs the column object in the class body. Schema-neutral (same column definitions) — no migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
41 lines
1.4 KiB
Python
41 lines
1.4 KiB
Python
from datetime import date, datetime, timezone
|
|
|
|
from sqlalchemy import DateTime, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
|
|
def iso(value: datetime | date | None) -> str | None:
|
|
"""ISO-8601 for a payload, None for an unset column.
|
|
|
|
Every model's to_dict serialises timestamps through this one helper so a
|
|
row read before flush (created_at still None) and a nullable column both
|
|
come out as null instead of raising on `.isoformat()`.
|
|
"""
|
|
return value.isoformat() if value else None
|
|
|
|
|
|
class SoftDeleteMixin:
|
|
"""Recoverable-delete columns. NULL deleted_at = live row. deleted_batch_id
|
|
groups rows soft-deleted in one operation so a cascade restores as a unit."""
|
|
deleted_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True, default=None
|
|
)
|
|
deleted_batch_id: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
|
|
|
|
|
|
class TimestampMixin:
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=lambda: datetime.now(timezone.utc),
|
|
onupdate=lambda: datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
class CreatedAtMixin:
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
)
|