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"]