This commit is contained in:
timka123 committed 2026-10-18 18:02:05 +03:00
commit 17a0a3c4c9
121 files changed
+17641

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
"""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 concurrent.futures import ThreadPoolExecutor
from typing import Any, Callable
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 complete_remote(settings: AppSettings, item: TodoItem) -> tuple[bool, str | None]:
"""Close a task in its own system. Returns `(did_write, error)`."""
sources = build_sources(settings)
source = sources.get(item.source)
if source is None:
return False, None
try:
return bool(source.complete(item)), None
except SourceError as exc:
return False, str(exc)
finally:
for built in sources.values():
_close(built)
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)