File size: 4,003 Bytes
66fd359
 
 
 
 
 
a7ffb22
 
 
 
 
 
66fd359
 
 
 
 
 
 
 
 
 
 
 
a7ffb22
 
0815e01
fd475e7
 
aeda545
 
0815e01
 
 
66fd359
 
 
 
 
 
 
 
 
 
 
 
 
 
a7ffb22
 
 
 
 
66fd359
 
 
 
 
 
a7ffb22
 
0815e01
fd475e7
 
aeda545
 
0815e01
 
 
66fd359
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
"""Settings, read from the environment when the app is created."""
from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from pathlib import Path

from dotenv import load_dotenv

ENV_FILE = Path(__file__).parent / ".env"            # this app's own settings, if it has any
ROOT_ENV_FILE = Path(__file__).parent.parent / ".env"   # the repository's .env, shared with mathly1 (same Supabase project)


def _emails(name: str) -> frozenset[str]:
    return frozenset(re.split(r"[\s,;]+", os.environ.get(name, "").strip().lower())) - {""}


@dataclass(frozen=True)
class Settings:
    supabase_url: str = ""
    supabase_key: str = ""         # publishable key, sent to the browser for Google sign-in
    supabase_secret: str = ""      # secret key, server only
    teachers: frozenset[str] = field(default_factory=frozenset)
    developers: frozenset[str] = field(default_factory=frozenset)   # the developer role comes only from here
    admins: frozenset[str] = field(default_factory=frozenset)       # the first admins; others are given the role by an admin
    answer_bucket: str = "m2-answers"                               # the private bucket for answer pictures (not mathly1's)
    anthropic_key: str = ""                                         # the AI is unavailable without it (a send fails with "missing key")
    math_model: str = ""                                            # empty: the default model in ai.py
    gemini_key: str = ""                                            # the AI review of the way in tests (boardreview.py); none without it
    gemini_model: str = ""                                          # empty: the default (free tier) model in boardreview.py
    cache_seconds: float = 30                                       # how long slow-changing reads are remembered; 0 is off
    vapid_private_key: str = ""                                     # the server's push key; push notifications are off without it
    vapid_subject: str = ""                                         # a contact the push services may write to ("mailto:...")

    @property
    def supabase_on(self) -> bool:
        return bool(self.supabase_url and self.supabase_key and self.supabase_secret)

    def check(self) -> "Settings":
        """Either all three Supabase settings are set, or none (local development with the "dev only" sign-in).
        A half-configured server must not fall back to dev sign-in, which trusts any email address."""
        given = [bool(self.supabase_url), bool(self.supabase_key), bool(self.supabase_secret)]
        if any(given) and not all(given):
            raise ValueError("Set all of SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY, or none of them.")
        return self


def load_settings(env_file: Path | None = None) -> Settings:
    """Settings from the environment. Files fill in what the environment does not set, in this order: mathly2/.env, then the
    repository's .env. A real environment variable always wins, so deployments are unaffected. Passing `env_file` reads only that file."""
    for path in ([env_file] if env_file else [ENV_FILE, ROOT_ENV_FILE]):
        load_dotenv(path, override=False)
    env = lambda name: os.environ.get(name, "").strip()
    return Settings(
        supabase_url=env("SUPABASE_URL").rstrip("/"),
        supabase_key=env("SUPABASE_PUBLISHABLE_KEY"),
        supabase_secret=env("SUPABASE_SECRET_KEY"),
        teachers=_emails("TEACHER_EMAILS"),
        developers=_emails("DEVELOPER_EMAILS"),
        admins=_emails("ADMIN_EMAILS"),
        answer_bucket=env("MATHLY_M2_ANSWER_BUCKET") or "m2-answers",
        anthropic_key=env("ANTHROPIC_API_KEY"),
        math_model=env("MATH_MODEL"),
        gemini_key=env("GEMINI_API_KEY"),
        gemini_model=env("GEMINI_MODEL"),
        cache_seconds=float(env("MATHLY_CACHE_SECONDS") or 30),
        vapid_private_key=env("VAPID_PRIVATE_KEY"),
        vapid_subject=env("VAPID_SUBJECT"),
    ).check()