"""The contract every todo source implements.""" from __future__ import annotations from dataclasses import dataclass, field from datetime import date from typing import Any, Protocol class SourceError(RuntimeError): """A source could not be read. Carries a message meant for the user.""" @dataclass(slots=True) class TodoItem: """One todo, normalised across Jira / Google Tasks / Todoist / local.""" source: str external_id: str title: str description: str | None = None url: str | None = None project: str | None = None status: str | None = None priority: str | None = None due_date: date | None = None start_date: date | None = None labels: list[str] = field(default_factory=list) is_done: bool = False raw: dict[str, Any] = field(default_factory=dict) def days_overdue(self, today: date) -> int: """Days past the due date, or 0 when due today / not dated.""" if self.due_date is None: return 0 return max((today - self.due_date).days, 0) @dataclass(slots=True) class SourceResult: """What one source returned, including the failure if it had one. A source that is down costs you that source, never the whole page — the same rule jira-helper applies to its message sections. """ source: str items: list[TodoItem] = field(default_factory=list) ok: bool = True error: str | None = None @classmethod def failed(cls, source: str, error: str) -> "SourceResult": return cls(source=source, items=[], ok=False, error=error) class TodoSource(Protocol): """A configured, ready-to-call source.""" name: str def fetch(self) -> list[TodoItem]: """Every open todo this source knows about.""" def complete(self, item: TodoItem) -> bool: """Close the task in the external system. False = not supported.""" def check(self) -> str: """Verify credentials; return a short human description of the account."""