11from typing import List , Literal
22
3- from pydantic import AnyHttpUrl
3+ from pydantic import AnyHttpUrl , SecretStr
44from pydantic_settings import (
55 BaseSettings ,
66 SettingsConfigDict ,
77 PydanticBaseSettingsSource ,
88)
99from typing import Type
1010from functools import lru_cache
11+ from urllib .parse import urlparse , parse_qs , quote_plus
1112
1213
1314class 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 ,
0 commit comments