Files
kanbante/app/config.py
T
Spark 395ab6e14c feat(capture): capture éclair — API à jeton dédié + écoute ntfy (issue #12)
POST /api/capture (X-Capture-Token ou Bearer; 503 si non configuré;
exempté Basic Auth/CSRF car machine-à-machine), JSON ou texte libre
(1re ligne = titre), dépôt par défaut KANBANTE_CAPTURE_REPO.
Module app/capture.py: parsing partagé + abonné ntfy en tâche de
fond (reconnexion, curseur kv_store anti-rejeu, tag 'kanban'...
'kanbante' ignoré). _do_capture commun: issue créée, carte placée,
événement 'created' source 'capture'.
2026-10-11 12:40:31 -04:00

154 lines
6.6 KiB
Python

"""
Centralised settings for Kanbante.
Every setting is read from the environment at call time (no import-time
freezing), so tests and operators can change them without code edits.
Environment variables:
GITEA_URL Base URL of the Gitea instance.
GITEA_TOKEN Gitea API token (read/write issues).
KANBANTE_DB_PATH Path of the local SQLite database.
KANBANTE_AUTH_USER Basic-auth username (with _PASSWORD: enables auth).
KANBANTE_AUTH_PASSWORD Basic-auth password.
KANBANTE_REQUIRE_AUTH "1" = refuse to serve if auth is not configured.
KANBANTE_ACTIVE_WINDOW_DAYS A non-archived repo updated within this many
days counts as "active" (default 365).
KANBANTE_CACHE_TTL Seconds to cache Gitea list responses (default 30).
KANBANTE_MAX_NOTE_CHARS Maximum size of a project note (default 20000).
KANBANTE_SYNC_GITEA "1" (default) = moving a card to/from "done"
closes / reopens the Gitea issue.
KANBANTE_WIP_LIMITS Comma list "column:limit" (e.g.
"in_progress:5,review:3"). Columns not
listed are unlimited. Soft limits: the UI
warns, moves are never blocked.
KANBANTE_AGING_DAYS Days in the same column before a card is
flagged as aging (default 7; "stale" at 2x).
KANBANTE_WEBHOOK_SECRET Shared secret for the Gitea webhook
(POST /webhooks/gitea). When set, the
X-Gitea-Signature HMAC is enforced.
KANBANTE_SYNC_LABELS "1" (default) = mirror intermediate
columns to Gitea labels named kanban:<col>.
KANBANTE_NTFY_URL Base URL of an ntfy server (e.g.
https://ntfy.sh or self-hosted).
KANBANTE_NTFY_TOPIC ntfy topic for the daily briefing.
KANBANTE_NTFY_TOKEN Optional Bearer token for ntfy.
KANBANTE_BRIEFING_TIME "HH:MM" (server local time) at which
the daily briefing is sent to ntfy.
Empty (default) = no automatic send,
the /briefing page stays available.
KANBANTE_CAPTURE_REPO Default repo for quick capture
(POST /api/capture, the /capture page
and the ntfy capture listener), e.g.
"Projets/kanbante". Empty = captures
must name their repo explicitly.
KANBANTE_CAPTURE_TOKEN Dedicated token for POST /api/capture
(header X-Capture-Token or Bearer).
Empty (default) = the endpoint is
disabled (503). Distinct from the
Gitea token; revocable on its own.
KANBANTE_CAPTURE_NTFY_TOPIC ntfy topic listened to for captures:
each message published there becomes
an issue in KANBANTE_CAPTURE_REPO.
Uses KANBANTE_NTFY_URL/_TOKEN as the
server. Empty (default) = no listener.
"""
import os
from dataclasses import dataclass
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
APP_VERSION = "0.7.0"
def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in ("1", "true", "yes", "on")
def _env_int(name: str, default: int) -> int:
raw = os.getenv(name)
try:
return int(raw) if raw is not None else default
except ValueError:
return default
def _env_float(name: str, default: float) -> float:
raw = os.getenv(name)
try:
return float(raw) if raw is not None else default
except ValueError:
return default
def _env_wip_limits(name: str) -> dict[str, int]:
limits: dict[str, int] = {}
for chunk in os.getenv(name, "").split(","):
column, sep, value = chunk.partition(":")
column = column.strip()
if sep and column and value.strip().isdigit():
limits[column] = int(value.strip())
return limits
@dataclass(frozen=True)
class Settings:
gitea_url: str = "https://git.dracodev.net"
gitea_token: str = ""
db_path: str = "~/.kanbante/kanbante.db"
auth_user: str = ""
auth_password: str = ""
require_auth: bool = False
active_window_days: int = 365
cache_ttl: float = 30.0
max_note_chars: int = 20_000
sync_gitea: bool = True
wip_limits: dict | None = None
aging_days: int = 7
webhook_secret: str = ""
sync_labels: bool = True
ntfy_url: str = ""
ntfy_topic: str = ""
ntfy_token: str = ""
briefing_time: str = ""
capture_repo: str = ""
capture_token: str = ""
capture_ntfy_topic: str = ""
@property
def auth_enabled(self) -> bool:
return bool(self.auth_user and self.auth_password)
def get_settings() -> Settings:
"""Build settings from the current environment."""
return Settings(
gitea_url=os.getenv("GITEA_URL", "https://git.dracodev.net").rstrip("/"),
gitea_token=os.getenv("GITEA_TOKEN", ""),
db_path=os.getenv(
"KANBANTE_DB_PATH", os.path.expanduser("~/.kanbante/kanbante.db")
),
auth_user=os.getenv("KANBANTE_AUTH_USER", ""),
auth_password=os.getenv("KANBANTE_AUTH_PASSWORD", ""),
require_auth=_env_bool("KANBANTE_REQUIRE_AUTH", False),
active_window_days=_env_int("KANBANTE_ACTIVE_WINDOW_DAYS", 365),
cache_ttl=_env_float("KANBANTE_CACHE_TTL", 30.0),
max_note_chars=_env_int("KANBANTE_MAX_NOTE_CHARS", 20_000),
sync_gitea=_env_bool("KANBANTE_SYNC_GITEA", True),
wip_limits=_env_wip_limits("KANBANTE_WIP_LIMITS"),
aging_days=_env_int("KANBANTE_AGING_DAYS", 7),
webhook_secret=os.getenv("KANBANTE_WEBHOOK_SECRET", ""),
sync_labels=_env_bool("KANBANTE_SYNC_LABELS", True),
ntfy_url=os.getenv("KANBANTE_NTFY_URL", ""),
ntfy_topic=os.getenv("KANBANTE_NTFY_TOPIC", ""),
ntfy_token=os.getenv("KANBANTE_NTFY_TOKEN", ""),
briefing_time=os.getenv("KANBANTE_BRIEFING_TIME", ""),
capture_repo=os.getenv("KANBANTE_CAPTURE_REPO", ""),
capture_token=os.getenv("KANBANTE_CAPTURE_TOKEN", ""),
capture_ntfy_topic=os.getenv("KANBANTE_CAPTURE_NTFY_TOPIC", ""),
)