110 lines
4.1 KiB
Python
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
|