Skip to content

Commit 874c3c0

Browse files
committed
feat(config): add dynamic database_url property with current password and connection params parsing
1 parent 8f6b02b commit 874c3c0

File tree

3 files changed

+121
-9
lines changed

3 files changed

+121
-9
lines changed

.env.example

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Application Settings
22
APP_PROJECT_NAME=python-template
33
# sqlite:// will silently swallow exceptions, better not to use for in-memory database
4-
# APP_DATABASE_URL=sqlite://
4+
# APP_DATABASE_URL_TEMPLATE=sqlite://
55
# sqlite:///:memory: follows normal exception handling rules
6-
APP_DATABASE_URL=sqlite:///:memory:
7-
# APP_DATABASE_URL=sqlite:///./issues.db
8-
# APP_DATABASE_URL=postgresql+psycopg://app_user:change_this_password@localhost:5432/app_database
6+
# APP_DATABASE_URL_TEMPLATE=sqlite:///:memory:
7+
# APP_DATABASE_URL_TEMPLATE=sqlite:///./issues.db
8+
APP_DATABASE_URL_TEMPLATE=postgresql+psycopg://app_user:change_this_password@localhost:5432/app_database
99
APP_DATABASE_SCHEMA=issue_analysis
1010
APP_MIGRATE_DATABASE=false
1111
APP_SQLITE_WAL_MODE=false

config.py

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
from typing import List, Literal
22

3-
from pydantic import AnyHttpUrl
3+
from pydantic import AnyHttpUrl, SecretStr
44
from pydantic_settings import (
55
BaseSettings,
66
SettingsConfigDict,
77
PydanticBaseSettingsSource,
88
)
99
from typing import Type
1010
from functools import lru_cache
11+
from urllib.parse import urlparse, parse_qs, quote_plus
1112

1213

1314
class Settings(BaseSettings):
1415
# Application settings
1516
project_name: str
16-
database_url: str
17+
database_url_template: str
1718
database_schema: str | None = None
1819
migrate_database: bool = False
1920
sqlite_wal_mode: bool = False
2021
database_type: str = "sqlmodel"
2122
current_env: Literal["testing", "default"]
23+
app_db_password: SecretStr
24+
app_db_user: str
2225

2326
# CORS settings
2427
backend_cors_origins: List[str] = ["http://localhost:8000", "http://localhost:3000"]
@@ -44,14 +47,125 @@ class Settings(BaseSettings):
4447
@property
4548
def get_table_schema(self) -> str | None:
4649
"""Return table_schema if database_url does not start with sqlite, otherwise return None."""
47-
if self.database_url.startswith("sqlite"):
50+
if self.database_url_template.startswith("sqlite"):
4851
return None
4952
return self.database_schema
5053

5154
@property
5255
def backend_cors_origins_list(self) -> List[AnyHttpUrl]:
5356
return [AnyHttpUrl(origin) for origin in self.backend_cors_origins]
5457

58+
@property
59+
def database_url(self) -> str:
60+
"""
61+
Dynamically return the database_url with the password from app_db_password.
62+
This ensures the database connection always uses the current password from the settings.
63+
"""
64+
# Get the current password and username from class properties
65+
current_password = (
66+
self.app_db_password.get_secret_value() if self.app_db_password else None
67+
)
68+
current_username = self.app_db_user
69+
70+
# If password not found, return the original URL
71+
if not current_password:
72+
return self.database_url_template
73+
74+
# Handle SQLite connections differently
75+
if self.database_url_template.startswith("sqlite"):
76+
return self.database_url_template
77+
78+
try:
79+
# Split the URL into components
80+
protocol_part, rest = self.database_url_template.split("://", 1)
81+
82+
if "@" in rest:
83+
# Handle URLs with authentication (username:password@host:port/db)
84+
_, server_part = rest.split("@", 1)
85+
86+
# Use the username from settings or fall back to default
87+
username = current_username or "app_user"
88+
89+
# Reconstruct with current username and password, ensuring password is URL-encoded
90+
return f"{protocol_part}://{username}:{quote_plus(current_password)}@{server_part}"
91+
else:
92+
# For URLs without authentication, use username from settings or default
93+
username = current_username or "app_user"
94+
return f"{protocol_part}://{username}:{quote_plus(current_password)}@{rest}"
95+
except Exception as e:
96+
# Log the error but don't crash - return original URL as fallback
97+
print(f"Error generating dynamic database URL: {e}")
98+
return self.database_url_template
99+
100+
@property
101+
def get_db_connection_params(self) -> dict:
102+
"""
103+
Return a dictionary of database connection parameters.
104+
Useful for libraries that accept connection parameters as separate arguments.
105+
"""
106+
# Use the dynamic URL with current password
107+
url = self.database_url_with_current_password
108+
109+
if url.startswith("sqlite"):
110+
# Handle SQLite connections
111+
path = url.replace("sqlite:///", "")
112+
return {
113+
"database": path if path != ":memory:" else ":memory:",
114+
"database_type": "sqlite",
115+
}
116+
117+
try:
118+
# Parse the URL to extract components
119+
parsed = urlparse(url)
120+
121+
# Extract username and password from netloc
122+
userpass, hostport = (
123+
parsed.netloc.split("@", 1)
124+
if "@" in parsed.netloc
125+
else ("", parsed.netloc)
126+
)
127+
username, password = (
128+
userpass.split(":", 1) if ":" in userpass else (userpass, "")
129+
)
130+
131+
# Extract host and port
132+
host, port = hostport.split(":", 1) if ":" in hostport else (hostport, "")
133+
134+
# Extract database name from path
135+
database = parsed.path.lstrip("/")
136+
137+
# Extract query parameters
138+
params = parse_qs(parsed.query)
139+
140+
result = {
141+
"username": username,
142+
"password": password,
143+
"host": host,
144+
"port": int(port) if port.isdigit() else None,
145+
"database": database,
146+
"database_type": parsed.scheme.split("+")[0]
147+
if "+" in parsed.scheme
148+
else parsed.scheme,
149+
"driver": parsed.scheme.split("+")[1] if "+" in parsed.scheme else None,
150+
}
151+
152+
# Add schema if available
153+
if self.database_schema:
154+
result["schema"] = self.database_schema
155+
156+
# Add query parameters
157+
for key, values in params.items():
158+
if len(values) == 1:
159+
result[key] = values[0]
160+
else:
161+
result[key] = values
162+
163+
return result
164+
except Exception as e:
165+
# Log the error but don't crash - return minimal dict as fallback
166+
print(f"Error parsing database URL: {e}")
167+
return {"database_url": url}
168+
55169
@classmethod
56170
def settings_customise_sources(
57171
cls,

docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: "3.8"
2-
31
services:
42
db:
53
image: postgres:17-alpine

0 commit comments

Comments
 (0)