Coverage for config.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2026-07-24 16:42 +0000

1import tempfile 

2from typing import Optional 

3 

4from pydantic import Field, field_validator, model_validator 

5from pydantic_settings import BaseSettings, SettingsConfigDict 

6 

7 

8class Settings(BaseSettings): 

9 '''Deployment settings, loaded and validated once at startup. 

10 

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) 

16 

17 DEBUG: bool = False 

18 TESTING: bool = False 

19 

20 MONGO_HOST: str = 'mongomock://localhost' 

21 

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 

27 

28 REDIS_HOST: Optional[str] = None 

29 REDIS_PORT: Optional[int] = None 

30 

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' 

36 

37 SMTP_SERVER: Optional[str] = None 

38 SMTP_NOREPLY: Optional[str] = None 

39 SMTP_NOREPLY_PASSWORD: Optional[str] = None 

40 

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 

45 

46 SUBMISSION_TMP_DIR: str = Field( 

47 default_factory=lambda: tempfile.mkdtemp(suffix='noj-submissions')) 

48 

49 @field_validator('TESTING', mode='before') 

50 @classmethod 

51 def _lenient_testing(cls, v): 

52 '''Interpret ``1``, ``true``, and ``yes`` (case-insensitive) as True. 

53 

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 

60 

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 

66 

67 

68settings = Settings()