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 CHANGED
@@ -11,7 +11,7 @@ from .notification import NotificationService
11
11
  from .storage import StorageService
12
12
  from .utils import Helper
13
13
 
14
- __version__ = "1.0.20"
14
+ __version__ = "1.0.29"
15
15
  __author__ = "Bright Debrah Owusu"
16
16
  __email__ = "owusu.debrah@deladetech.com"
17
17
 
@@ -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
- self.pool_size = 5
25
- self.max_overflow = 10
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": 10
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=self.pool_size,
88
+ maxconn=pool_size,
63
89
  **self.get_connection_params()
64
90
  )
65
- 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
+ )
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
- """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
+ """
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
- try:
96
- # Test connection first
97
- if not db_config.test_connection():
98
- raise Exception("Database connection test failed")
99
-
100
- # Create connection pool
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
- logger.info("Database initialization completed successfully")
164
+ logger.info("Database initialization completed successfully")
104
165
 
105
- except Exception as e:
106
- logger.error(f"Database initialization failed: {str(e)}")
107
- raise
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
- """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
+ """
112
177
  global _connection_pool
178
+
113
179
  if _connection_pool is None:
114
180
  error_msg = (
115
- "Database not initialized. This usually means:\n"
116
- "1. Missing or incorrect .env file in app/ directory\n"
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
- """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
+ """
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
- logger.error(f"Database connection error: {str(e)}")
156
- if conn:
207
+ # If connection exists and isn't closed, rollback transaction
208
+ if conn and not conn.closed:
157
209
  try:
158
- # Only rollback if connection is still open
159
- if not conn.closed:
160
- conn.rollback()
161
- except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
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
- """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
+ """
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 on module import
307
- try:
308
- initialize_database()
309
- except Exception as e:
310
- logger.error(f"Failed to initialize database on startup: {str(e)}")
311
- logger.error("⚠️ CRITICAL: Application started without database connection!")
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()
@@ -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.cp_shared_resource_ids")
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")
@@ -96,11 +96,12 @@ class Helper:
96
96
 
97
97
  try:
98
98
  if tenant_id:
99
- # For tenant-specific resource IDs, check shared schema with tenant_id filter
100
- tenant_resource_table = getattr(db_settings, 'CORE_PLATFORM_TENANT_RESOURCE_ID_TABLE', None) or getattr(db_settings, 'CORE_PLATFORM_RESOURCE_ID_TABLE', None)
101
- if tenant_resource_table:
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 {tenant_resource_table}
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
- """Send a dynamic notification email."""
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
- # Build email data
348
- mail_sender_email = getattr(db_settings, 'MAIL_SENDER_EMAIL', None)
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("MAIL_SENDER_EMAIL or MAIL_SENDER_PWD not configured in settings")
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.24
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=vKnbXtMVw2mVbOuJLD1GhwcIx0oaNnyUL7Mrpk9Qa-I,646
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=IPSu8fXjxyYeJ3bFknJG06Qm2L2ub6Ht19xhKv8g7nA,11731
9
+ trovesuite/configs/database.py,sha256=83lckIpRLNLKgLNzXdzczlGjCRAM6DDFLyJTMxleyfw,15008
10
10
  trovesuite/configs/logging.py,sha256=mGjR2d4urVNry9l5_aXycMMtcY2RAFIpEL35hw33KZg,9308
11
- trovesuite/configs/settings.py,sha256=cogrBmLoKUTwzsFrMecMMXqPbyRZNCaFvotMqykh0gk,7300
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=xrr9W_JfhLckHGAgh4ECjsnMz_DIRXfUbwqVRlvLJfI,27070
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.24.dist-info/licenses/LICENSE,sha256=EJT35ct-Q794JYPdAQy3XNczQGKkU1HzToLeK1YVw2s,1070
31
- trovesuite-1.0.24.dist-info/METADATA,sha256=lEIZkE_5Rv12-EU3BfsLShJZWlrpOXDEUp9QyJFnrYg,21737
32
- trovesuite-1.0.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- trovesuite-1.0.24.dist-info/top_level.txt,sha256=GzKhG_-MTaxeHrIgkGkBH_nof2vroGFBrjeHKWUIwNc,11
34
- trovesuite-1.0.24.dist-info/RECORD,,
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,,