Skip to content

Commit ae168a2

Browse files
committed
Remove duplicated sourcing of settings via environment variables and .env file
1 parent 62a56f1 commit ae168a2

File tree

1 file changed

+97
-75
lines changed

1 file changed

+97
-75
lines changed

src/app/core/config.py

Lines changed: 97 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,160 @@
11
import os
22
from enum import Enum
33

4-
from pydantic import SecretStr
5-
from pydantic_settings import BaseSettings
6-
from starlette.config import Config
4+
from pydantic import SecretStr, computed_field, field_validator
5+
from pydantic_settings import BaseSettings, SettingsConfigDict
76

8-
current_file_dir = os.path.dirname(os.path.realpath(__file__))
9-
env_path = os.path.join(current_file_dir, "..", "..", ".env")
10-
config = Config(env_path)
117

12-
13-
def str_setting_to_list(setting: str) -> list[str]:
14-
if isinstance(setting, str):
15-
return [item.strip() for item in setting.split(",") if item.strip()]
8+
def str_to_list(value: str) -> list[str]:
9+
if isinstance(value, str):
10+
return [item.strip() for item in value.split(",") if item.strip()]
1611
raise ValueError("Invalid string setting for list conversion.")
1712

1813

1914
class AppSettings(BaseSettings):
20-
APP_NAME: str = config("APP_NAME", default="FastAPI app")
21-
APP_DESCRIPTION: str | None = config("APP_DESCRIPTION", default=None)
22-
APP_VERSION: str | None = config("APP_VERSION", default=None)
23-
LICENSE_NAME: str | None = config("LICENSE", default=None)
24-
CONTACT_NAME: str | None = config("CONTACT_NAME", default=None)
25-
CONTACT_EMAIL: str | None = config("CONTACT_EMAIL", default=None)
15+
APP_NAME: str = "FastAPI app"
16+
APP_DESCRIPTION: str | None = None
17+
APP_VERSION: str | None = None
18+
LICENSE_NAME: str | None = None
19+
CONTACT_NAME: str | None = None
20+
CONTACT_EMAIL: str | None = None
2621

2722

2823
class CryptSettings(BaseSettings):
29-
SECRET_KEY: SecretStr = config("SECRET_KEY", cast=SecretStr)
30-
ALGORITHM: str = config("ALGORITHM", default="HS256")
31-
ACCESS_TOKEN_EXPIRE_MINUTES: int = config("ACCESS_TOKEN_EXPIRE_MINUTES", default=30)
32-
REFRESH_TOKEN_EXPIRE_DAYS: int = config("REFRESH_TOKEN_EXPIRE_DAYS", default=7)
24+
SECRET_KEY: SecretStr
25+
ALGORITHM: str = "HS256"
26+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
27+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
3328

3429

3530
class DatabaseSettings(BaseSettings):
3631
pass
3732

3833

3934
class SQLiteSettings(DatabaseSettings):
40-
SQLITE_URI: str = config("SQLITE_URI", default="./sql_app.db")
41-
SQLITE_SYNC_PREFIX: str = config("SQLITE_SYNC_PREFIX", default="sqlite:///")
42-
SQLITE_ASYNC_PREFIX: str = config("SQLITE_ASYNC_PREFIX", default="sqlite+aiosqlite:///")
35+
SQLITE_URI: str = "./sql_app.db"
36+
SQLITE_SYNC_PREFIX: str = "sqlite:///"
37+
SQLITE_ASYNC_PREFIX: str = "sqlite+aiosqlite:///"
4338

4439

