Files
2026-04-16 13:22:23 -05:00

106 lines
6.7 KiB
Python

"""
config.py — Central configuration loader for NAS Drive Price Tracker.
Import this module at the top of any entry-point (api/server.py,
scraper/runner.py) to load the .env file before anything else reads
os.getenv(). Every other module continues using os.getenv() unchanged.
.env file location (searched in order):
1. Path in NAS_TRACKER_ENV environment variable
2. <project_root>/.env (default)
3. Already-set environment variables are never overwritten, so
real env vars always take precedence over .env values.
All recognised variables and their defaults are documented below.
"""
import os
import sys
from pathlib import Path
# ── Locate the project root ───────────────────────────────────────────────
# This file lives at <root>/config.py, so its parent IS the root.
ROOT = Path(__file__).parent.resolve()
def load(env_path: str | Path | None = None, override: bool = False) -> Path | None:
"""
Load a .env file using python-dotenv.
Parameters
----------
env_path : path to a specific .env file; auto-detected if None.
override : if True, .env values overwrite existing env vars.
Default False — real env vars always win.
Returns the resolved path that was loaded, or None if no file found.
"""
try:
from dotenv import load_dotenv
except ImportError:
print(
"[config] python-dotenv is not installed. "
"Run: pip install python-dotenv\n"
"[config] Falling back to environment variables only.",
file=sys.stderr,
)
return None
# Resolve path
if env_path is not None:
candidate = Path(env_path).resolve()
elif os.getenv("NAS_TRACKER_ENV"):
candidate = Path(os.getenv("NAS_TRACKER_ENV")).resolve()
else:
candidate = ROOT / ".env"
if not candidate.is_file():
print(
f"[config] No .env file found at {candidate}. "
"Using environment variables only.",
file=sys.stderr,
)
return None
loaded = load_dotenv(candidate, override=override)
if loaded:
print(f"[config] Loaded environment from {candidate}", file=sys.stderr)
return candidate
# ── Auto-load when this module is imported ───────────────────────────────
# Calling load() here means `import config` at the top of any entry-point
# is all that's needed — no extra call required.
load()
# ── Variable reference (for documentation; not enforced here) ────────────
#
# ┌─────────────────────────┬─────────────────────────┬───────────────────────────────────────────┐
# │ Variable │ Default │ Description │
# ├─────────────────────────┼─────────────────────────┼───────────────────────────────────────────┤
# │ SMTP_HOST │ localhost │ SMTP server hostname │
# │ SMTP_PORT │ 587 │ SMTP port (587=STARTTLS, 465=SSL, 25=none)│
# │ SMTP_USER │ (none) │ SMTP login username │
# │ SMTP_PASS │ (none) │ SMTP login password / app-password │
# │ SMTP_FROM_NAME │ NAS Drive Tracker │ "From" display name in emails │
# │ SMTP_FROM_EMAIL │ SMTP_USER or noreply@.. │ "From" address in emails │
# │ APP_BASE_URL │ http://localhost:5000 │ Base URL for links in emails │
# ├─────────────────────────┼─────────────────────────┼───────────────────────────────────────────┤
# │ PORT │ 5000 │ API server port │
# │ FLASK_ENV │ (none) │ Set to "development" for debug mode │
# │ FLASK_SECRET │ random │ Flask session secret (set in production!) │
# ├─────────────────────────┼─────────────────────────┼───────────────────────────────────────────┤
# │ SCRAPE_INTERVAL_HOURS │ 6 │ Hours between scheduled scrape runs │
# │ SCRAPE_WORKERS │ 3 │ Parallel scraper threads per run │
# ├─────────────────────────┼─────────────────────────┼───────────────────────────────────────────┤
# │ ALERT_DROP_PCT │ 20 │ Alert when % off MSRP ≥ this value │
# │ ALERT_DROP_ABS │ 30 │ Alert when absolute $ drop ≥ this value │
# │ ALERT_EMAIL_TO │ (none) │ Comma-separated alert recipient addresses │
# ├─────────────────────────┼─────────────────────────┼───────────────────────────────────────────┤
# │ PROXY_URL │ (none) │ HTTP/S proxy for all scraper requests │
# │ LOG_LEVEL │ INFO │ Logging verbosity: DEBUG/INFO/WARNING │
# │ NAS_TRACKER_ENV │ (none) │ Override path to .env file │
# └─────────────────────────┴─────────────────────────┴───────────────────────────────────────────┘