Coverage for config.py: 100%
36 statements
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
1import tempfile
2from typing import Optional
4from pydantic import Field, field_validator, model_validator
5from pydantic_settings import BaseSettings, SettingsConfigDict
8class Settings(BaseSettings):
9 '''Deployment settings, loaded and validated once at startup.
11 Field names are UPPER_CASE and match the environment variable names they
12 are read from. Reads are case-sensitive to mirror the previous
13 ``os.environ`` access.
14 '''
15 model_config = SettingsConfigDict(case_sensitive=True)
17 DEBUG: bool = False
18 TESTING: bool = False
20 MONGO_HOST: str = 'mongomock://localhost'
22 MINIO_HOST: Optional[str] = None
23 MINIO_ACCESS_KEY: Optional[str] = None
24 MINIO_SECRET_KEY: Optional[str] = None
25 MINIO_BUCKET: str = 'normal-oj-testing'
26 MINIO_REGION: Optional[str] = None
28 REDIS_HOST: Optional[str] = None
29 REDIS_PORT: Optional[int] = None
31 # JWT_EXP is kept as an int (days); the timedelta is built at the use site.
32 # Typing it as timedelta would make pydantic parse env ints as seconds.
33 JWT_EXP: int = 30
34 JWT_ISS: str = 'test.test'
35 JWT_SECRET: str = 'SuperSecretString'
37 SMTP_SERVER: Optional[str] = None
38 SMTP_NOREPLY: Optional[str] = None
39 SMTP_NOREPLY_PASSWORD: Optional[str] = None
41 # Shared secret runners present when registering (spec §7.1). Unset/empty
42 # ⇒ registration is disabled, fail closed. Startup snapshot: rotating it
43 # takes a restart; per-runner revocation stays immediate (ADR-0005).
44 RUNNER_REGISTRATION_TOKEN: Optional[str] = None
46 SUBMISSION_TMP_DIR: str = Field(
47 default_factory=lambda: tempfile.mkdtemp(suffix='noj-submissions'))
49 @field_validator('TESTING', mode='before')
50 @classmethod
51 def _lenient_testing(cls, v):
52 '''Interpret ``1``, ``true``, and ``yes`` (case-insensitive) as True.
54 Any other value — including the common mistake of setting
55 ``TESTING=false`` — is treated as False.
56 '''
57 if isinstance(v, str):
58 return v.lower() in ('1', 'true', 'yes')
59 return v
61 @model_validator(mode='after')
62 def _require_smtp_noreply(self):
63 if self.SMTP_SERVER is not None and self.SMTP_NOREPLY is None:
64 raise ValueError("missing required configuration 'SMTP_NOREPLY'")
65 return self
68settings = Settings()