Files
kadence/backend/app/sources/registry.py
T
timka123andClaude Opus 5 ec7eedbc66 style: say which lint rules this repo keeps, then keep them
`[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>
2026-10-22 17:28:57 +03:00

112 lines
3.8 KiB
Python

"""Builds the configured sources and pulls them all, independently.
Independence is the whole point: an expired laundry cookie or a Jira outage
costs you that one source and shows up as a warning beside it, never as an
empty task list.
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from app.settings_schema import AppSettings
from app.sources.base import SourceError, SourceResult, TodoItem
from app.sources.google_tasks import GoogleTasksSource
from app.sources.jira import JiraSource
from app.sources.laundry import LaundrySource
from app.sources.todoist import TodoistSource
log = logging.getLogger(__name__)
SOURCE_LABELS = {
"jira": "Jira",
"todoist": "Todoist",
"google_tasks": "Google Tasks",
"laundry": "Laundry",
"local": "My list",
}
def build_sources(settings: AppSettings) -> dict[str, Any]:
"""Instantiate every enabled source. A misconfigured one is skipped, loudly."""
timeout = settings.sources.request_timeout_seconds
builders: list[tuple[str, Callable[[], Any]]] = [
("jira", lambda: JiraSource(settings.sources.jira, timeout=timeout)),
("todoist", lambda: TodoistSource(settings.sources.todoist, timeout=timeout)),
(
"google_tasks",
lambda: GoogleTasksSource(settings.sources.google_tasks, timeout=timeout),
),
("laundry", lambda: LaundrySource(settings.sources.laundry, timeout=timeout)),
]
enabled = {
"jira": settings.sources.jira.enabled,
"todoist": settings.sources.todoist.enabled,
"google_tasks": settings.sources.google_tasks.enabled,
"laundry": settings.sources.laundry.enabled,
}
sources: dict[str, Any] = {}
for name, build in builders:
if not enabled[name]:
continue
try:
sources[name] = build()
except SourceError as exc:
log.warning("source %s is enabled but unusable: %s", name, exc)
return sources
def _close(source: Any) -> None:
closer = getattr(source, "close", None)
if callable(closer):
try:
closer()
except Exception: # pragma: no cover
log.debug("closing %s failed", type(source).__name__, exc_info=True)
def fetch_all(settings: AppSettings) -> list[SourceResult]:
"""Fetch every enabled source in parallel; never raises."""
sources = build_sources(settings)
if not sources:
return []
def run(name: str, source: Any) -> SourceResult:
try:
items: list[TodoItem] = source.fetch()
return SourceResult(source=name, items=items)
except SourceError as exc:
return SourceResult.failed(name, str(exc))
except Exception as exc: # pragma: no cover - defensive
log.exception("source %s blew up", name)
return SourceResult.failed(name, f"unexpected error: {exc}")
try:
with ThreadPoolExecutor(max_workers=len(sources)) as pool:
futures = [pool.submit(run, name, src) for name, src in sources.items()]
return [future.result() for future in futures]
finally:
for source in sources.values():
_close(source)
def check_source(settings: AppSettings, name: str) -> tuple[bool, str]:
"""Verify one source's credentials — what the settings page's Test button calls."""
sources = build_sources(settings)
source = sources.get(name)
if source is None:
return False, f"{SOURCE_LABELS.get(name, name)} is not enabled or not configured."
try:
return True, source.check()
except SourceError as exc:
return False, str(exc)
except Exception as exc: # pragma: no cover
return False, f"unexpected error: {exc}"
finally:
for built in sources.values():
_close(built)