trovesuite 1.0.1__py3-none-any.whl → 1.0.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,65 @@
1
1
  """
2
2
  Database configuration and connection management
3
+
4
+ FIXED VERSION - Removed pool reinitialization during runtime to prevent "unkeyed connection" errors.
5
+ Key changes:
6
+ - Pool created once at startup, never recreated during runtime
7
+ - Removed connection validation queries that consume pool connections
8
+ - Increased default pool size to 2 (safe for Azure Basic tier)
9
+ - Simplified get_db_connection() - let pool.getconn() block naturally
10
+ - Removed _recover_connection_pool() and _is_pool_valid() runtime checks
11
+
12
+ NOTE: Database initialization is NOT automatic for package version.
13
+ You must call initialize_database() explicitly in your application startup.
14
+ Example in FastAPI:
15
+ @app.on_event("startup")
16
+ async def startup_event():
17
+ initialize_database()
3
18
  """
4
19
  from contextlib import contextmanager
5
20
  from typing import Generator, Optional
6
21
  import psycopg2
7
22
  import psycopg2.pool
8
23
  from psycopg2.extras import RealDictCursor
24
+ import threading
9
25
  from .settings import db_settings
10
26
  from .logging import get_logger
11
27
 
12
28
  logger = get_logger("database")
13
29
 
14
- # Database connection pool
30
+ # Database connection pool - created once at startup, never replaced during runtime
15
31
  _connection_pool: Optional[psycopg2.pool.ThreadedConnectionPool] = None
16
- _sqlmodel_engine = None
32
+ _initialization_lock = threading.Lock()
17
33
 
18
34
 
19
35
  class DatabaseConfig:
20
36
  """Database configuration and connection management"""
21
-
37
+
22
38
  def __init__(self):
39
+ import os
23
40
  self.settings = db_settings
24
- self.pool_size = 5
25
- self.max_overflow = 10
26
-
27
- @property
28
- def database_url(self):
29
- """Get database URL (lazy evaluation)"""
30
- return self.settings.database_url
31
-
41
+ self.database_url = self.settings.database_url
42
+ # Safe pool size for Azure Basic tier (B_Standard_B1ms)
43
+ # With 2 workers and pool_size=2: 2 × 2 = 4 connections (very safe)
44
+ # With 4 workers and pool_size=2: 4 × 2 = 8 connections (still safe, well under 50 limit)
45
+ # Can be overridden with DB_POOL_SIZE environment variable
46
+ self.pool_size = int(os.getenv("DB_POOL_SIZE", "2"))
47
+
32
48
  def get_connection_params(self) -> dict:
33
49
  """Get database connection parameters"""
50
+ if self.settings.DATABASE_URL:
51
+ # Use full DATABASE_URL if available
52
+ return {
53
+ "dsn": self.settings.DATABASE_URL,
54
+ "cursor_factory": RealDictCursor,
55
+ "keepalives": 1,
56
+ "keepalives_idle": 30,
57
+ "keepalives_interval": 10,
58
+ "keepalives_count": 5,
59
+ "connect_timeout": 30 # Increased timeout for Azure PostgreSQL
60
+ }
61
+
62
+ # fallback to individual DB_* variables
34
63
  return {
35
64
  "host": self.settings.DB_HOST,
36
65
  "port": self.settings.DB_PORT,
@@ -38,25 +67,54 @@ class DatabaseConfig:
38
67
  "user": self.settings.DB_USER,
39
68
  "password": self.settings.DB_PASSWORD,
40
69
  "cursor_factory": RealDictCursor,
41
- "application_name": f"{self.settings.APP_NAME}_{self.settings.ENVIRONMENT}"
70
+ "application_name": f"{self.settings.APP_NAME}_{self.settings.ENVIRONMENT}",
71
+ "keepalives": 1,
72
+ "keepalives_idle": 30,
73
+ "keepalives_interval": 10,
74
+ "keepalives_count": 5,
75
+ "connect_timeout": 10
42
76
  }
43
-
77
+
44
78
  def create_connection_pool(self) -> psycopg2.pool.ThreadedConnectionPool:
45
79
  """Create a connection pool for psycopg2"""
46
80
  try:
