DRY across the "organize/recall" backend surface:
- serialize.py (new): serialize_label(label) — the base {id,name,color}
shape. labels.py builds on it (adds count); sync deltas will (S4).
- labeling.py (new): resolve_owned_label_ids() + reconcile_manual_labels()
— the "set a note's MANUAL (picker) labels, leave the via_tag rows alone"
logic was duplicated line-for-line between notes.set_note_labels and
sync._apply_note_manual_labels. Now one home; both adopt it (removes the
redundant `chosen`==owned recompute in notes). Behavior-preserving.
- labels.py: json_error/not_found/parse_uuid, colors.normalize_color, and
serialize_label; dropped local LABEL_COLORS + _normalize_label_color
(NOTE_COLORS is the single palette) and `import uuid` (rule 22).
- saved_filters.py: json_error/not_found/parse_uuid for its 2 uuid parses
+ error shapes.
- graph.py: no change — no error/uuid/palette-normalize duplication to fold.
Test: DB-free test_serialize_label_shape guards the base shape.
sync.py's reconciliation swap is behavior-identical; operator-verified on
deploy (no Postgres CI lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
37 lines
1.6 KiB
Python
37 lines
1.6 KiB
Python
"""Shared note↔label membership reconciliation. The "set a note's MANUAL (picker)
|
|
labels, leave the tag-sourced ones alone" logic was duplicated line-for-line between
|
|
the labels-picker API (notes.set_note_labels) and sync push (sync._apply_note_manual_labels).
|
|
Single home so both stay in lockstep. via_tag=True rows track the body #tags and are
|
|
governed by _reconcile_tags — this function never touches them."""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from .models.label import Label, NoteLabel
|
|
from .models.note import Note
|
|
|
|
|
|
async def resolve_owned_label_ids(db, label_ids, owner_id) -> set:
|
|
"""Of `label_ids` (an iterable of UUIDs), the subset actually owned by `owner_id`.
|
|
Callers parse/validate the raw ids first; this just enforces ownership."""
|
|
ids = list(label_ids)
|
|
if not ids:
|
|
return set()
|
|
return set(
|
|
(await db.scalars(select(Label.id).where(Label.owner_id == owner_id, Label.id.in_(ids)))).all()
|
|
)
|
|
|
|
|
|
async def reconcile_manual_labels(db, note: Note, owned_label_ids: set) -> None:
|
|
"""Make the note's MANUAL (via_tag=False) memberships exactly `owned_label_ids`:
|
|
drop manual rows no longer wanted, add missing ones. Tag-sourced rows survive
|
|
untouched. Caller commits."""
|
|
existing = (await db.scalars(select(NoteLabel).where(NoteLabel.note_id == note.id))).all()
|
|
attached = {r.label_id for r in existing}
|
|
for r in existing:
|
|
if not r.via_tag and r.label_id not in owned_label_ids:
|
|
await db.delete(r)
|
|
for lid in owned_label_ids:
|
|
if lid not in attached:
|
|
db.add(NoteLabel(note_id=note.id, label_id=lid, via_tag=False))
|