4540
class MySQLSettings(DatabaseSettings):
46-
MYSQL_USER: str = config("MYSQL_USER", default="username")
47-
MYSQL_PASSWORD: str = config("MYSQL_PASSWORD", default="password")
48-
MYSQL_SERVER: str = config("MYSQL_SERVER", default="localhost")
49-
MYSQL_PORT: int = config("MYSQL_PORT", default=5432)
50-
MYSQL_DB: str = config("MYSQL_DB", default="dbname")
51-
MYSQL_URI: str = f"{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_SERVER}:{MYSQL_PORT}/{MYSQL_DB}"
52-
MYSQL_SYNC_PREFIX: str = config("MYSQL_SYNC_PREFIX", default="mysql://")
53-
MYSQL_ASYNC_PREFIX: str = config("MYSQL_ASYNC_PREFIX", default="mysql+aiomysql://")
54-
MYSQL_URL: str | None = config("MYSQL_URL", default=None)
41+
MYSQL_USER: str = "username"
42+
MYSQL_PASSWORD: str = "password"
43+
MYSQL_SERVER: str = "localhost"
44+
MYSQL_PORT: int = 5432
45+
MYSQL_DB: str = "dbname"
46+
MYSQL_SYNC_PREFIX: str = "mysql://"
47+
MYSQL_ASYNC_PREFIX: str = "mysql+aiomysql://"
48+
MYSQL_URL: str | None = None
49+
50+
@computed_field
51+
@property
52+
def MYSQL_URI(self) -> str:
53+
credentials = f"{self.MYSQL_USER}:{self.MYSQL_PASSWORD}"
54+
location = f"{self.MYSQL_SERVER}:{self.MYSQL_PORT}/{self.MYSQL_DB}"
55+
return f"{credentials}@{location}"
5556

5657

5758
class PostgresSettings(DatabaseSettings):
58-
POSTGRES_USER: str = config("POSTGRES_USER", default="postgres")
59-
POSTGRES_PASSWORD: str = config("POSTGRES_PASSWORD", default="postgres")
60-
POSTGRES_SERVER: str = config("POSTGRES_SERVER", default="localhost")
61-
POSTGRES_PORT: int = config("POSTGRES_PORT", default=5432)
62-
POSTGRES_DB: str = config("POSTGRES_DB", default="postgres")
63-
POSTGRES_SYNC_PREFIX: str = config("POSTGRES_SYNC_PREFIX", default="postgresql://")
64-
POSTGRES_ASYNC_PREFIX: str = config("POSTGRES_ASYNC_PREFIX", default="postgresql+asyncpg://")
65-
POSTGRES_URI: str = f"{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}"
66-
POSTGRES_URL: str | None = config("POSTGRES_URL", default=None)
59+
POSTGRES_USER: str = "postgres"
60+
POSTGRES_PASSWORD: str = "postgres"
61+
POSTGRES_SERVER: str = "localhost"
62+
POSTGRES_PORT: int = 5432
63+
POSTGRES_DB: str = "postgres"
64+
POSTGRES_SYNC_PREFIX: str = "postgresql://"
65+
POSTGRES_ASYNC_PREFIX: str = "postgresql+asyncpg://"
66+
POSTGRES_URL: str | None = None
67+
68+
@computed_field
69+
@property
70+
def POSTGRES_URI(self) -> str:
71+
credentials = f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
72+
location = f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
73+
return f"{credentials}@{location}"
6774

6875

6976
class FirstUserSettings(BaseSettings):
70-
ADMIN_NAME: str = config("ADMIN_NAME", default="admin")
71-
ADMIN_EMAIL: str = config("ADMIN_EMAIL", default="[email protected]")
72-
ADMIN_USERNAME: str = config("ADMIN_USERNAME", default="admin")
73-
ADMIN_PASSWORD: str = config("ADMIN_PASSWORD", default="!Ch4ng3Th1sP4ssW0rd!")
77+
ADMIN_NAME: str = "admin"
78+
ADMIN_EMAIL: str = "[email protected]"
79+
ADMIN_USERNAME: str = "admin"
80+
ADMIN_PASSWORD: str = "!Ch4ng3Th1sP4ssW0rd!"
7481

7582