81
+ import os
82
+ dsn = os.getenv("DATABASE_URL", "") or str(self.database_url or "")
83
+ is_azure = "database.azure.com" in dsn.lower()
84
+ pool_size = self.pool_size
85
+
47
86
  pool = psycopg2.pool.ThreadedConnectionPool(
48
87
  minconn=1,
49
- maxconn=self.pool_size,
88
+ maxconn=pool_size,
50
89
  **self.get_connection_params()
51
90
  )
52
- logger.info(f"Database connection pool created with {self.pool_size} connections")
91
+ logger.info(
92
+ f"Database connection pool created with {pool_size} connections "
93
+ f"(Azure: {is_azure}, DB_POOL_SIZE: {self.pool_size})"
94
+ )
53
95
  return pool
96
+ except psycopg2.OperationalError as e:
97
+ error_str = str(e).lower()
98
+ if any(keyword in error_str for keyword in ["connection", "slot", "limit", "exhausted", "too many"]):
99
+ logger.error("⚠️ Database connection limit reached!")
100
+ logger.error(" Possible causes:")
101
+ logger.error(" 1. Too many connections from multiple replicas/workers")
102
+ logger.error(" 2. Pool size too high (DB_POOL_SIZE environment variable)")
103
+ logger.error(" 3. Too many Gunicorn workers (GUNICORN_WORKERS environment variable)")
104
+ logger.error(" 4. Connections not being properly returned to pool")
105
+ logger.error(" Solutions:")
106
+ logger.error(" - Set DB_POOL_SIZE=2 (current default)")
107
+ logger.error(" - Reduce GUNICORN_WORKERS (default: 4)")
108
+ logger.error(" - Consider using PgBouncer for connection pooling")
109
+ logger.error(" - Upgrade to a higher PostgreSQL tier if needed")
110
+ logger.error(f"Failed to create database connection pool: {str(e)}")
111
+ raise
54
112
  except Exception as e:
55
113
  logger.error(f"Failed to create database connection pool: {str(e)}")
56
114
  raise
57
-
115
+
58
116
  def test_connection(self) -> bool:
59
- """Test database connection"""
117
+ """Test database connection (only used at startup)"""
60
118
  try:
61
119
  with psycopg2.connect(**self.get_connection_params()) as conn:
62
120
  with conn.cursor() as cursor:
@@ -76,93 +134,147 @@ db_config = DatabaseConfig()
76
134
 
77
135
 
78
136
  def initialize_database():
79
- """Initialize database connections and pool"""
137
+ """
138
+ Initialize database connections and pool.
139
+ This should ONLY be called at application startup.
140
+ Pool is created once and never recreated during runtime.
141
+
142
+ NOTE: For package version, this must be called explicitly in application startup.
143
+ """
80
144
  global _connection_pool
81
145
 
82
- try:
83
- # Test connection first
84
- if not db_config.test_connection():
85
- raise Exception("Database connection test failed")
86
-
87
- # Create connection pool
88
- _connection_pool = db_config.create_connection_pool()
89
-
90
- logger.info("Database initialization completed successfully")
146
+ with _initialization_lock:
147
+ # If pool already exists, don't recreate it
148
+ if _connection_pool is not None:
149
+ logger.warning("Database pool already initialized, skipping reinitialization")
150
+ return
91
151
 
92
- except Exception as e:
93
- logger.error(f"Database initialization failed: {str(e)}")
94
- raise
152
+ try:
153
+ # Test connection first (only at startup)
154
+ if not db_config.test_connection():
155
+ raise Exception("Database connection test failed")
156
+
157
+ # Create connection pool (only once at startup)
158
+ _connection_pool = db_config.create_connection_pool()
159
+
160
+ # Verify pool was created successfully
161
+ if _connection_pool is None:
162
+ raise Exception("Connection pool creation returned None")
163
+
164
+ logger.info("✅ Database initialization completed successfully")
165
+
166
+ except Exception as e:
167
+ logger.error(f"❌ Database initialization failed: {str(e)}")
168
+ _connection_pool = None # Ensure pool is None on failure
169
+ raise
95
170
 
96
171
 
97
172
  def get_connection_pool() -> psycopg2.pool.ThreadedConnectionPool:
