"""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. ``/../.env`` (sibling of the ``app/`` package — the canonical location: ``CyberArena/.env``) 4. ``/.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 "" 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 ''}", ) 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"]