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
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-26 10:48 +0000
1import warnings
2from typing import Literal, Self
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
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
29 PROJECT_NAME: str
30 SENTRY_DSN: HttpUrl | None = None
31 DATABASE_URL: PostgresDsn
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
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
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
57 EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
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
64 EMAIL_TEST_USER: EmailStr = "test@example.com"
65 FIRST_SUPERUSER: EmailStr
66 FIRST_SUPERUSER_PASSWORD: str
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)
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 )
88 return self
91settings = Settings() # type: ignore # ty: ignore[unused-ignore-comment]