Skip to content

Commit 4369524

Browse files
committed
♻️ Refactor and update config.py
1 parent 3acd1bd commit 4369524

File tree

3 files changed

+53
-58
lines changed

3 files changed

+53
-58
lines changed

backend/app/api/routes/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
6060
)
6161

6262
user = crud.create_user(session=session, user_create=user_in)
63-
if settings.EMAILS_ENABLED and user_in.email:
63+
if settings.emails_enabled and user_in.email:
6464
email_data = generate_new_account_email(
6565
email_to=user_in.email, username=user_in.email, password=user_in.password
6666
)

backend/app/core/config.py

Lines changed: 48 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,92 @@
11
import secrets
2-
from typing import Any
2+
from typing import Annotated, Any, Literal
33

44
from pydantic import (
5-
AnyHttpUrl,
5+
AnyUrl,
6+
BeforeValidator,
67
HttpUrl,
78
PostgresDsn,
8-
ValidationInfo,
9-
field_validator,
9+
computed_field,
10+
model_validator,
1011
)
12+
from pydantic_core import MultiHostUrl
1113
from pydantic_settings import BaseSettings, SettingsConfigDict
14+
from typing_extensions import Self
15+
16+
17+
def parse_cors(v: Any) -> list[str] | str:
18+
if isinstance(v, str) and not v.startswith("["):
19+
return [i.strip() for i in v.split(",")]
20+
elif isinstance(v, list | str):
21+
return v
22+
raise ValueError(v)
1223

1324

1425
class Settings(BaseSettings):
26+
model_config = SettingsConfigDict(env_file=".env", env_ignore_empty=True)
1527
API_V1_STR: str = "/api/v1"
1628
SECRET_KEY: str = secrets.token_urlsafe(32)
1729
# 60 minutes * 24 hours * 8 days = 8 days
1830
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
19-
SERVER_HOST: AnyHttpUrl
20-
BACKEND_CORS_ORIGINS: list[AnyHttpUrl] | str = []
21-
22-
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
23-
@classmethod
24-
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
25-
if isinstance(v, str) and not v.startswith("["):
26-
return [i.strip() for i in v.split(",")]
27-
elif isinstance(v, list | str):
28-
return v
29-
raise ValueError(v)
31+
DOMAIN: str = "localhost"
32+
HOST_SCHEME: Literal["https", "http"] = "http"
3033

31-
PROJECT_NAME: str
32-
SENTRY_DSN: HttpUrl | None = None
34+
@computed_field # type: ignore[misc]
35+
@property
36+
def server_host(self) -> str:
37+
return f"{self.HOST_SCHEME}://{self.DOMAIN}"
3338

34-
@field_validator("SENTRY_DSN", mode="before")
35-
@classmethod
36-
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
37-
if not v:
38-
return None
39-
return v
39+
BACKEND_CORS_ORIGINS: Annotated[
40+
list[AnyUrl] | str, BeforeValidator(parse_cors)
41+
] = []
4042