98
- """Get the database connection pool"""
173
+ """
174
+ Get the database connection pool.
175
+ Pool must be initialized at startup. This function will raise if pool is None.
176
+ """
99
177
  global _connection_pool
178
+
100
179
  if _connection_pool is None:
101
- raise Exception("Database not initialized. Call initialize_database() first.")
180
+ error_msg = (
181
+ "Database connection pool is not initialized. "
182
+ "Please ensure initialize_database() was called at application startup."
183
+ )
184
+ logger.error(error_msg)
185
+ raise Exception(error_msg)
186
+
102
187
  return _connection_pool
103
188
 
104
189
 
105
- def get_sqlmodel_engine():
106
- """Get the SQLModel engine"""
107
- global _sqlmodel_engine
108
- if _sqlmodel_engine is None:
109
- raise Exception("Database not initialized. Call initialize_database() first.")
110
- return _sqlmodel_engine
111
-
112
-
113
190
  @contextmanager
114
191
  def get_db_connection():
115
- """Get a database connection from the pool (context manager)"""
192
+ """
193
+ Get a database connection from the pool (context manager).
194
+
195
+ This is simplified - we let pool.getconn() block naturally.
196
+ No retries, no validation queries, no pool recovery.
197
+ """
116
198
  pool = get_connection_pool()
117
199
  conn = None
118
200
  try:
201
+ # Get connection from pool - this will block if pool is exhausted
202
+ # That's the correct behavior - let backpressure happen naturally
119
203
  conn = pool.getconn()
120
204
  logger.debug("Database connection acquired from pool")
121
205
  yield conn
122
206
  except Exception as e:
123
- logger.error(f"Database connection error: {str(e)}")
124
- if conn:
125
- conn.rollback()
207
+ # If connection exists and isn't closed, rollback transaction
208
+ if conn and not conn.closed:
209
+ try:
210
+ conn.rollback()
211
+ except Exception as rollback_error:
212
+ logger.warning(f"Could not rollback transaction: {str(rollback_error)}")
213
+ # Re-raise the exception - don't retry here
126
214
  raise
127
215
  finally:
216
+ # Always return connection to pool
128
217
  if conn:
129
- pool.putconn(conn)
130
- logger.debug("Database connection returned to pool")
218
+ try:
219
+ if conn.closed:
220
+ # If connection is closed, tell pool to close it instead of returning
221
+ pool.putconn(conn, close=True)
222
+ else:
223
+ # Return connection to pool normally
224
+ pool.putconn(conn)
225
+ logger.debug("Database connection returned to pool")
226
+ except Exception as put_error:
227
+ # Log error but don't fail - connection will be cleaned up by pool
228
+ logger.error(f"Error returning connection to pool: {str(put_error)}")
229
+ # Try to close connection manually as last resort
230
+ try:
231
+ if not conn.closed:
232
+ conn.close()
233
+ except Exception:
234
+ pass
131
235
 
132
236
 
133
237
  @contextmanager
134
238
  def get_db_cursor():
135
239
  """Get a database cursor (context manager)"""
136
240
  with get_db_connection() as conn:
137
- cursor = conn.cursor()
241
+ cursor = conn.cursor(cursor_factory=RealDictCursor)
138
242
  try:
139
243
  yield cursor
140
- conn.commit()
244
+ if not conn.closed:
245
+ conn.commit()
141
246
  except Exception as e:
142
- conn.rollback()
247
+ if not conn.closed:
248
+ try:
249
+ conn.rollback()
250
+ except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
251
+ logger.warning(f"Could not rollback transaction on closed connection: {str(rollback_error)}")
143
252
  logger.error(f"Database cursor error: {str(e)}")
144
253
  raise
145
254
  finally:
146
- cursor.close()
255
+ try:
256
+ cursor.close()
257
+ except Exception as close_error:
258
+ logger.warning(f"Error closing cursor: {str(close_error)}")
147
259
 
148
260
 
149
261
  class DatabaseManager:
150
262
  """Database manager for common operations"""
151
-
263
+
152
264
  @staticmethod
153
265
  def execute_query(query: str, params: tuple = None) -> list:
154
266
  """Execute a SELECT query and return results"""
155
267
  with get_db_cursor() as cursor:
156
268
  cursor.execute(query, params)
