trovesuite 1.0.24__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.
- trovesuite/__init__.py +1 -1
- trovesuite/configs/database.py +124 -64
- trovesuite/configs/settings.py +2 -9
- trovesuite/utils/helper.py +99 -10
- {trovesuite-1.0.24.dist-info → trovesuite-1.0.31.dist-info}/METADATA +1 -1
- {trovesuite-1.0.24.dist-info → trovesuite-1.0.31.dist-info}/RECORD +9 -9
- {trovesuite-1.0.24.dist-info → trovesuite-1.0.31.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.24.dist-info → trovesuite-1.0.31.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.24.dist-info → trovesuite-1.0.31.dist-info}/top_level.txt +0 -0
trovesuite/__init__.py
CHANGED
trovesuite/configs/database.py
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
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
|
|
32
|
+
_initialization_lock = threading.Lock()
|
|
16
33
|
|
|
17
34
|
|
|
18
35
|
class DatabaseConfig:
|
|
19
36
|
"""Database configuration and connection management"""
|
|
20
37
|
|
|
21
38
|
def __init__(self):
|
|
39
|
+
import os
|
|
22
40
|
self.settings = db_settings
|
|
23
41
|
self.database_url = self.settings.database_url
|
|
24
|
-
|
|
25
|
-
|
|
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"))
|
|
26
47
|
|
|
27
48
|
def get_connection_params(self) -> dict:
|
|
28
49
|
"""Get database connection parameters"""
|
|
@@ -35,7 +56,7 @@ class DatabaseConfig:
|
|
|
35
56
|
"keepalives_idle": 30,
|
|
36
57
|
"keepalives_interval": 10,
|
|
37
58
|
"keepalives_count": 5,
|
|
38
|
-
"connect_timeout":
|
|
59
|
+
"connect_timeout": 30 # Increased timeout for Azure PostgreSQL
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
# fallback to individual DB_* variables
|
|
@@ -57,19 +78,43 @@ class DatabaseConfig:
|
|
|
57
78
|
def create_connection_pool(self) -> psycopg2.pool.ThreadedConnectionPool:
|
|
58
79
|
"""Create a connection pool for psycopg2"""
|
|
59
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
|
+
|
|
60
86
|
pool = psycopg2.pool.ThreadedConnectionPool(
|
|
61
87
|
minconn=1,
|
|
62
|
-
maxconn=
|
|
88
|
+
maxconn=pool_size,
|
|
63
89
|
**self.get_connection_params()
|
|
64
90
|
)
|
|
65
|
-
logger.info(
|
|
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
|
+
)
|
|
66
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
|
|
67
112
|
except Exception as e:
|
|
68
113
|
logger.error(f"Failed to create database connection pool: {str(e)}")
|
|
69
114
|
raise
|
|
70
115
|
|
|
71
116
|
def test_connection(self) -> bool:
|
|
72
|
-
"""Test database connection"""
|
|
117
|
+
"""Test database connection (only used at startup)"""
|
|
73
118
|
try:
|
|
74
119
|
with psycopg2.connect(**self.get_connection_params()) as conn:
|
|
75
120
|
with conn.cursor() as cursor:
|
|
@@ -89,89 +134,104 @@ db_config = DatabaseConfig()
|
|
|
89
134
|
|
|
90
135
|
|
|
91
136
|
def initialize_database():
|
|
92
|
-
"""
|
|
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
|
+
"""
|
|
93
144
|
global _connection_pool
|
|
145
|
+
|
|
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
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Test connection first (only at startup)
|
|
154
|
+
if not db_config.test_connection():
|
|
155
|
+
raise Exception("Database connection test failed")
|
|
94
156
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
_connection_pool = db_config.create_connection_pool()
|
|
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")
|
|
102
163
|
|
|
103
|
-
|
|
164
|
+
logger.info("✅ Database initialization completed successfully")
|
|
104
165
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
170
|
|
|
109
171
|
|
|
110
172
|
def get_connection_pool() -> psycopg2.pool.ThreadedConnectionPool:
|
|
111
|
-
"""
|
|
173
|
+
"""
|
|
174
|
+
Get the database connection pool.
|
|
175
|
+
Pool must be initialized at startup. This function will raise if pool is None.
|
|
176
|
+
"""
|
|
112
177
|
global _connection_pool
|
|
178
|
+
|
|
113
179
|
if _connection_pool is None:
|
|
114
180
|
error_msg = (
|
|
115
|
-
"Database not initialized.
|
|
116
|
-
"
|
|
117
|
-
"2. Database credentials are wrong\n"
|
|
118
|
-
"3. Database container is not running\n"
|
|
119
|
-
"4. Database initialization failed during startup\n"
|
|
120
|
-
"Please check the startup logs for more details."
|
|
181
|
+
"Database connection pool is not initialized. "
|
|
182
|
+
"Please ensure initialize_database() was called at application startup."
|
|
121
183
|
)
|
|
122
184
|
logger.error(error_msg)
|
|
123
185
|
raise Exception(error_msg)
|
|
186
|
+
|
|
124
187
|
return _connection_pool
|
|
125
188
|
|
|
126
189
|
|
|
127
|
-
def _validate_connection(conn) -> bool:
|
|
128
|
-
"""Validate if a connection is still alive"""
|
|
129
|
-
try:
|
|
130
|
-
# Test if connection is alive with a simple query
|
|
131
|
-
with conn.cursor() as cursor:
|
|
132
|
-
cursor.execute("SELECT 1")
|
|
133
|
-
return True
|
|
134
|
-
except (psycopg2.OperationalError, psycopg2.InterfaceError):
|
|
135
|
-
return False
|
|
136
|
-
|
|
137
|
-
|
|
138
190
|
@contextmanager
|
|
139
191
|
def get_db_connection():
|
|
140
|
-
"""
|
|
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
|
+
"""
|
|
141
198
|
pool = get_connection_pool()
|
|
142
199
|
conn = None
|
|
143
200
|
try:
|
|
201
|
+
# Get connection from pool - this will block if pool is exhausted
|
|
202
|
+
# That's the correct behavior - let backpressure happen naturally
|
|
144
203
|
conn = pool.getconn()
|
|
145
|
-
|
|
146
|
-
# Validate connection before using it
|
|
147
|
-
if not _validate_connection(conn):
|
|
148
|
-
logger.warning("Stale connection detected, getting new connection")
|
|
149
|
-
pool.putconn(conn, close=True)
|
|
150
|
-
conn = pool.getconn()
|
|
151
|
-
|
|
152
204
|
logger.debug("Database connection acquired from pool")
|
|
153
205
|
yield conn
|
|
154
206
|
except Exception as e:
|
|
155
|
-
|
|
156
|
-
if conn:
|
|
207
|
+
# If connection exists and isn't closed, rollback transaction
|
|
208
|
+
if conn and not conn.closed:
|
|
157
209
|
try:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
logger.warning(f"Could not rollback closed connection: {str(rollback_error)}")
|
|
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
|
|
163
214
|
raise
|
|
164
215
|
finally:
|
|
216
|
+
# Always return connection to pool
|
|
165
217
|
if conn:
|
|
166
218
|
try:
|
|
167
|
-
# If connection is broken, close it instead of returning to pool
|
|
168
219
|
if conn.closed:
|
|
220
|
+
# If connection is closed, tell pool to close it instead of returning
|
|
169
221
|
pool.putconn(conn, close=True)
|
|
170
222
|
else:
|
|
223
|
+
# Return connection to pool normally
|
|
171
224
|
pool.putconn(conn)
|
|
172
225
|
logger.debug("Database connection returned to pool")
|
|
173
226
|
except Exception as put_error:
|
|
227
|
+
# Log error but don't fail - connection will be cleaned up by pool
|
|
174
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
|
|
175
235
|
|
|
176
236
|
|
|
177
237
|
@contextmanager
|
|
@@ -244,6 +304,7 @@ class DatabaseManager:
|
|
|
244
304
|
cursor.execute("INSERT INTO table2 ...")
|
|
245
305
|
# Auto-commits on success, auto-rollbacks on exception
|
|
246
306
|
"""
|
|
307
|
+
# Use get_db_connection() instead of directly accessing pool
|
|
247
308
|
with get_db_connection() as conn:
|
|
248
309
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
|
249
310
|
try:
|
|
@@ -267,7 +328,10 @@ class DatabaseManager:
|
|
|
267
328
|
|
|
268
329
|
@staticmethod
|
|
269
330
|
def health_check() -> dict:
|
|
270
|
-
"""
|
|
331
|
+
"""
|
|
332
|
+
Perform database health check.
|
|
333
|
+
Health checks are allowed to fail - they don't attempt to repair the pool.
|
|
334
|
+
"""
|
|
271
335
|
try:
|
|
272
336
|
with get_db_cursor() as cursor:
|
|
273
337
|
cursor.execute("SELECT version(), current_database(), current_user")
|
|
@@ -303,13 +367,9 @@ class DatabaseManager:
|
|
|
303
367
|
}
|
|
304
368
|
|
|
305
369
|
|
|
306
|
-
# Database initialization
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
logger.error("⚠️ Please check your .env file and database configuration")
|
|
313
|
-
logger.error("⚠️ The application will not function properly without a database connection")
|
|
314
|
-
# Don't raise here to allow the app to start even if DB is unavailable
|
|
315
|
-
# But log it clearly so it's obvious what's wrong
|
|
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()
|
trovesuite/configs/settings.py
CHANGED
|
@@ -10,11 +10,6 @@ class Settings:
|
|
|
10
10
|
DB_PORT: str = os.getenv("DB_PORT")
|
|
11
11
|
DB_PASSWORD: str = os.getenv("DB_PASSWORD")
|
|
12
12
|
|
|
13
|
-
# CORS Endpoint
|
|
14
|
-
# LOCAL_HOST_URL = os.getenv("LOCAL_HOST_URL")
|
|
15
|
-
# FRONTEND_SERVER_URL_1 = os.getenv("FRONTEND_SERVER_URL")
|
|
16
|
-
# FRONTEND_SERVER_URL_2 = os.getenv("FRONTEND_SERVER_URL")
|
|
17
|
-
|
|
18
13
|
# Application settings
|
|
19
14
|
DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
|
|
20
15
|
APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
|
|
@@ -38,13 +33,11 @@ class Settings:
|
|
|
38
33
|
# SHARED TABLES (core_platform schema)
|
|
39
34
|
# =============================================================================
|
|
40
35
|
CORE_PLATFORM_TENANTS_TABLE = os.getenv("CORE_PLATFORM_TENANTS_TABLE", "core_platform.cp_tenants")
|
|
41
|
-
CORE_PLATFORM_TENANT_RESOURCE_ID_TABLE = os.getenv("CORE_PLATFORM_TENANT_RESOURCE_ID_TABLE", "core_platform.cp_resource_ids")
|
|
42
36
|
CORE_PLATFORM_SUBSCRIPTIONS_TABLE = os.getenv("CORE_PLATFORM_SUBSCRIPTIONS_TABLE", "core_platform.cp_subscriptions")
|
|
43
37
|
CORE_PLATFORM_APPS_TABLE = os.getenv("CORE_PLATFORM_APPS_TABLE", "core_platform.cp_apps")
|
|
44
38
|
CORE_PLATFORM_USERS_TABLE = os.getenv("CORE_PLATFORM_USERS_TABLE", "core_platform.cp_users")
|
|
45
39
|
CORE_PLATFORM_RESOURCE_TYPES_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_TYPES_TABLE", "core_platform.cp_resource_types")
|
|
46
|
-
CORE_PLATFORM_RESOURCE_ID_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_ID_TABLE", "core_platform.
|
|
47
|
-
CORE_PLATFORM_RESOURCES_TABLE = os.getenv("CORE_PLATFORM_RESOURCES_TABLE", "core_platform.resources")
|
|
40
|
+
CORE_PLATFORM_RESOURCE_ID_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_ID_TABLE", "core_platform.cp_resource_ids")
|
|
48
41
|
CORE_PLATFORM_PERMISSIONS_TABLE = os.getenv("CORE_PLATFORM_PERMISSIONS_TABLE", "core_platform.cp_permissions")
|
|
49
42
|
CORE_PLATFORM_ROLES_TABLE = os.getenv("CORE_PLATFORM_ROLES_TABLE", "core_platform.cp_roles")
|
|
50
43
|
CORE_PLATFORM_ROLE_PERMISSIONS_TABLE = os.getenv("CORE_PLATFORM_ROLE_PERMISSIONS_TABLE", "core_platform.cp_role_permissions")
|
|
@@ -69,7 +62,6 @@ class Settings:
|
|
|
69
62
|
CORE_PLATFORM_LOGIN_SETTINGS_TABLE = os.getenv("CORE_PLATFORM_LOGIN_SETTINGS_TABLE", "core_platform.cp_login_settings")
|
|
70
63
|
CORE_PLATFORM_RESOURCES_TABLE = os.getenv("CORE_PLATFORM_RESOURCES_TABLE", "core_platform.cp_resources")
|
|
71
64
|
CORE_PLATFORM_ASSIGN_ROLES_TABLE = os.getenv("CORE_PLATFORM_ASSIGN_ROLES_TABLE", "core_platform.cp_assign_roles")
|
|
72
|
-
CORE_PLATFORM_RESOURCE_ID_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_ID_TABLE", "core_platform.cp_resource_ids")
|
|
73
65
|
CORE_PLATFORM_SUBSCRIPTION_HISTORY_TABLE = os.getenv("CORE_PLATFORM_SUBSCRIPTION_HISTORY_TABLE", "core_platform.cp_user_subscription_histories")
|
|
74
66
|
CORE_PLATFORM_RESOURCE_DELETION_CHAT_HISTORY_TABLE = os.getenv("CORE_PLATFORM_RESOURCE_DELETION_CHAT_HISTORY_TABLE", "core_platform.cp_resource_deletion_chat_histories")
|
|
75
67
|
CORE_PLATFORM_USER_GROUPS_TABLE = os.getenv("CORE_PLATFORM_USER_GROUPS_TABLE", "core_platform.cp_user_groups")
|
|
@@ -82,6 +74,7 @@ class Settings:
|
|
|
82
74
|
CORE_PLATFORM_UNIT_OF_MEASURE_TABLE = os.getenv("CORE_PLATFORM_UNIT_OF_MEASURE_TABLE", "core_platform.cp_unit_of_measures")
|
|
83
75
|
CORE_PLATFORM_CURRENCY = os.getenv("CORE_PLATFORM_CURRENCY", "core_platform.cp_currencies")
|
|
84
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")
|
|
85
78
|
|
|
86
79
|
# Mail Configurations
|
|
87
80
|
MAIL_SENDER_EMAIL=os.getenv("MAIL_SENDER_EMAIL")
|
trovesuite/utils/helper.py
CHANGED
|
@@ -96,11 +96,12 @@ class Helper:
|
|
|
96
96
|
|
|
97
97
|
try:
|
|
98
98
|
if tenant_id:
|
|
99
|
-
# For tenant-specific resource IDs,
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
# For tenant-specific resource IDs, use CORE_PLATFORM_RESOURCE_ID_TABLE
|
|
100
|
+
# This table has tenant_id column for filtering
|
|
101
|
+
resource_table = getattr(db_settings, 'CORE_PLATFORM_RESOURCE_ID_TABLE', None)
|
|
102
|
+
if resource_table:
|
|
102
103
|
resource_exists = DatabaseManager.execute_scalar(
|
|
103
|
-
f"""SELECT COUNT(1) FROM {
|
|
104
|
+
f"""SELECT COUNT(1) FROM {resource_table}
|
|
104
105
|
WHERE tenant_id = %s AND id = %s""",
|
|
105
106
|
(tenant_id, candidate,),
|
|
106
107
|
)
|
|
@@ -108,7 +109,7 @@ class Helper:
|
|
|
108
109
|
# Fallback: assume no conflict if table not configured
|
|
109
110
|
resource_exists = 0
|
|
110
111
|
else:
|
|
111
|
-
# For main schema resource IDs
|
|
112
|
+
# For main/shared schema resource IDs (no tenant_id)
|
|
112
113
|
main_resource_table = getattr(db_settings, 'CORE_PLATFORM_RESOURCE_ID_TABLE', None)
|
|
113
114
|
if main_resource_table:
|
|
114
115
|
resource_exists = DatabaseManager.execute_scalar(
|
|
@@ -313,6 +314,75 @@ class Helper:
|
|
|
313
314
|
# Log the error but don't fail the main operation
|
|
314
315
|
logger.error(f"Failed to log activity: {str(e)}", exc_info=True)
|
|
315
316
|
|
|
317
|
+
@staticmethod
|
|
318
|
+
def get_email_credentials(tenant_id: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
|
|
319
|
+
"""
|
|
320
|
+
Get email credentials for sending notifications.
|
|
321
|
+
If tenant_id is provided and tenant has custom email credentials, use those.
|
|
322
|
+
Otherwise, fall back to system default credentials.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
tenant_id: Optional tenant ID to check for tenant-specific credentials
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple of (email, password) or (None, None) if not configured
|
|
329
|
+
"""
|
|
330
|
+
# If tenant_id is provided, check for tenant-specific credentials
|
|
331
|
+
if tenant_id:
|
|
332
|
+
try:
|
|
333
|
+
credentials_table = getattr(db_settings, 'CORE_PLATFORM_NOTIFICATION_EMAIL_CREDENTIALS_TABLE', 'core_platform.cp_notification_email_credentials')
|
|
334
|
+
tenant_credentials = DatabaseManager.execute_query(
|
|
335
|
+
f"""SELECT notification_email, notification_password
|
|
336
|
+
FROM {credentials_table}
|
|
337
|
+
WHERE tenant_id = %s
|
|
338
|
+
AND delete_status = 'NOT_DELETED'
|
|
339
|
+
AND is_active = true""",
|
|
340
|
+
(tenant_id,),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if tenant_credentials and len(tenant_credentials) > 0:
|
|
344
|
+
tenant_email = tenant_credentials[0].get('notification_email')
|
|
345
|
+
tenant_password = tenant_credentials[0].get('notification_password')
|
|
346
|
+
|
|
347
|
+
# If tenant has both email and password configured, use them
|
|
348
|
+
if tenant_email and tenant_password:
|
|
349
|
+
logger.info(
|
|
350
|
+
f"Using tenant-specific email credentials for tenant {tenant_id}",
|
|
351
|
+
extra={
|
|
352
|
+
"extra_fields": {
|
|
353
|
+
"tenant_id": tenant_id,
|
|
354
|
+
"email": tenant_email
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
return (tenant_email, tenant_password)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.warning(
|
|
361
|
+
f"Error fetching tenant email credentials for tenant {tenant_id}: {str(e)}",
|
|
362
|
+
extra={
|
|
363
|
+
"extra_fields": {
|
|
364
|
+
"tenant_id": tenant_id,
|
|
365
|
+
"error": str(e)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Fall back to system default credentials
|
|
371
|
+
mail_sender_email = getattr(db_settings, 'MAIL_SENDER_EMAIL', None)
|
|
372
|
+
mail_sender_pwd = getattr(db_settings, 'MAIL_SENDER_PWD', None)
|
|
373
|
+
|
|
374
|
+
if tenant_id:
|
|
375
|
+
logger.info(
|
|
376
|
+
f"Using system default email credentials for tenant {tenant_id}",
|
|
377
|
+
extra={
|
|
378
|
+
"extra_fields": {
|
|
379
|
+
"tenant_id": tenant_id
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return (mail_sender_email, mail_sender_pwd)
|
|
385
|
+
|
|
316
386
|
@staticmethod
|
|
317
387
|
def send_notification(
|
|
318
388
|
email: str,
|
|
@@ -320,8 +390,19 @@ class Helper:
|
|
|
320
390
|
text_template: str,
|
|
321
391
|
html_template: str,
|
|
322
392
|
variables: Optional[Dict[str, str]] = None,
|
|
393
|
+
tenant_id: Optional[str] = None,
|
|
323
394
|
):
|
|
324
|
-
"""
|
|
395
|
+
"""
|
|
396
|
+
Send a dynamic notification email.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
email: Recipient email address
|
|
400
|
+
subject: Email subject
|
|
401
|
+
text_template: Plain text email template
|
|
402
|
+
html_template: HTML email template
|
|
403
|
+
variables: Optional dictionary of variables to format templates
|
|
404
|
+
tenant_id: Optional tenant ID to use tenant-specific email credentials
|
|
405
|
+
"""
|
|
325
406
|
current_time = Helper.current_date_time()
|
|
326
407
|
cdate = current_time["cdate"]
|
|
327
408
|
ctime = current_time["ctime"]
|
|
@@ -344,12 +425,19 @@ class Helper:
|
|
|
344
425
|
text_message = text_template.format(**safe_variables)
|
|
345
426
|
html_message = html_template.format(**safe_variables)
|
|
346
427
|
|
|
347
|
-
#
|
|
348
|
-
mail_sender_email =
|
|
349
|
-
mail_sender_pwd = getattr(db_settings, 'MAIL_SENDER_PWD', None)
|
|
428
|
+
# Get email credentials (tenant-specific or system default)
|
|
429
|
+
mail_sender_email, mail_sender_pwd = Helper.get_email_credentials(tenant_id)
|
|
350
430
|
|
|
351
431
|
if not mail_sender_email or not mail_sender_pwd:
|
|
352
|
-
logger.error(
|
|
432
|
+
logger.error(
|
|
433
|
+
"Email credentials not configured. MAIL_SENDER_EMAIL or MAIL_SENDER_PWD not set in settings",
|
|
434
|
+
extra={
|
|
435
|
+
"extra_fields": {
|
|
436
|
+
"tenant_id": tenant_id,
|
|
437
|
+
"receiver_email": email
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
)
|
|
353
441
|
return
|
|
354
442
|
|
|
355
443
|
notification_data = NotificationEmailServiceWriteDto(
|
|
@@ -625,6 +713,7 @@ class Helper:
|
|
|
625
713
|
"message_row_style": message_row_style,
|
|
626
714
|
"app_url": app_url,
|
|
627
715
|
},
|
|
716
|
+
tenant_id=tenant_id,
|
|
628
717
|
)
|
|
629
718
|
|
|
630
719
|
logger.info(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trovesuite
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.31
|
|
4
4
|
Summary: TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications
|
|
5
5
|
Home-page: https://dev.azure.com/brightgclt/trovesuite/_git/packages
|
|
6
6
|
Author: Bright Debrah Owusu
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trovesuite/__init__.py,sha256=
|
|
1
|
+
trovesuite/__init__.py,sha256=tcNxxwRpfYZ23yCEn4b5EHqGL5zRe_BqQHLcNAP23PQ,646
|
|
2
2
|
trovesuite/auth/__init__.py,sha256=OjZllVvjul1glDazJ-d5TrNjgHFigFlQQi1G99DYshk,239
|
|
3
3
|
trovesuite/auth/auth_base.py,sha256=rZHQVLeJRBQ8GClgF5UwG-er4_HXVX5-nt8o6_Z29uY,75
|
|
4
4
|
trovesuite/auth/auth_controller.py,sha256=PAgaVlf5TYEfkSfK4vGGsvO84i8zEmeVVXyUF2YBppI,420
|
|
@@ -6,9 +6,9 @@ trovesuite/auth/auth_read_dto.py,sha256=e27JqKVPVUM83A_mYF452QCflsvGNo7aKje7q_ur
|
|
|
6
6
|
trovesuite/auth/auth_service.py,sha256=TQOJFG0AzhPGwZBAXVxMkHxyG2wyct4Zcoq4z0cVBO4,22201
|
|
7
7
|
trovesuite/auth/auth_write_dto.py,sha256=rdwI7w6-9QZGv1H0PAGrjkLBCzaMHjgPIXeLb9RmNec,234
|
|
8
8
|
trovesuite/configs/__init__.py,sha256=h1mSZOaZ3kUy1ZMO_m9O9KklsxywM0RfMVZLh9h9WvQ,328
|
|
9
|
-
trovesuite/configs/database.py,sha256=
|
|
9
|
+
trovesuite/configs/database.py,sha256=83lckIpRLNLKgLNzXdzczlGjCRAM6DDFLyJTMxleyfw,15008
|
|
10
10
|
trovesuite/configs/logging.py,sha256=mGjR2d4urVNry9l5_aXycMMtcY2RAFIpEL35hw33KZg,9308
|
|
11
|
-
trovesuite/configs/settings.py,sha256=
|
|
11
|
+
trovesuite/configs/settings.py,sha256=s-RvzFI7IR8-Q_A0ZRxlTr-iDoY6MvbxM0RkbhmJOL4,6915
|
|
12
12
|
trovesuite/entities/__init__.py,sha256=Dbl_03Bueyh2vOP2hykd40MmNMrl5nNHSRGP-kqwwNo,160
|
|
13
13
|
trovesuite/entities/health.py,sha256=KaW7yxTQdymIPlnkJJkDqEebBXkD0a7A66i5GgNZLoE,2700
|
|
14
14
|
trovesuite/entities/sh_response.py,sha256=1_sw3PpVaDxWsNiBU0W9YLHZgTFxEj4JJBLBfSY63Ho,1579
|
|
@@ -25,10 +25,10 @@ trovesuite/storage/storage_read_dto.py,sha256=o7EVJdwrwVZAaeyGU9O01WMECGVaytkvLR
|
|
|
25
25
|
trovesuite/storage/storage_service.py,sha256=V7LIePIV6b_iuhm-9x8r4zwpZHgeRPL1YIe5IBnxhco,19768
|
|
26
26
|
trovesuite/storage/storage_write_dto.py,sha256=vl1iCZ93bpFmpvkCrn587QtMtOA_TPDseXSoTuj9RTQ,1355
|
|
27
27
|
trovesuite/utils/__init__.py,sha256=mDZuY77BphvQFYLmcWxjP5Tcq9ZZ3WXJWBKB1v6wzHU,185
|
|
28
|
-
trovesuite/utils/helper.py,sha256=
|
|
28
|
+
trovesuite/utils/helper.py,sha256=qpd-EWPaX3-QJA5xvxb4s9rEb9W2RKPCDXcdAKSUwSM,30858
|
|
29
29
|
trovesuite/utils/templates.py,sha256=_92k4-EkqWs-h0LNJxPgorbspmp24kDngS7O3qWIFyQ,20388
|
|
30
|
-
trovesuite-1.0.
|
|
31
|
-
trovesuite-1.0.
|
|
32
|
-
trovesuite-1.0.
|
|
33
|
-
trovesuite-1.0.
|
|
34
|
-
trovesuite-1.0.
|
|
30
|
+
trovesuite-1.0.31.dist-info/licenses/LICENSE,sha256=EJT35ct-Q794JYPdAQy3XNczQGKkU1HzToLeK1YVw2s,1070
|
|
31
|
+
trovesuite-1.0.31.dist-info/METADATA,sha256=gPt59uOFfKxbY9v0WuWclaw4ex4l-UVO-GyNte45apo,21737
|
|
32
|
+
trovesuite-1.0.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
trovesuite-1.0.31.dist-info/top_level.txt,sha256=GzKhG_-MTaxeHrIgkGkBH_nof2vroGFBrjeHKWUIwNc,11
|
|
34
|
+
trovesuite-1.0.31.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|