Files
chemenu/tools/chemenu/tasks/config.py
T
torben 1875449b31
CI / verify (push) Successful in 48s
Release / release (push) Successful in 35s
stack: Provider-Schicht für Aufgaben-Tracker mit Super-Productivity-Adapter (#124)
Files changed:
- .gitignore
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/config.py
- tools/chemenu/errors.py
- tools/chemenu/tasks/__init__.py
- tools/chemenu/tasks/config.py
- tools/chemenu/tasks/protocol.py
- tools/chemenu/tasks/superproductivity.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_superproductivity.py
- tools/chemenu/tests/test_tasks_config.py
- tools/chemenu/tests/test_tasks_protocol.py
2026-09-19 21:46:56 +02:00

115 lines
4.5 KiB
Python

"""Reads `.wikitool-tasks.json` (Gitea #124) - `config.TASKS_CONFIG_FILENAME`.
Same posture as `chemenu.upload.read_config`: absent means "no tracker
configured for this instance", a legitimate state that `doctor` reports as OK,
never as a fault. Malformed is a `ValidationError`, never silently ignored -
this file decides which provider (and which credentials) the review talks to,
so a broken one must not be read as "nothing configured".
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from chemenu import config
from chemenu.errors import ValidationError
# The providers this package ships an adapter for. Checked at read time so a
# typo in `provider` fails here, at the one place that knows the full list,
# rather than surfacing later as an unhelpful "unknown provider" from whatever
# code tried to dispatch on it.
KNOWN_PROVIDERS = ("superproductivity",)
@dataclass(frozen=True)
class Thresholds:
"""The weekly review's three staleness thresholds (#119 D10/D26) - each
named for the check it feeds, not for its unit alone, since two of the
three checks besides the "obvious" one also read a day count via
`datetime.timedelta`."""
stalled_waiting_days: int # check 2: a WAITING item older than this
unpaged_project_weeks: int # check 3: a tracker project with no kb/ page, older than this
someday_stale_months: int # check 5: a someday/maybe item untouched for this long
@dataclass(frozen=True)
class TasksConfig:
"""The opt-in, read from `.wikitool-tasks.json`."""
provider: str
thresholds: Thresholds
provider_config: dict[str, Any]
def read_config(root: "Any") -> "TasksConfig | None":
"""The tracker configuration for `root`, or `None` when the file is
absent - which means no tracker is configured for this instance, not that
one failed to load."""
from pathlib import Path
import json
path = Path(root) / config.TASKS_CONFIG_FILENAME
if not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValidationError(
f"{config.TASKS_CONFIG_FILENAME} is unreadable ({exc}). It decides which task "
"tracker the weekly review talks to, so a broken file is not treated as 'no "
"tracker configured' - fix it or delete it deliberately."
) from exc
if not isinstance(data, dict):
raise ValidationError(f"{config.TASKS_CONFIG_FILENAME} must contain a JSON object.")
expected = (
'{"schema": 1, "provider": "superproductivity", '
'"thresholds": {"stalled_waiting_days": 14, "unpaged_project_weeks": 3, '
'"someday_stale_months": 5}, "superproductivity": {"backups_dir": "...", '
'"api_base_url": "http://127.0.0.1:3876", "api_token": "..."}}'
)
try:
provider = str(data["provider"])
raw_thresholds = data["thresholds"]
stalled_waiting_days = int(raw_thresholds["stalled_waiting_days"])
unpaged_project_weeks = int(raw_thresholds["unpaged_project_weeks"])
someday_stale_months = int(raw_thresholds["someday_stale_months"])
except (KeyError, TypeError, ValueError) as exc:
raise ValidationError(
f"{config.TASKS_CONFIG_FILENAME} is missing or misshapes a required field ({exc}). "
f"Expected: {expected}"
) from exc
if provider not in KNOWN_PROVIDERS:
raise ValidationError(
f"{config.TASKS_CONFIG_FILENAME}: unknown provider {provider!r}. "
f"Known: {', '.join(KNOWN_PROVIDERS)}"
)
for field_name, value in (
("stalled_waiting_days", stalled_waiting_days),
("unpaged_project_weeks", unpaged_project_weeks),
("someday_stale_months", someday_stale_months),
):
if value <= 0:
raise ValidationError(
f"{config.TASKS_CONFIG_FILENAME}: thresholds.{field_name} must be positive."
)
provider_config = data.get(provider)
if not isinstance(provider_config, dict):
raise ValidationError(
f"{config.TASKS_CONFIG_FILENAME}: missing or non-object {provider!r} section "
f"holding that provider's own connection settings. Expected: {expected}"
)
return TasksConfig(
provider=provider,
thresholds=Thresholds(
stalled_waiting_days=stalled_waiting_days,
unpaged_project_weeks=unpaged_project_weeks,
someday_stale_months=someday_stale_months,
),
provider_config=provider_config,
)