157
269
  return cursor.fetchall()
158
-
270
+
159
271
  @staticmethod
160
272
  def execute_update(query: str, params: tuple = None) -> int:
161
273
  """Execute an INSERT/UPDATE/DELETE query and return affected rows"""
162
274
  with get_db_cursor() as cursor:
163
275
  cursor.execute(query, params)
164
276
  return cursor.rowcount
165
-
277
+
166
278
  @staticmethod
167
279
  def execute_scalar(query: str, params: tuple = None):
168
280
  """Execute a query and return a single value"""
@@ -178,15 +290,53 @@ class DatabaseManager:
178
290
  # Handle tuple result
179
291
  return result[0] if len(result) > 0 else None
180
292
  return None
181
-
293
+
294
+ @staticmethod
295
+ @contextmanager
296
+ def transaction():
297
+ """
298
+ Context manager for database transactions.
299
+ Wraps multiple operations in a single transaction.
300
+
301
+ Usage:
302
+ with DatabaseManager.transaction() as cursor:
303
+ cursor.execute("INSERT INTO table1 ...")
304
+ cursor.execute("INSERT INTO table2 ...")
305
+ # Auto-commits on success, auto-rollbacks on exception
306
+ """
307
+ # Use get_db_connection() instead of directly accessing pool
308
+ with get_db_connection() as conn:
309
+ cursor = conn.cursor(cursor_factory=RealDictCursor)
310
+ try:
311
+ yield cursor
312
+ if not conn.closed:
313
+ conn.commit()
314
+ logger.debug("Transaction committed successfully")
315
+ except Exception as e:
316
+ if not conn.closed:
317
+ try:
318
+ conn.rollback()
319
+ logger.warning(f"Transaction rolled back due to error: {str(e)}")
320
+ except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
321
+ logger.error(f"Could not rollback transaction: {str(rollback_error)}")
322
+ raise
323
+ finally:
324
+ try:
325
+ cursor.close()
326
+ except Exception as close_error:
327
+ logger.warning(f"Error closing transaction cursor: {str(close_error)}")
328
+
182
329
  @staticmethod
183
330
  def health_check() -> dict:
184
- """Perform database health check"""
331
+ """
332
+ Perform database health check.
333
+ Health checks are allowed to fail - they don't attempt to repair the pool.
334
+ """
185
335
  try:
186
336
  with get_db_cursor() as cursor:
187
337
  cursor.execute("SELECT version(), current_database(), current_user")
188
338
  result = cursor.fetchone()
189
-
339
+
190
340
  if result:
191
341
  # Handle RealDictRow (dictionary-like) result
192
342
  if hasattr(result, 'get'):
@@ -217,5 +367,9 @@ class DatabaseManager:
217
367
  }
218
368
 
219
369
 
220
- # Database initialization is done lazily when needed
221
- # Call initialize_database() explicitly when you need to use the database
370
+ # NOTE: Database initialization is NOT automatic for package version
371
+ # You must call initialize_database() explicitly in your application startup
372
+ # Example in FastAPI:
373
+ # @app.on_event("startup")
374
+ # async def startup_event():
375
+ # initialize_database()
@@ -1,153 +1,96 @@
1
1
  import os
2
- import warnings
3
- from typing import Optional
4
-
5
2
  class Settings:
6
- """Settings configuration for TroveSuite Auth Service"""
7
-
8
- # =============================================================================
9
- # DATABASE CONFIGURATION
10
- # =============================================================================
11
- DATABASE_URL: str = os.getenv(
12
- "DATABASE_URL",
13
- "postgresql://username:password@localhost:5432/database_name"
14
- )
15
3
 
16
- # Alternative database configuration
17
- DB_USER: Optional[str] = os.getenv("DB_USER")
18
- DB_HOST: Optional[str] = os.getenv("DB_HOST")
19
- DB_NAME: Optional[str] = os.getenv("DB_NAME")
20
- DB_PORT: int = int(os.getenv("DB_PORT", "5432"))
21
- DB_PASSWORD: Optional[str] = os.getenv("DB_PASSWORD")
22
- ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development")
4
+ # Database URL
5
+ DATABASE_URL: str = os.getenv("DATABASE_URL")
23
6
 