7683
class TestSettings(BaseSettings):
7784
...
7885

7986

8087
class RedisCacheSettings(BaseSettings):
81-
REDIS_CACHE_HOST: str = config("REDIS_CACHE_HOST", default="localhost")
82-
REDIS_CACHE_PORT: int = config("REDIS_CACHE_PORT", default=6379)
83-
REDIS_CACHE_URL: str = f"redis://{REDIS_CACHE_HOST}:{REDIS_CACHE_PORT}"
88+
REDIS_CACHE_HOST: str = "localhost"
89+
REDIS_CACHE_PORT: int = 6379
90+
91+
@computed_field
92+
@property
93+
def REDIS_CACHE_URL(self) -> str:
94+
return f"redis://{self.REDIS_CACHE_HOST}:{self.REDIS_CACHE_PORT}"
8495

8596

8697
class ClientSideCacheSettings(BaseSettings):
87-
CLIENT_CACHE_MAX_AGE: int = config("CLIENT_CACHE_MAX_AGE", default=60)
98+
CLIENT_CACHE_MAX_AGE: int = 60
8899

89100

90101
class RedisQueueSettings(BaseSettings):
91-
REDIS_QUEUE_HOST: str = config("REDIS_QUEUE_HOST", default="localhost")
92-
REDIS_QUEUE_PORT: int = config("REDIS_QUEUE_PORT", default=6379)
102+
REDIS_QUEUE_HOST: str = "localhost"
103+
REDIS_QUEUE_PORT: int = 6379
93104

94105

95106
class RedisRateLimiterSettings(BaseSettings):
96-
REDIS_RATE_LIMIT_HOST: str = config("REDIS_RATE_LIMIT_HOST", default="localhost")
97-
REDIS_RATE_LIMIT_PORT: int = config("REDIS_RATE_LIMIT_PORT", default=6379)
98-
REDIS_RATE_LIMIT_URL: str = f"redis://{REDIS_RATE_LIMIT_HOST}:{REDIS_RATE_LIMIT_PORT}"
107+
REDIS_RATE_LIMIT_HOST: str = "localhost"
108+
REDIS_RATE_LIMIT_PORT: int = 6379
109+
110+
@computed_field
111+
@property
112+
def REDIS_RATE_LIMIT_URL(self) -> str:
113+
return f"redis://{self.REDIS_RATE_LIMIT_HOST}:{self.REDIS_RATE_LIMIT_PORT}"
99114

100115

101116
class DefaultRateLimitSettings(BaseSettings):
102-
DEFAULT_RATE_LIMIT_LIMIT: int = config("DEFAULT_RATE_LIMIT_LIMIT", default=10)
103-
DEFAULT_RATE_LIMIT_PERIOD: int = config("DEFAULT_RATE_LIMIT_PERIOD", default=3600)
117+
DEFAULT_RATE_LIMIT_LIMIT: int = 10
118+
DEFAULT_RATE_LIMIT_PERIOD: int = 3600
104119

105120

106121
class CRUDAdminSettings(BaseSettings):
107-
CRUD_ADMIN_ENABLED: bool = config("CRUD_ADMIN_ENABLED", default=True)
108-
CRUD_ADMIN_MOUNT_PATH: str = config("CRUD_ADMIN_MOUNT_PATH", default="/admin")
122+
CRUD_ADMIN_ENABLED: bool = True
123+
CRUD_ADMIN_MOUNT_PATH: str = "/admin"
109124

110125
CRUD_ADMIN_ALLOWED_IPS_LIST: list[str] | None = None
111126
CRUD_ADMIN_ALLOWED_NETWORKS_LIST: list[str] | None = None
112-
CRUD_ADMIN_MAX_SESSIONS: int = config("CRUD_ADMIN_MAX_SESSIONS", default=10)
113-
CRUD_ADMIN_SESSION_TIMEOUT: int = config("CRUD_ADMIN_SESSION_TIMEOUT", default=1440)
114-
SESSION_SECURE_COOKIES: bool = config("SESSION_SECURE_COOKIES", default=True)
127+
CRUD_ADMIN_MAX_SESSIONS: int = 10
128+
CRUD_ADMIN_SESSION_TIMEOUT: int = 1440
129+
SESSION_SECURE_COOKIES: bool = True
115130

