M9 section S1, commit 3 — two more shared-toolkit pieces: - common.iso(dt): the "x.isoformat() if x else None" idiom (repeated 20+ times across every serializer) as one helper. Adopted in Note.serialize() and the revision serializer; other serializers adopt it in their sections. - colors.py: NOTE_COLORS (canonical, on the model) + a single normalize_color(). notes.py now imports the palette + normalizer from here and drops its local copy. labels.py's identical LABEL_COLORS/_normalize_label_color fold into this in the Organize section (S3); sync in S4. normalize_color and NOTE_COLORS remain importable from thoughtsync.notes (used by tests + sync), so nothing downstream breaks. common has no in-app imports, so the model→common→colors chain has no cycle. Behavior-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
42 lines
1.4 KiB
Python
42 lines
1.4 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
# Small, dependency-free value coercions shared across the blueprints. Kept in one
|
|
# place so the "parse an ISO date" / "is this flag truthy" logic has a single
|
|
# definition instead of a near-identical copy per module.
|
|
|
|
|
|
def iso(dt: datetime | None) -> str | None:
|
|
"""A tz-aware datetime as an ISO-8601 string, or None. Collapses the
|
|
`x.isoformat() if x else None` idiom repeated across every serializer."""
|
|
return dt.isoformat() if dt else None
|
|
|
|
|
|
def parse_dt(raw: object) -> datetime | None:
|
|
"""Parse an ISO-8601 timestamp (accepting a trailing 'Z' for UTC).
|
|
|
|
Returns None for anything that isn't a non-empty string or doesn't parse, so
|
|
callers can treat "absent", "blank", and "malformed" uniformly (a route that
|
|
wants a 400 on malformed input checks for None itself).
|
|
"""
|
|
if not isinstance(raw, str) or not raw:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def coerce_bool(raw: object) -> bool:
|
|
"""Truthy for the common flag spellings ('true'/'1'/'yes'/'on', or a real bool).
|
|
|
|
Used for query-string booleans (?has_reminder=true) and DB-backed bool settings,
|
|
which arrive as strings.
|
|
"""
|
|
if isinstance(raw, bool):
|
|
return raw
|
|
if isinstance(raw, str):
|
|
return raw.strip().lower() in ("true", "1", "yes", "on")
|
|
return False
|