from typing import List, Sequence from .todo_item import Todo, TodoDraft, validate_draft from .todo_item_validate_draft import trim_space from .todo_item_validate_todo import is_utc_timestamp from .todo_normalise_tags import normalise_tags def require_utc_now(now: str) -> None: """Refuse a clock reading that would not compare as a string with the stored ones.""" if not is_utc_timestamp(now): raise ValueError("now must be a UTC timestamp like 2026-09-28T09:30:00Z") def checked_draft(draft: TodoDraft) -> TodoDraft: """The draft as it will be stored (trimmed, blank notes None, tags normalised), checked. The first field in field order that fails validate_draft is the error. """ notes = "" if draft.notes is None else trim_space(draft.notes) clean = TodoDraft( title=trim_space(draft.title), notes=None if notes == "" else notes, priority=draft.priority, due=draft.due, tags=list(normalise_tags(draft.tags)), recurrence=draft.recurrence, ) verdict = validate_draft(clean) for field, message in verdict.errors.items(): raise ValueError(f"invalid todo: {field}: {message}") return clean def add_todo(todos: Sequence[Todo], draft: TodoDraft, id: str, now: str) -> List[Todo]: """Add a todo from the add form: open, created now, last in manual order, appended. Caller mistakes (id, clock) are reported before form mistakes (the draft). """ if trim_space(id) == "": raise ValueError("a todo needs an id") if any(t.id == id for t in todos): raise ValueError(f'a todo with id "{id}" already exists') require_utc_now(now) clean = checked_draft(draft) order = 0 for t in todos: if t.order + 1 > order: order = t.order + 1 todo = Todo( id=id, title=clean.title, notes=clean.notes, done=False, priority=clean.priority, due=clean.due, tags=clean.tags, recurrence=clean.recurrence, created_at=now, completed_at=None, order=order, ) return list(todos) + [todo]