Spaces:
Sleeping
Sleeping
| """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"] | |