24
- # =============================================================================
25
- # APPLICATION SETTINGS
26
- # =============================================================================
27
- APP_NAME: str = os.getenv("APP_NAME", "TroveSuite Auth Service")
28
- DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
7
+ DB_USER: str = os.getenv("DB_USER")
8
+ DB_HOST: str = os.getenv("DB_HOST")
9
+ DB_NAME: str = os.getenv("DB_NAME")
10
+ DB_PORT: str = os.getenv("DB_PORT")
11
+ DB_PASSWORD: str = os.getenv("DB_PASSWORD")
29
12
 
30
- # =============================================================================
31
- # SECURITY SETTINGS
32
- # =============================================================================
33
- ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
34
- SECRET_KEY: str = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
35
- ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
13
+ # Application settings
14
+ DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
15
+ APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
16
+ APP_VERSION: str = os.getenv("APP_VERSION", "1.0.0")
36
17
 
37
- # =============================================================================
38
- # LOGGING SETTINGS
39
- # =============================================================================
18
+ # Logging settings
40
19
  LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
41
20
  LOG_FORMAT: str = os.getenv("LOG_FORMAT", "detailed") # detailed, json, simple
42
- LOG_TO_FILE: bool = os.getenv("LOG_TO_FILE", "False").lower() == "false"
21
+ LOG_TO_FILE: bool = os.getenv("LOG_TO_FILE", "False").lower() in ("true", 1)
43
22
  LOG_MAX_SIZE: int = int(os.getenv("LOG_MAX_SIZE", "10485760")) # 10MB
44
23
  LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "5"))
45
24
  LOG_DIR: str = os.getenv("LOG_DIR", "logs")
46
-
25
+
26
+ # Security settings
27
+ ALGORITHM: str = os.getenv("ALGORITHM")
28
+ SECRET_KEY: str = os.getenv("SECRET_KEY")
29
+ ENVIRONMENT: str = os.getenv("ENVIRONMENT")
30
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
31
+
47
32
  # =============================================================================
48
- # DATABASE TABLE NAMES
33
+ # SHARED TABLES (core_platform schema)
49
34
  # =============================================================================