116-
CRUD_ADMIN_TRACK_EVENTS: bool = config("CRUD_ADMIN_TRACK_EVENTS", default=True)
117-
CRUD_ADMIN_TRACK_SESSIONS: bool = config("CRUD_ADMIN_TRACK_SESSIONS", default=True)
131+
CRUD_ADMIN_TRACK_EVENTS: bool = True
132+
CRUD_ADMIN_TRACK_SESSIONS: bool = True
118133

119-
CRUD_ADMIN_REDIS_ENABLED: bool = config("CRUD_ADMIN_REDIS_ENABLED", default=False)
120-
CRUD_ADMIN_REDIS_HOST: str = config("CRUD_ADMIN_REDIS_HOST", default="localhost")
121-
CRUD_ADMIN_REDIS_PORT: int = config("CRUD_ADMIN_REDIS_PORT", default=6379)
122-
CRUD_ADMIN_REDIS_DB: int = config("CRUD_ADMIN_REDIS_DB", default=0)
123-
CRUD_ADMIN_REDIS_PASSWORD: str | None = config("CRUD_ADMIN_REDIS_PASSWORD", default="None")
124-
CRUD_ADMIN_REDIS_SSL: bool = config("CRUD_ADMIN_REDIS_SSL", default=False)
134+
CRUD_ADMIN_REDIS_ENABLED: bool = False
135+
CRUD_ADMIN_REDIS_HOST: str = "localhost"
136+
CRUD_ADMIN_REDIS_PORT: int = 6379
137+
CRUD_ADMIN_REDIS_DB: int = 0
138+
CRUD_ADMIN_REDIS_PASSWORD: str | None = "None"
139+
CRUD_ADMIN_REDIS_SSL: bool = False
125140

126141

127-
class EnvironmentOption(Enum):
142+
class EnvironmentOption(str, Enum):
128143
LOCAL = "local"
129144
STAGING = "staging"
130145
PRODUCTION = "production"
131146

132147

133148
class EnvironmentSettings(BaseSettings):
134-
ENVIRONMENT: EnvironmentOption = config("ENVIRONMENT", default=EnvironmentOption.LOCAL)
149+
ENVIRONMENT: EnvironmentOption = EnvironmentOption.LOCAL
135150

136151

137152
class CORSSettings(BaseSettings):
138-
CORS_ORIGINS: list[str] = config("CORS_ORIGINS", cast=str_setting_to_list, default="*")
139-
CORS_METHODS: list[str] = config("CORS_METHODS", cast=str_setting_to_list, default="*")
140-
CORS_HEADERS: list[str] = config("CORS_HEADERS", cast=str_setting_to_list, default="*")
153+
CORS_ORIGINS: list[str] | str = "*"
154+
CORS_METHODS: list[str] | str = "*"
155+
CORS_HEADERS: list[str] | str = "*"
156+
157+
_normalize_to_list = field_validator("CORS_ORIGINS", "CORS_METHODS", "CORS_HEADERS", mode="before")(str_to_list)
141158

142159

143160
class Settings(
@@ -156,7 +173,12 @@ class Settings(
156173
EnvironmentSettings,
157174
CORSSettings,
158175
):
159-
pass
176+
model_config = SettingsConfigDict(
177+
env_file=os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "..", ".env"),
178+
env_file_encoding="utf-8",
179+
case_sensitive=True,
180+
extra="ignore",
181+
)
160182

161183

162184
settings = Settings()

0 commit comments

Comments
 (0)