Spaces:
Sleeping
Sleeping
File size: 6,550 Bytes
80a4a65 73b272a 80a4a65 73b272a 80a4a65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | """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"]
|