50
- # Main schema tables
51
- MAIN_TENANTS_TABLE: str = os.getenv("MAIN_TENANTS_TABLE", "tenants")
52
- ROLE_PERMISSIONS_TABLE: str = os.getenv("ROLE_PERMISSIONS_TABLE", "role_permissions")
53
-
54
- # Tenant-specific tables (used in queries with tenant schema)
55
- TENANT_LOGIN_SETTINGS_TABLE: str = os.getenv("TENANT_LOGIN_SETTINGS_TABLE", "login_settings")
56
- USER_GROUPS_TABLE: str = os.getenv("USER_GROUPS_TABLE", "user_groups")
57
- ASSIGN_ROLES_TABLE: str = os.getenv("ASSIGN_ROLES_TABLE", "assign_roles")
35
+ CORE_PLATFORM_TENANTS_TABLE = os.getenv("CORE_PLATFORM_TENANTS_TABLE", "core_platform.cp_tenants")
36
+ CORE_PLATFORM_SUBSCRIPTIONS_TABLE = os.getenv("CORE_PLATFORM_SUBSCRIPTIONS_TABLE", "core_platform.cp_subscriptions")
37
+ CORE_PLATFORM_APPS_TABLE = os.getenv("CORE_PLATFORM_APPS_TABLE", "core_platform.cp_apps")
38
+ CORE_PLATFORM_USERS_TABLE = os.getenv("CORE_PLATFORM_USERS_TABLE", "core_platform.cp_users")
39
+ CORE_PLATFORM_RESOURCE_TYPES_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_TYPES_TABLE", "core_platform.cp_resource_types")
40
+ CORE_PLATFORM_RESOURCE_ID_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_ID_TABLE", "core_platform.cp_resource_ids")
41
+ CORE_PLATFORM_PERMISSIONS_TABLE = os.getenv("CORE_PLATFORM_PERMISSIONS_TABLE", "core_platform.cp_permissions")
42
+ CORE_PLATFORM_ROLES_TABLE = os.getenv("CORE_PLATFORM_ROLES_TABLE", "core_platform.cp_roles")
43
+ CORE_PLATFORM_ROLE_PERMISSIONS_TABLE = os.getenv("CORE_PLATFORM_ROLE_PERMISSIONS_TABLE", "core_platform.cp_role_permissions")
44
+ CORE_PLATFORM_USER_SUBSCRIPTIONS_TABLE = os.getenv("CORE_PLATFORM_USER_SUBSCRIPTIONS_TABLE", "core_platform.cp_user_subscriptions")
45
+ CORE_PLATFORM_USER_SUBSCRIPTION_HISTORY_TABLE = os.getenv("CORE_PLATFORM_USER_SUBSCRIPTION_HISTORY_TABLE", "core_platform.cp_user_subscription_histories")
46
+ CORE_PLATFORM_OTP = os.getenv("CORE_PLATFORM_OTP", "core_platform.cp_otps")
47
+ CORE_PLATFORM_PASSWORD_POLICY = os.getenv("CORE_PLATFORM_PASSWORD_POLICY", "core_platform.cp_password_policies")
48
+ CORE_PLATFORM_MULTI_FACTOR_SETTINGS = os.getenv("CORE_PLATFORM_MULTI_FACTOR_SETTINGS", "core_platform.cp_multi_factor_settings")
49
+ CORE_PLATFORM_USER_LOGIN_TRACKING = os.getenv("CORE_PLATFORM_USER_LOGIN_TRACKING", "core_platform.cp_user_login_tracking")
50
+ CORE_PLATFORM_ENTERPRISE_SUBSCRIPTIONS_TABLE = os.getenv("CORE_PLATFORM_ENTERPRISE_SUBSCRIPTIONS_TABLE", "core_platform.cp_enterprise_subscriptions")
51
+ CORE_PLATFORM_CHANGE_PASSWORD_POLICY_TABLE = os.getenv("CORE_PLATFORM_CHANGE_PASSWORD_POLICY_TABLE", "core_platform.cp_change_password_policy")
52
+ CORE_PLATFORM_APP_FEATURES_TABLE = os.getenv("CORE_PLATFORM_APP_FEATURES_TABLE", "core_platform.cp_app_features")
58
53
 
59
54
  # =============================================================================
60
- # AZURE CONFIGURATION (Optional - for queue functionality)
55
+ # CORE PLATFORM TABLES (prefixed with cp_, now in core_platform schema with tenant_id)
61
56
  # =============================================================================
