Coverage for app/core/config.py: 96%

52 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-26 10:48 +0000

1import warnings 

2from typing import Literal, Self 

3 

4from pydantic import ( 

5 EmailStr, 

6 HttpUrl, 

7 PostgresDsn, 

8 computed_field, 

9 field_validator, 

10 model_validator, 

11) 

12from pydantic_settings import BaseSettings, SettingsConfigDict 

13 

14 

15class Settings(BaseSettings): 

16 model_config = SettingsConfigDict( 

17 # Use top level .env file (one level above ./backend/) 

18 env_file="../.env", 

19 env_ignore_empty=True, 

20 extra="ignore", 

21 ) 

22 API_V1_STR: str = "/api/v1" 

23 SECRET_KEY: str 

24 # 60 minutes * 24 hours * 8 days = 8 days 

25 ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 

26 FRONTEND_HOST: str = "http://localhost:5173" 

27 FASTAPI_ENV: Literal["development"] | None = None 

28 

29 PROJECT_NAME: str 

30 SENTRY_DSN: HttpUrl | None = None 

31 DATABASE_URL: PostgresDsn 

32 

33 @field_validator("DATABASE_URL", mode="before") 

34 @classmethod 

35 def _use_psycopg_driver(cls, value: str | PostgresDsn) -> str: 

36 database_url = str(value) 

37 for scheme in ("postgres://", "postgresql://"): 

38 if database_url.startswith(scheme): 

39 return database_url.replace(scheme, "postgresql+psycopg://", 1) 

40 return database_url 

41 

42 SMTP_TLS: bool = True 

43 SMTP_SSL: bool = False 

44 SMTP_PORT: int = 587 

45 SMTP_HOST: str | None = None 

46 SMTP_USER: str | None = None 

47 SMTP_PASSWORD: str | None = None 

48 EMAILS_FROM_EMAIL: EmailStr | None = None 

49 EMAILS_FROM_NAME: str | None = None 

50 

51 @model_validator(mode="after") 

52 def _set_default_emails_from(self) -> Self: 

53 if not self.EMAILS_FROM_NAME: 

54 self.EMAILS_FROM_NAME = self.PROJECT_NAME 

55 return self 

56 

57 EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48 

58 

59 @computed_field # type: ignore[prop-decorator] 

60 @property 

61 def emails_enabled(self) -> bool: 

62 return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL) 1ab

63 

64 EMAIL_TEST_USER: EmailStr = "test@example.com" 

65 FIRST_SUPERUSER: EmailStr 

66 FIRST_SUPERUSER_PASSWORD: str 

67 

68 def _check_default_secret(self, var_name: str, value: str | None) -> None: 

69 if value == "changethis": 

70 message = ( 

71 f'The value of {var_name} is "changethis", ' 

72 "for security, please change it, at least for deployments." 

73 ) 

74 if self.FASTAPI_ENV == "development": 

75 warnings.warn(message, stacklevel=1) 

76 else: 

77 raise ValueError(message) 

78 

79 @model_validator(mode="after") 

80 def _enforce_non_default_secrets(self) -> Self: 

81 self._check_default_secret("SECRET_KEY", self.SECRET_KEY) 

82 for host in self.DATABASE_URL.hosts(): 

83 self._check_default_secret("DATABASE_URL password", host["password"]) 

84 self._check_default_secret( 

85 "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD 

86 ) 

87 

88 return self 

89 

90 

91settings = Settings() # type: ignore # ty: ignore[unused-ignore-comment]