Add configuration rules, refactor settings management, and enhance import functionality
- Introduced a new configuration file `.cursorrules` to define coding standards, error handling, testing requirements, and project-specific guidelines. - Refactored `config.py` to implement a `Settings` dataclass for better management of environment variables, improving testability and maintainability. - Updated the import duty schedule handler to utilize session management with `session_scope`, ensuring proper database session handling. - Enhanced the import service to streamline the duty schedule import process, improving code organization and readability. - Added new service layer functions to encapsulate business logic related to group duty pinning and duty schedule imports. - Updated README documentation to reflect the new configuration structure and improved import functionality.
This commit is contained in:
59
config.py
59
config.py
@@ -1,11 +1,66 @@
|
||||
"""Load configuration from environment. Fail fast if BOT_TOKEN is missing."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
"""Optional injectable settings built from env. Tests can override or build from env."""
|
||||
|
||||
bot_token: str
|
||||
database_url: str
|
||||
mini_app_base_url: str
|
||||
http_port: int
|
||||
allowed_usernames: set[str]
|
||||
admin_usernames: set[str]
|
||||
mini_app_skip_auth: bool
|
||||
init_data_max_age_seconds: int
|
||||
cors_origins: list[str]
|
||||
external_calendar_ics_url: str
|
||||
duty_display_tz: str
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Settings":
|
||||
"""Build Settings from current environment (same logic as module-level vars)."""
|
||||
bot_token = os.getenv("BOT_TOKEN") or ""
|
||||
raw_allowed = os.getenv("ALLOWED_USERNAMES", "").strip()
|
||||
allowed = {
|
||||
s.strip().lstrip("@").lower() for s in raw_allowed.split(",") if s.strip()
|
||||
}
|
||||
raw_admin = os.getenv("ADMIN_USERNAMES", "").strip()
|
||||
admin = {
|
||||
s.strip().lstrip("@").lower() for s in raw_admin.split(",") if s.strip()
|
||||
}
|
||||
raw_cors = os.getenv("CORS_ORIGINS", "").strip()
|
||||
cors = (
|
||||
[_o.strip() for _o in raw_cors.split(",") if _o.strip()]
|
||||
if raw_cors and raw_cors != "*"
|
||||
else ["*"]
|
||||
)
|
||||
return cls(
|
||||
bot_token=bot_token,
|
||||
database_url=os.getenv("DATABASE_URL", "sqlite:///data/duty_teller.db"),
|
||||
mini_app_base_url=os.getenv("MINI_APP_BASE_URL", "").rstrip("/"),
|
||||
http_port=int(os.getenv("HTTP_PORT", "8080")),
|
||||
allowed_usernames=allowed,
|
||||
admin_usernames=admin,
|
||||
mini_app_skip_auth=os.getenv("MINI_APP_SKIP_AUTH", "").strip()
|
||||
in ("1", "true", "yes"),
|
||||
init_data_max_age_seconds=int(os.getenv("INIT_DATA_MAX_AGE_SECONDS", "0")),
|
||||
cors_origins=cors,
|
||||
external_calendar_ics_url=os.getenv(
|
||||
"EXTERNAL_CALENDAR_ICS_URL", ""
|
||||
).strip(),
|
||||
duty_display_tz=os.getenv("DUTY_DISPLAY_TZ", "Europe/Moscow").strip()
|
||||
or "Europe/Moscow",
|
||||
)
|
||||
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
if not BOT_TOKEN:
|
||||
raise SystemExit(
|
||||
@@ -45,7 +100,9 @@ CORS_ORIGINS = (
|
||||
EXTERNAL_CALENDAR_ICS_URL = os.getenv("EXTERNAL_CALENDAR_ICS_URL", "").strip()
|
||||
|
||||
# Timezone for displaying duty times in the pinned group message (e.g. Europe/Moscow).
|
||||
DUTY_DISPLAY_TZ = os.getenv("DUTY_DISPLAY_TZ", "Europe/Moscow").strip() or "Europe/Moscow"
|
||||
DUTY_DISPLAY_TZ = (
|
||||
os.getenv("DUTY_DISPLAY_TZ", "Europe/Moscow").strip() or "Europe/Moscow"
|
||||
)
|
||||
|
||||
|
||||
def is_admin(username: str) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user