diff --git a/src/thoughtsync/colors.py b/src/thoughtsync/colors.py new file mode 100644 index 0000000..3158981 --- /dev/null +++ b/src/thoughtsync/colors.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from .models.note import NOTE_COLORS + +# Notes and labels share one colour palette (their sets were identical). NOTE_COLORS +# is the canonical vocabulary (defined on the model); this module is the single home +# for the "clamp to the palette" normalizer so notes.py, labels.py and sync.py stop +# each carrying their own copy. + +__all__ = ["NOTE_COLORS", "normalize_color"] + + +def normalize_color(color: object) -> str: + """Return `color` if it's a known palette key, else the default. One definition + for both notes and labels.""" + return color if color in NOTE_COLORS else "default" diff --git a/src/thoughtsync/common.py b/src/thoughtsync/common.py index 4c0f074..2a4b6f5 100644 --- a/src/thoughtsync/common.py +++ b/src/thoughtsync/common.py @@ -7,6 +7,12 @@ from datetime import datetime # 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). diff --git a/src/thoughtsync/models/note.py b/src/thoughtsync/models/note.py index fb1bc00..bc5576f 100644 --- a/src/thoughtsync/models/note.py +++ b/src/thoughtsync/models/note.py @@ -8,6 +8,7 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column from . import Base +from ..common import iso # The Keep-style palette. Stored as a key string, so the actual tints live in the # frontend and can change without a schema migration. @@ -82,8 +83,8 @@ class Note(Base): "pinned": self.pinned, "archived": self.archived, "trashed": self.deleted_at is not None, - "remind_at": self.remind_at.isoformat() if self.remind_at else None, + "remind_at": iso(self.remind_at), "recurrence": self.recurrence, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "created_at": iso(self.created_at), + "updated_at": iso(self.updated_at), } diff --git a/src/thoughtsync/notes.py b/src/thoughtsync/notes.py index 0e73a40..9c62a0f 100644 --- a/src/thoughtsync/notes.py +++ b/src/thoughtsync/notes.py @@ -16,14 +16,15 @@ from sqlalchemy import case, delete, func, literal_column, select from .acl import visible_to_user from .auth import login_required -from .common import coerce_bool, parse_dt +from .colors import NOTE_COLORS, normalize_color +from .common import coerce_bool, iso, parse_dt from .config import Config from .db import session_scope from .responses import json_error, not_found, parse_uuid from .settings import get_setting from .unfurl import UnfurlError, unfurl from .models.label import Label, NoteLabel -from .models.note import NOTE_COLORS, Note +from .models.note import Note from .models.note_attachment import NoteAttachment from .models.note_item import NoteItem from .models.note_link import NoteLink @@ -101,10 +102,6 @@ def parse_list_items(raw: object) -> list[str]: return [s.strip() for s in raw if isinstance(s, str) and s.strip()] -def normalize_color(color: object) -> str: - return color if color in NOTE_COLORS else "default" - - def apply_filter(stmt, filter_name: str): """Narrow a notes query to one board view.""" if filter_name == "archived": @@ -1143,7 +1140,7 @@ def _serialize_revision(rev: NoteRevision) -> dict: "id": str(rev.id), "title": rev.title, "body": rev.body, - "created_at": rev.created_at.isoformat() if rev.created_at else None, + "created_at": iso(rev.created_at), }