62
- STORAGE_ACCOUNT_NAME: str = os.getenv("STORAGE_ACCOUNT_NAME", "")
63
- USER_ASSIGNED_MANAGED_IDENTITY: str = os.getenv("USER_ASSIGNED_MANAGED_IDENTITY", "")
64
-
57
+ # NOTE: These tables have been renamed from tenant_ prefix to cp_ (core platform).
58
+ # All tables include tenant_id column for multi-tenant isolation.
59
+ # Tables with is_system column can contain both user and system data.
60
+ # =============================================================================
61
+ CORE_PLATFORM_GROUPS_TABLE = os.getenv("CORE_PLATFORM_GROUPS_TABLE", "core_platform.cp_groups")
62
+ CORE_PLATFORM_LOGIN_SETTINGS_TABLE = os.getenv("CORE_PLATFORM_LOGIN_SETTINGS_TABLE", "core_platform.cp_login_settings")
63
+ CORE_PLATFORM_RESOURCES_TABLE = os.getenv("CORE_PLATFORM_RESOURCES_TABLE", "core_platform.cp_resources")
64
+ CORE_PLATFORM_ASSIGN_ROLES_TABLE = os.getenv("CORE_PLATFORM_ASSIGN_ROLES_TABLE", "core_platform.cp_assign_roles")
65
+ CORE_PLATFORM_SUBSCRIPTION_HISTORY_TABLE = os.getenv("CORE_PLATFORM_SUBSCRIPTION_HISTORY_TABLE", "core_platform.cp_user_subscription_histories")
66
+ CORE_PLATFORM_RESOURCE_DELETION_CHAT_HISTORY_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_DELETION_CHAT_HISTORY_TABLE", "core_platform.cp_resource_deletion_chat_histories")
67
+ CORE_PLATFORM_USER_GROUPS_TABLE = os.getenv("CORE_PLATFORM_USER_GROUPS_TABLE", "core_platform.cp_user_groups")
68
+ CORE_PLATFORM_ACTIVITY_LOGS_TABLE = os.getenv("CORE_PLATFORM_ACTIVITY_LOGS_TABLE", "core_platform.cp_activity_logs")
69
+ CORE_PLATFORM_ORGANIZATIONS_TABLE = os.getenv("CORE_PLATFORM_ORGANIZATIONS_TABLE", "core_platform.cp_organizations")
70
+ CORE_PLATFORM_BUSINESSES_TABLE = os.getenv("CORE_PLATFORM_BUSINESSES_TABLE", "core_platform.cp_businesses")
71
+ CORE_PLATFORM_BUSINESS_APPS_TABLE = os.getenv("CORE_PLATFORM_BUSINESS_APPS_TABLE", "core_platform.cp_business_apps")
72
+ CORE_PLATFORM_LOCATIONS_TABLE = os.getenv("CORE_PLATFORM_LOCATIONS_TABLE", "core_platform.cp_locations")
73
+ CORE_PLATFORM_ASSIGN_LOCATIONS_TABLE = os.getenv("CORE_PLATFORM_ASSIGN_LOCATIONS_TABLE", "core_platform.cp_assign_locations")
74
+ CORE_PLATFORM_UNIT_OF_MEASURE_TABLE = os.getenv("CORE_PLATFORM_UNIT_OF_MEASURE_TABLE", "core_platform.cp_unit_of_measures")
75
+ CORE_PLATFORM_CURRENCY = os.getenv("CORE_PLATFORM_CURRENCY", "core_platform.cp_currencies")
76
+ CORE_PLATFORM_THEMES_TABLE = os.getenv("CORE_PLATFORM_THEMES_TABLE", "core_platform.cp_themes")
77
+ CORE_PLATFORM_NOTIFICATION_EMAIL_CREDENTIALS_TABLE = os.getenv("CORE_PLATFORM_NOTIFICATION_EMAIL_CREDENTIALS_TABLE", "core_platform.cp_notification_email_credentials")
78
+
79
+ # Mail Configurations
80
+ MAIL_SENDER_EMAIL=os.getenv("MAIL_SENDER_EMAIL")
81
+ MAIL_SENDER_PWD=os.getenv("MAIL_SENDER_PWD")
82
+
83
+ # Application Configurations
84
+ APP_URL=os.getenv("APP_URL", "https://trovesuite.com")
85
+ USER_ASSIGNED_MANAGED_IDENTITY=os.getenv("USER_ASSIGNED_MANAGED_IDENTITY")
86
+
65
87
  @property
66
88
  def database_url(self) -> str:
67
- """Get the database URL, either from DATABASE_URL or constructed from individual components"""
68
- if self.DATABASE_URL != "postgresql://username:password@localhost:5432/database_name":
89
+ if self.DATABASE_URL:
69
90
  return self.DATABASE_URL
