`[tool.ruff]` set a line length and a target version and nothing else, so which rules ran depended on the installed ruff's defaults — fine while nobody ran it, not fine for a CI job that can turn a push red because a tool changed its mind. The set is named explicitly (E, F, I, UP, SIM, B, RUF) with three ignores that are each a real decision rather than a shrug: B008, because FastAPI's whole dependency idiom is a call in a default argument; E501, because wrapping prose is a formatter's business; and RUF001, because the reports and the bot speak Russian and "ambiguous character" is the wrong complaint about a string the user is meant to read. The 73 findings that remained are fixed — mostly `datetime.timezone.utc` → `datetime.UTC`, `typing.Callable` → `collections.abc.Callable`, import order, and unused `noqa` markers — plus seven by hand: `contextlib.suppress` for two try/except/pass pairs, unpacking instead of list concatenation, one negated return. `ruff format` is deliberately *not* adopted: it would reformat 51 files, and some of the layout here is hand-set (the comment tables, the DNS wire-format structs) in ways a formatter flattens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
1.8 KiB
Python
68 lines
1.8 KiB
Python
"""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 check(self) -> str:
|
|
"""Verify credentials; return a short human description of the account."""
|