chore: add changelog and documentation updates
All checks were successful
CI / lint-and-test (push) Successful in 17s

- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format.
- Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs.
- Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings.
- Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers.
- Included new sections in the README for changelog and documentation links, improving accessibility to project information.
This commit is contained in:
2026-02-20 15:32:10 +03:00
parent b61e1ca8a5
commit 86f6d66865
88 changed files with 28912 additions and 118 deletions

View File

@@ -1,4 +1,8 @@
"""Load configuration from environment. BOT_TOKEN is not validated on import; check in main/entry point."""
"""Load configuration from environment (e.g. .env via python-dotenv).
BOT_TOKEN is not validated on import; call require_bot_token() in the entry point
when running the bot.
"""
import re
import os
@@ -17,7 +21,14 @@ _PHONE_DIGITS_RE = re.compile(r"\D")
def normalize_phone(phone: str | None) -> str:
"""Return phone as digits only (spaces, +, parentheses, dashes removed). Empty string if None/empty."""
"""Return phone as digits only (spaces, +, parentheses, dashes removed).
Args:
phone: Raw phone string or None.
Returns:
Digits-only string, or empty string if None or empty.
"""
if not phone or not isinstance(phone, str):
return ""
return _PHONE_DIGITS_RE.sub("", phone.strip())
@@ -43,7 +54,7 @@ def _parse_phone_list(raw: str) -> set[str]:
@dataclass(frozen=True)
class Settings:
"""Optional injectable settings built from env. Tests can override or build from env."""
"""Injectable settings built from environment. Used in tests or when env is overridden."""
bot_token: str
database_url: str
@@ -62,7 +73,11 @@ class Settings:
@classmethod
def from_env(cls) -> "Settings":
"""Build Settings from current environment (same logic as module-level vars)."""
"""Build Settings from current environment (same logic as module-level variables).
Returns:
Settings instance with all fields populated from env.
"""
bot_token = os.getenv("BOT_TOKEN") or ""
raw_allowed = os.getenv("ALLOWED_USERNAMES", "").strip()
allowed = {
@@ -143,18 +158,39 @@ DEFAULT_LANGUAGE = _normalize_default_language(
def is_admin(username: str) -> bool:
"""True if the given Telegram username (no @, any case) is in ADMIN_USERNAMES."""
"""Check if Telegram username is in ADMIN_USERNAMES.
Args:
username: Telegram username (with or without @; case-insensitive).
Returns:
True if in ADMIN_USERNAMES.
"""
return (username or "").strip().lower() in ADMIN_USERNAMES
def can_access_miniapp(username: str) -> bool:
"""True if username is in ALLOWED_USERNAMES or ADMIN_USERNAMES."""
"""Check if username is allowed to open the calendar Miniapp.
Args:
username: Telegram username (with or without @; case-insensitive).
Returns:
True if in ALLOWED_USERNAMES or ADMIN_USERNAMES.
"""
u = (username or "").strip().lower()
return u in ALLOWED_USERNAMES or u in ADMIN_USERNAMES
def can_access_miniapp_by_phone(phone: str | None) -> bool:
"""True if normalized phone is in ALLOWED_PHONES or ADMIN_PHONES."""
"""Check if phone (set via /set_phone) is allowed to open the Miniapp.
Args:
phone: Raw phone string or None.
Returns:
True if normalized phone is in ALLOWED_PHONES or ADMIN_PHONES.
"""
normalized = normalize_phone(phone)
if not normalized:
return False
@@ -162,13 +198,27 @@ def can_access_miniapp_by_phone(phone: str | None) -> bool:
def is_admin_by_phone(phone: str | None) -> bool:
"""True if normalized phone is in ADMIN_PHONES."""
"""Check if phone is in ADMIN_PHONES.
Args:
phone: Raw phone string or None.
Returns:
True if normalized phone is in ADMIN_PHONES.
"""
normalized = normalize_phone(phone)
return bool(normalized and normalized in ADMIN_PHONES)
def require_bot_token() -> None:
"""Raise SystemExit with a clear message if BOT_TOKEN is not set. Call from entry point."""
"""Raise SystemExit with a clear message if BOT_TOKEN is not set.
Call from the application entry point (e.g. main.py or duty_teller.run) so the
process exits with a helpful message instead of failing later.
Raises:
SystemExit: If BOT_TOKEN is empty.
"""
if not BOT_TOKEN:
raise SystemExit(
"BOT_TOKEN is not set. Copy .env.example to .env and set your token from @BotFather."