Files
2026-08-07 21:17:17 +03:30

110 lines
4.1 KiB
Python

"""Environment-sourced configuration classes (12-factor factor 3).
Every value comes from os.environ. No secret, DB URL, or filesystem path
is ever hardcoded; .env.example documents every variable a deployment
must set.
"""
from __future__ import annotations
import os
from datetime import timedelta
def _bool_env(name: str, default: bool = False) -> bool:
"""Parse a boolean-ish environment variable."""
val = os.environ.get(name)
return default if val is None else val.strip().lower() in {"1", "true", "yes", "on"}
def _engine_options_for(database_url: str) -> dict:
"""pool_size/pool_recycle are QueuePool-only kwargs.
BUG FIX (caught by actually booting the app): SQLite's default pool
classes (NullPool for file DBs, StaticPool for :memory:) raise
TypeError if handed pool_size/pool_recycle at all — they're not
silently ignored. Chapter 06 makes SQLite the default engine and
MySQL the opt-in fallback, so these kwargs are only meaningful (and
only passed) when DATABASE_URL actually points at a non-SQLite engine.
"""
if database_url.startswith("sqlite"):
return {}
return {
"pool_size": int(os.environ.get("DB_POOL_SIZE", "3")),
"pool_recycle": int(os.environ.get("DB_POOL_RECYCLE_SECONDS", "280")),
"pool_pre_ping": True,
}
class BaseConfig:
"""Shared config. Subclasses override only what differs per environment."""
SECRET_KEY: str | None = os.environ.get("SECRET_KEY")
# Chapter 06: SQLite/WAL default; swappable via DATABASE_URL without code changes.
SQLALCHEMY_DATABASE_URI: str = os.environ.get("DATABASE_URL", "sqlite:///kavosh.db")
SQLALCHEMY_ENGINE_OPTIONS: dict = _engine_options_for(SQLALCHEMY_DATABASE_URI)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Chapter 03: 150 max DB connections shared across up to 60 entry
# processes -> keep each process's pool small. Exposed as its own
# config key (not just buried inside SQLALCHEMY_ENGINE_OPTIONS) so
# budget.py can validate the *intended* setting regardless of whether
# the active engine (SQLite) actually consumes it.
DB_POOL_SIZE: int = int(os.environ.get("DB_POOL_SIZE", "3"))
# Chapter 03: filesystem cache, not Redis.
CACHE_TYPE = os.environ.get("CACHE_TYPE", "FileSystemCache")
CACHE_DIR = os.environ.get("CACHE_DIR", "/tmp/kavosh-cache")
CACHE_DEFAULT_TIMEOUT = int(os.environ.get("CACHE_DEFAULT_TIMEOUT", "60"))
# Chapter 12: secure session cookie flags.
SESSION_COOKIE_SECURE = _bool_env("SESSION_COOKIE_SECURE", True)
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
PERMANENT_SESSION_LIFETIME = timedelta(
hours=int(os.environ.get("SESSION_LIFETIME_HOURS", "12"))
)
WTF_CSRF_ENABLED = True
# Chapter 04/12: upload constraints.
UPLOAD_MAX_SIZE_MB = int(os.environ.get("UPLOAD_MAX_SIZE_MB", "500"))
MAX_CONTENT_LENGTH = UPLOAD_MAX_SIZE_MB * 1024 * 1024
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/kavosh/uploads")
PARSE_BATCH_SIZE = int(os.environ.get("PARSE_BATCH_SIZE", "5000"))
ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL")
class DevConfig(BaseConfig):
DEBUG = True
SESSION_COOKIE_SECURE = False # allow plain-http local dev
class ProdConfig(BaseConfig):
DEBUG = False
class TestConfig(BaseConfig):
TESTING = True
DEBUG = True # lets asset() tolerate a missing Vite manifest during tests
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
SQLALCHEMY_ENGINE_OPTIONS = {} # always in-memory SQLite regardless of DATABASE_URL
WTF_CSRF_ENABLED = False
_CONFIGS = {"development": DevConfig, "production": ProdConfig, "testing": TestConfig}
def get_config(config_name: str | None = None):
"""Resolve a config class from FLASK_ENV or an explicit name.
Defaults to `production` if unset, so an unconfigured deployment never
silently runs with DEBUG on.
"""
name = config_name or os.environ.get("FLASK_ENV", "production")
try:
return _CONFIGS[name]
except KeyError as exc:
raise ValueError(f"Unknown config_name {name!r}; expected one of {list(_CONFIGS)}") from exc