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
115 lines
4.5 KiB
Python
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,
|
|
)
|