"""Application configuration (env-overridable). No third-party data egress.""" from __future__ import annotations import os from pathlib import Path from urllib.parse import quote_plus from dotenv import load_dotenv # Load ar-aging-app/.env (or cwd) before reading settings. _APP_ROOT = Path(__file__).resolve().parent.parent.parent # .../ar-aging-app load_dotenv(_APP_ROOT / ".env") load_dotenv() # also allow cwd overrides BASE_DIR = Path(__file__).resolve().parent.parent # .../backend DATA_DIR = Path(os.environ.get("AR_DATA_DIR", str(BASE_DIR / "data"))) UPLOAD_DIR = DATA_DIR / "uploads" EXPORT_DIR = DATA_DIR / "exports" # --------------------------------------------------------------------------- database # The store is switchable so the app is never blocked on infrastructure: # AR_DB_BACKEND=sqlite a single local file — zero setup, good for a laptop or a demo # AR_DB_BACKEND=mysql the shared server, for real multi-user month-end work # Unset, it picks MySQL when a real MYSQL_HOST is configured and SQLite otherwise, so a # machine with no database installed still runs instead of failing at import. _PLACEHOLDER_HOSTS = {"", "your-mysql-host.example.com", "changeme", "todo"} SQLITE_PATH = Path(os.environ.get("AR_SQLITE_PATH", str(DATA_DIR / "ar_aging.db"))) MYSQL_HOST = os.environ.get("MYSQL_HOST", "") MYSQL_PORT = int(os.environ.get("MYSQL_PORT", "3306")) MYSQL_USER = os.environ.get("MYSQL_USER", "") MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", "") MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE", "") MYSQL_SLOW_QUERY_MS = int(os.environ.get("MYSQL_SLOW_QUERY_MS", "500")) MYSQL_POOL_SIZE = int(os.environ.get("MYSQL_POOL_SIZE", "10")) MYSQL_POOL_RECYCLE = int(os.environ.get("MYSQL_POOL_RECYCLE", "3600")) def _mysql_configured() -> bool: return (MYSQL_HOST.strip().lower() not in _PLACEHOLDER_HOSTS and bool(MYSQL_USER) and bool(MYSQL_DATABASE)) DB_BACKEND = (os.environ.get("AR_DB_BACKEND") or ("mysql" if _mysql_configured() else "sqlite")).strip().lower() if DB_BACKEND not in ("sqlite", "mysql"): raise RuntimeError(f"AR_DB_BACKEND must be 'sqlite' or 'mysql' (got {DB_BACKEND!r}).") def mysql_url() -> str: if not _mysql_configured(): raise RuntimeError( "MYSQL_HOST, MYSQL_USER, and MYSQL_DATABASE are required for the mysql " "backend. Copy example.env to .env and fill in real credentials, or set " "AR_DB_BACKEND=sqlite to use a local file." ) user = quote_plus(MYSQL_USER) password = quote_plus(MYSQL_PASSWORD) return ( f"mysql+pymysql://{user}:{password}" f"@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}" f"?charset=utf8mb4" ) def database_url() -> str: if DB_BACKEND == "mysql": return mysql_url() SQLITE_PATH.parent.mkdir(parents=True, exist_ok=True) return f"sqlite:///{SQLITE_PATH}" def database_label() -> str: """Human-readable target, for logs and the launcher — never includes the password.""" if DB_BACKEND == "mysql": return f"MySQL {MYSQL_USER}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}" return f"SQLite {SQLITE_PATH}" # Retention: temp uploads/exports older than this are purged (0 = keep forever). RETENTION_DAYS = int(os.environ.get("AR_RETENTION_DAYS", "30")) # Defaults DEFAULT_CLEARING_LAG_DAYS = int(os.environ.get("AR_CLEARING_LAG_DAYS", "2")) DEFAULT_TOLERANCE = float(os.environ.get("AR_TOLERANCE", "0.01")) MAX_UPLOAD_BYTES = int(os.environ.get("AR_MAX_UPLOAD_BYTES", str(2 * 1024 * 1024 * 1024))) # 2 GB ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"} # CORS (frontend dev server) CORS_ORIGINS = os.environ.get( "AR_CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173" ).split(",") def ensure_dirs() -> None: for d in (DATA_DIR, UPLOAD_DIR, EXPORT_DIR): d.mkdir(parents=True, exist_ok=True)