start project
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user