-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
131 lines (109 loc) · 3.98 KB
/
config.py
File metadata and controls
131 lines (109 loc) · 3.98 KB
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""Application configuration settings."""
import secrets
import warnings
from typing import Annotated, Literal, Self
from pydantic import (
AnyUrl,
BeforeValidator,
EmailStr,
PostgresDsn,
computed_field,
model_validator,
)
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.constants import TOKEN_LENGTH
def parse_cors(cors_value: str | list[str]) -> list[str] | str:
"""Parse CORS configuration from string or list."""
if isinstance(cors_value, str) and not cors_value.startswith("["):
return [cors_item.strip() for cors_item in cors_value.split(",")]
if isinstance(cors_value, (list, str)):
return cors_value
raise ValueError(cors_value)
class Settings(BaseSettings): # type: ignore[explicit-any]
"""Application settings configuration."""
# Configuration
model_config = SettingsConfigDict(
# Use top level .env file (one level above ./backend/)
env_file="../.env",
env_ignore_empty=True,
extra="ignore",
)
# API Settings
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = secrets.token_urlsafe(TOKEN_LENGTH)
ACCESS_TOKEN_EXPIRE_MINUTES: int = (
60 * 24 * 8
) # 60 minutes * 24 hours * 8 days = 8 days
FRONTEND_HOST: str = "http://localhost:5173"
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
# CORS Settings
BACKEND_CORS_ORIGINS: Annotated[
list[AnyUrl] | str,
BeforeValidator(parse_cors),
] = []
# Project Settings
PROJECT_NAME: str
# Database Settings
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str = ""
POSTGRES_DB: str = ""
# Email Settings
SMTP_TLS: bool = True
SMTP_SSL: bool = False
SMTP_PORT: int = 587
SMTP_HOST: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
EMAILS_FROM_EMAIL: EmailStr | None = None
EMAILS_FROM_NAME: EmailStr | None = None
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
# Test Settings
EMAIL_TEST_USER: EmailStr = "test@example.com"
FIRST_SUPERUSER: EmailStr
FIRST_SUPERUSER_PASSWORD: str
@computed_field # type: ignore[prop-decorator]
@property
def all_cors_origins(self) -> list[str]:
"""Get all CORS origins."""
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
self.FRONTEND_HOST,
]
@computed_field # type: ignore[prop-decorator]
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: # noqa: N802
"""Build database URI from configuration."""
return PostgresDsn.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
@computed_field # type: ignore[prop-decorator]
@property
def emails_enabled(self) -> bool:
"""Check if email configuration is enabled."""
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
def _check_default_secret(self, var_name: str, secret_value: str | None) -> None:
if secret_value == "changethis": # noqa: S105
message = (
f'The value of {var_name} is "changethis", '
"for security, please change it, at least for deployments."
)
if self.ENVIRONMENT == "local":
warnings.warn(message, stacklevel=1)
else:
raise ValueError(message)
@model_validator(mode="after")
def _enforce_non_default_secrets(self) -> Self:
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
self._check_default_secret(
"FIRST_SUPERUSER_PASSWORD",
self.FIRST_SUPERUSER_PASSWORD,
)
return self
settings = Settings() # type: ignore[call-arg]