CyberArena / app /_env.py
Hussien Haider
H
73b272a
Raw
History Blame Contribute Delete
6.55 kB
"""Centralised .env loader.
This module is imported **first** by every other module that touches
``os.environ`` (currently :mod:`app.main` and :mod:`app.core.config`).
It searches a small list of candidate locations for the project's
``.env`` file and loads it via :func:`dotenv.load_dotenv`.
**Search order** (first hit wins):
1. ``./.env`` (process working directory)
2. ``../.env`` (one level up — useful when running from ``app/``)
3. ``<this_file>/../.env`` (sibling of the ``app/`` package — the
canonical location: ``CyberArena/.env``)
4. ``<this_file>/.env`` (inside the ``app/`` package — auto-created
copies; we tolerate them but don't prefer them)
The function prints a single line of status to stdout so the operator
knows exactly which file was used, and which critical variables were
actually picked up.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Iterable, Optional
# --------------------------------------------------------------------------- #
# Internal helpers #
# --------------------------------------------------------------------------- #
_TRUTHY = {"1", "true", "yes", "on"}
def _is_loaded_marker_set() -> bool:
"""We use a sentinel env var so we never load twice."""
return os.environ.get("_APEX_ENV_LOADED") == "1"
def _set_loaded_marker() -> None:
os.environ["_APEX_ENV_LOADED"] = "1"
def _mask(value: str) -> str:
"""Return a masked version of a secret suitable for printing."""
if not value:
return "<MISSING>"
if len(value) <= 8:
return "***"
return f"{value[:4]}{value[-4:]} (len={len(value)})"
def _candidate_paths() -> Iterable[Path]:
"""Yield the candidate .env locations, in priority order."""
here = Path(__file__).resolve()
app_dir = here.parent
backend_dir = app_dir.parent # CyberArena/
cwd = Path.cwd()
seen: set[Path] = set()
for path in (
cwd / ".env",
cwd.parent / ".env", # one above cwd (in case you ran from app/)
backend_dir / ".env", # the canonical location
app_dir / ".env", # the stray copy inside the package
):
try:
resolved = path.resolve()
except FileNotFoundError:
continue
if resolved in seen:
continue
seen.add(resolved)
yield path
def _find_env() -> Optional[Path]:
"""Return the first existing .env in the candidate list."""
for path in _candidate_paths():
if path.is_file():
return path
return None
# --------------------------------------------------------------------------- #
# Public entry point #
# --------------------------------------------------------------------------- #
def load_app_env(verbose: bool = True) -> Optional[Path]:
"""Load the CyberArena ``.env`` file into ``os.environ``.
Idempotent: a second call is a no-op. Returns the path that was
loaded, or ``None`` if no file was found.
In containerised deployments (Hugging Face Spaces, Docker, etc.)
the secrets are injected as real environment variables, so the
absence of a ``.env`` file is **not** a problem — we just print an
info-level line instead of a warning.
"""
if _is_loaded_marker_set():
return None
from dotenv import load_dotenv # local import — cheap, no I/O
env_path = _find_env()
if env_path is None:
# If process env already has the critical secrets (HF Spaces,
# Docker, Kubernetes, …), treat the deployment as healthy and
# only print an info line. Otherwise warn loudly so the local
# developer knows they forgot to copy `.env.example`.
critical = (
os.environ.get("SUPABASE_URL"),
os.environ.get("SUPABASE_ANON_KEY"),
)
if all(critical):
if verbose:
print(
"[env] No .env file, but SUPABASE_URL and "
"SUPABASE_ANON_KEY are set in the process environment. "
"Using those (Hugging Face Spaces / Docker).",
)
else:
print(
"[env] WARNING: no .env file found. Searched:",
file=sys.stderr,
)
for p in _candidate_paths():
print(f" - {p}", file=sys.stderr)
print(
"[env] Create CyberArena/.env with SUPABASE_URL / "
"SUPABASE_ANON_KEY / CLOUDFLARE_* / GROQ_API_KEY.",
file=sys.stderr,
)
_set_loaded_marker()
return None
# ``override=False`` so process env wins over .env when both are set
# (e.g. secrets injected by Hugging Face Spaces / Docker).
load_dotenv(dotenv_path=str(env_path), override=False, encoding="utf-8")
_set_loaded_marker()
if verbose:
supabase_url = os.environ.get("SUPABASE_URL", "")
supabase_key = os.environ.get("SUPABASE_ANON_KEY", "")
cf_token = os.environ.get("CLOUDFLARE_API_TOKEN", "")
groq_key = os.environ.get("GROQ_API_KEY", "")
nvidia_key = os.environ.get("NVIDIA_API_KEY", "")
mistral_key = os.environ.get("MISTRAL_API_KEY", "")
print(f"[env] Loaded: {env_path}")
print(
f"[env] SUPABASE_URL = {supabase_url or '<MISSING>'}",
)
print(
f"[env] SUPABASE_ANON_KEY = {_mask(supabase_key)}",
)
print(
f"[env] CLOUDFLARE_API_TOKEN= {_mask(cf_token)}",
)
print(
f"[env] GROQ_API_KEY = {_mask(groq_key)}",
)
print(
f"[env] NVIDIA_API_KEY = {_mask(nvidia_key)}",
)
print(
f"[env] MISTRAL_API_KEY = {_mask(mistral_key)}",
)
return env_path
def assert_critical_env(*keys: str) -> None:
"""Raise :class:`RuntimeError` if any of ``keys`` is missing.
Use it right after :func:`load_app_env` to fail loudly instead of
later with a cryptic ``KeyError`` deep in an httpx call.
"""
missing = [k for k in keys if not os.environ.get(k)]
if missing:
raise RuntimeError(
"Missing required environment variables: "
+ ", ".join(missing)
+ ".\nMake sure CyberArena/.env is present and contains them."
)
__all__ = ["load_app_env", "assert_critical_env"]