43+
PROJECT_NAME: str
44+
SENTRY_DSN: HttpUrl | None = None
4145
POSTGRES_SERVER: str
4246
POSTGRES_USER: str
4347
POSTGRES_PASSWORD: str
44-
POSTGRES_DB: str
45-
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None
46-
47-
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
48-
def assemble_db_connection(cls, v: str | None, info: ValidationInfo) -> Any:
49-
if isinstance(v, str):
50-
return v
51-
return PostgresDsn.build(
48+
POSTGRES_DB: str = ""
49+
50+
@computed_field # type: ignore[misc]
51+
@property
52+
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
53+
return MultiHostUrl.build(
5254
scheme="postgresql+psycopg",
53-
username=info.data.get("POSTGRES_USER"),
54-
password=info.data.get("POSTGRES_PASSWORD"),
55-
host=info.data.get("POSTGRES_SERVER"),
56-
path=f"{info.data.get('POSTGRES_DB') or ''}",
55+
username=self.POSTGRES_USER,
56+
password=self.POSTGRES_PASSWORD,
57+
host=self.POSTGRES_SERVER,
58+
path=self.POSTGRES_DB,
5759
)
5860

5961
SMTP_TLS: bool = True
60-
SMTP_PORT: int | None = None
62+
SMTP_PORT: int = 587
6163
SMTP_HOST: str | None = None
6264
SMTP_USER: str | None = None
6365
SMTP_PASSWORD: str | None = None
6466
# TODO: update type to EmailStr when sqlmodel supports it
6567
EMAILS_FROM_EMAIL: str | None = None
6668
EMAILS_FROM_NAME: str | None = None
6769

68-
@field_validator("EMAILS_FROM_NAME")
69-
def get_project_name(cls, v: str | None, info: ValidationInfo) -> str:
70-
if not v:
71-
return str(info.data["PROJECT_NAME"])
72-
return v
70+
@model_validator(mode="after")
71+
def set_default_emails_from(self) -> Self:
72+
if not self.EMAILS_FROM_NAME:
73+
self.EMAILS_FROM_NAME = self.PROJECT_NAME
74+
return self
7375

7476
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
7577
EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
76-
EMAILS_ENABLED: bool = False
77-
78-
@field_validator("EMAILS_ENABLED", mode="before")
79-
def get_emails_enabled(cls, v: bool, info: ValidationInfo) -> bool:
80-
return bool(
81-
info.data.get("SMTP_HOST")
82-
and info.data.get("SMTP_PORT")
83-
and info.data.get("EMAILS_FROM_EMAIL")
84-
)
78+
79+
@computed_field # type: ignore[misc]
80+
@property
81+
def emails_enabled(self) -> bool:
82+
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
8583

8684
# TODO: update type to EmailStr when sqlmodel supports it
8785
EMAIL_TEST_USER: str = "test@example.com"
8886
# TODO: update type to EmailStr when sqlmodel supports it
8987
FIRST_SUPERUSER: str
9088
FIRST_SUPERUSER_PASSWORD: str
9189
USERS_OPEN_REGISTRATION: bool = False
92-
model_config = SettingsConfigDict(env_file=".env")
9390

9491

9592
settings = Settings() # type: ignore

backend/app/utils.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class EmailData:
1717
subject: str
1818

1919

20-
def render_email_template(*, template_name: str, context: dict[str, Any]):
20+
def render_email_template(*, template_name: str, context: dict[str, Any]) -> str:
2121
template_str = (Path(settings.EMAIL_TEMPLATES_DIR) / template_name).read_text()
2222
html_content = Template(template_str).render(context)
2323
return html_content
@@ -29,7 +29,7 @@ def send_email(
2929
subject: str = "",
3030
html_content: str = "",
3131
) -> None:
32-
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
32+
assert settings.emails_enabled, "no provided configuration for email variables"
3333
message = emails.Message(
3434
subject=subject,
3535
html=html_content,
@@ -59,8 +59,7 @@ def generate_test_email(email_to: str) -> EmailData:
5959
def generate_reset_password_email(email_to: str, email: str, token: str) -> EmailData:
6060
project_name = settings.PROJECT_NAME
6161
subject = f"{project_name} - Password recovery for user {email}"
62-
server_host = settings.SERVER_HOST
63-
link = f"{server_host}/reset-password?token={token}"
62+
link = f"{settings.server_host}/reset-password?token={token}"
6463
html_content = render_email_template(
6564
template_name="reset_password.html",
6665
context={
@@ -79,15 +78,14 @@ def generate_new_account_email(
7978
) -> EmailData:
8079
project_name = settings.PROJECT_NAME
8180
subject = f"{project_name} - New account for user {username}"
82-
link = settings.SERVER_HOST
8381
html_content = render_email_template(
8482
template_name="new_account.html",
8583
context={
8684
"project_name": settings.PROJECT_NAME,
8785
"username": username,
8886
"password": password,
8987
"email": email_to,
90-
"link": link,
88+
"link": settings.server_host,
9189
},
9290
)
9391
return EmailData(html_content=html_content, subject=subject)

0 commit comments

Comments
 (0)