70
-
71
- # Validate individual components
72
- if not all([self.DB_USER, self.DB_HOST, self.DB_NAME, self.DB_PASSWORD]):
73
- missing = []
74
- if not self.DB_USER:
75
- missing.append("DB_USER")
76
- if not self.DB_HOST:
77
- missing.append("DB_HOST")
78
- if not self.DB_NAME:
79
- missing.append("DB_NAME")
80
- if not self.DB_PASSWORD:
81
- missing.append("DB_PASSWORD")
82
-
83
- raise ValueError(
84
- f"Database configuration incomplete. Missing environment variables: {', '.join(missing)}. "
85
- f"Please set these variables or provide a complete DATABASE_URL."
86
- )
87
-
88
- return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
89
-
90
- def validate_configuration(self) -> None:
91
- """Validate the current configuration and warn about potential issues"""
92
- warnings_list = []
93
-
94
- # Check for default secret key
95
- if self.SECRET_KEY == "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7":
96
- warnings_list.append(
97
- "SECRET_KEY is using the default value. This is insecure for production. "
98
- "Please set a strong, unique SECRET_KEY environment variable."
99
- )
100
-
101
- # Check for development environment in production-like settings
102
- if self.ENVIRONMENT == "development" and self.DEBUG is False:
103
- warnings_list.append(
104
- "ENVIRONMENT is set to 'development' but DEBUG is False. "
105
- "Consider setting ENVIRONMENT to 'production' for production deployments."
106
- )
107
-
108
- # Check database configuration
109
- try:
110
- self.database_url
111
- except ValueError as e:
112
- warnings_list.append(f"Database configuration issue: {str(e)}")
113
-
114
- # Check for missing Azure configuration if needed
115
- if self.ENVIRONMENT == "production" and not self.STORAGE_ACCOUNT_NAME:
116
- warnings_list.append(
117
- "STORAGE_ACCOUNT_NAME is not set. Azure queue functionality may not work properly."
118
- )
119
-
120
- # Emit warnings
121
- for warning in warnings_list:
122
- warnings.warn(warning, UserWarning)
123
-
124
- def get_configuration_summary(self) -> dict:
125
- """Get a summary of the current configuration (excluding sensitive data)"""
126
- return {
127
- "app_name": self.APP_NAME,
128
- "environment": self.ENVIRONMENT,
129
- "debug": self.DEBUG,
130
- "database_host": self.DB_HOST,
131
- "database_port": self.DB_PORT,
132
- "database_name": self.DB_NAME,
133
- "database_user": self.DB_USER,
134
- "log_level": self.LOG_LEVEL,
135
- "log_format": self.LOG_FORMAT,
136
- "log_to_file": self.LOG_TO_FILE,
137
- "algorithm": self.ALGORITHM,
138
- "access_token_expire_minutes": self.ACCESS_TOKEN_EXPIRE_MINUTES,
139
- "MAIN_TENANTS_TABLE": self.MAIN_TENANTS_TABLE,
140
- "role_permissions_table": self.ROLE_PERMISSIONS_TABLE,
141
- "TENANT_LOGIN_SETTINGS_TABLE": self.TENANT_LOGIN_SETTINGS_TABLE,
142
- "user_groups_table": self.USER_GROUPS_TABLE,
143
- "assign_roles_table": self.ASSIGN_ROLES_TABLE,
144
- }
145
91
 
146
- # Global settings instance
147
- db_settings = Settings()
92
+ port = int(self.DB_PORT) if self.DB_PORT else 5432
93
+ return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{port}/{self.DB_NAME}"
148
94
 
149
- # Validate configuration on import
150
- try:
151
- db_settings.validate_configuration()
152
- except Exception as e:
153
- warnings.warn("Configuration validation failed: %s", str(e), UserWarning)
95
+ # Global settings instance
96
+ db_settings = Settings()
@@ -1,8 +1,8 @@
1
1
  from fastapi import APIRouter
2
- from src.entities.shared.shared_response import Respons
3
- from src.configs.settings import db_settings
4
- from src.configs.database import DatabaseManager
5
- from src.configs.logging import get_logger
2
+ from .sh_response import Respons
3
+ from ..configs.settings import db_settings
4
+ from ..configs.database import DatabaseManager
5
+ from ..configs.logging import get_logger
6
6
 
7
7
  health_check_router = APIRouter(tags=["Health Path"])
8
8
  logger = get_logger("health")
@@ -0,0 +1,14 @@
1
+ """
2
+ TroveSuite Notification Service
3
+
4
+ Provides email and SMS notification capabilities for TroveSuite applications.
5
+ """
6
+
7
+ from .notification_service import NotificationService
8
+ from .notification_write_dto import NotificationEmailServiceWriteDto, NotificationSMSServiceWriteDto
9
+
10
+ __all__ = [
11
+ "NotificationService",
12
+ "NotificationEmailServiceWriteDto",
13
+ "NotificationSMSServiceWriteDto"
14
+ ]
@@ -0,0 +1,13 @@
1
+ from typing import List, Optional, Union
2
+ from pydantic import BaseModel
3
+
4
+ class NotificationEmailBase(BaseModel):
5
+ sender_email: str
6
+ receiver_email: Union[str, List[str]]
7
+ password: str
8
+ subject: str
9
+ text_message: str
10
+ html_message: Optional[str] = None
11
+
12
+ class NotificationSMSBase(BaseModel):
13
+ pass