trovesuite 1.0.29__tar.gz → 1.0.31__tar.gz

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.
Files changed (43) hide show
  1. {trovesuite-1.0.29/src/trovesuite.egg-info → trovesuite-1.0.31}/PKG-INFO +1 -1
  2. {trovesuite-1.0.29 → trovesuite-1.0.31}/pyproject.toml +2 -2
  3. {trovesuite-1.0.29 → trovesuite-1.0.31}/setup.py +1 -1
  4. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/configs/database.py +115 -154
  5. {trovesuite-1.0.29 → trovesuite-1.0.31/src/trovesuite.egg-info}/PKG-INFO +1 -1
  6. {trovesuite-1.0.29 → trovesuite-1.0.31}/LICENSE +0 -0
  7. {trovesuite-1.0.29 → trovesuite-1.0.31}/MANIFEST.in +0 -0
  8. {trovesuite-1.0.29 → trovesuite-1.0.31}/README.md +0 -0
  9. {trovesuite-1.0.29 → trovesuite-1.0.31}/requirements.txt +0 -0
  10. {trovesuite-1.0.29 → trovesuite-1.0.31}/setup.cfg +0 -0
  11. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/__init__.py +0 -0
  12. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/__init__.py +0 -0
  13. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/auth_base.py +0 -0
  14. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/auth_controller.py +0 -0
  15. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/auth_read_dto.py +0 -0
  16. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/auth_service.py +0 -0
  17. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/auth/auth_write_dto.py +0 -0
  18. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/configs/__init__.py +0 -0
  19. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/configs/logging.py +0 -0
  20. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/configs/settings.py +0 -0
  21. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/entities/__init__.py +0 -0
  22. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/entities/health.py +0 -0
  23. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/entities/sh_response.py +0 -0
  24. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/__init__.py +0 -0
  25. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/notification_base.py +0 -0
  26. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/notification_controller.py +0 -0
  27. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/notification_read_dto.py +0 -0
  28. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/notification_service.py +0 -0
  29. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/notification/notification_write_dto.py +0 -0
  30. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/__init__.py +0 -0
  31. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/storage_base.py +0 -0
  32. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/storage_controller.py +0 -0
  33. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/storage_read_dto.py +0 -0
  34. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/storage_service.py +0 -0
  35. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/storage/storage_write_dto.py +0 -0
  36. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/utils/__init__.py +0 -0
  37. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/utils/helper.py +0 -0
  38. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite/utils/templates.py +0 -0
  39. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite.egg-info/SOURCES.txt +0 -0
  40. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite.egg-info/dependency_links.txt +0 -0
  41. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite.egg-info/not-zip-safe +0 -0
  42. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite.egg-info/requires.txt +0 -0
  43. {trovesuite-1.0.29 → trovesuite-1.0.31}/src/trovesuite.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trovesuite
3
- Version: 1.0.29
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "trovesuite"
7
- version = "1.0.29"
7
+ version = "1.0.31"
8
8
  description = "TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications"
9
9
  authors = ["brightgclt <brightgclt@gmail.com>"]
10
10
  license = "MIT"
@@ -58,7 +58,7 @@ Documentation = "https://dev.azure.com/brightgclt/trovesuite/_git/packages"
58
58
 
59
59
  [project]
60
60
  name = "trovesuite"
61
- version = "1.0.29"
61
+ version = "1.0.31"
62
62
  description = "TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications"
63
63
  readme = "README.md"
64
64
  license = {text = "MIT"}
@@ -15,7 +15,7 @@ with open("pyproject.toml", "r", encoding="utf-8") as fh:
15
15
 
16
16
  setup(
17
17
  name="trovesuite",
18
- version="1.0.29",
18
+ version="1.0.31",
19
19
  author="Bright Debrah Owusu",
20
20
  author_email="owusu.debrah@deladetech.com",
21
21
  description="TroveSuite services package providing authentication, authorization, notifications, and other enterprise services for TroveSuite applications",
@@ -1,5 +1,20 @@
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
@@ -12,7 +27,7 @@ from .logging import get_logger
12
27
 
13
28
  logger = get_logger("database")
14
29
 
15
- # Database connection pool
30
+ # Database connection pool - created once at startup, never replaced during runtime
16
31
  _connection_pool: Optional[psycopg2.pool.ThreadedConnectionPool] = None
17
32
  _initialization_lock = threading.Lock()
18
33
 
@@ -21,10 +36,14 @@ class DatabaseConfig:
21
36
  """Database configuration and connection management"""
22
37
 
23
38
  def __init__(self):
39
+ import os
24
40
  self.settings = db_settings
25
41
  self.database_url = self.settings.database_url
26
- self.pool_size = 5
27
- 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"))
28
47
 
29
48
  def get_connection_params(self) -> dict:
30
49
  """Get database connection parameters"""
@@ -37,7 +56,7 @@ class DatabaseConfig:
37
56
  "keepalives_idle": 30,
38
57
  "keepalives_interval": 10,
39
58
  "keepalives_count": 5,
40
- "connect_timeout": 10
59
+ "connect_timeout": 30 # Increased timeout for Azure PostgreSQL
41
60
  }
42
61
 
43
62
  # fallback to individual DB_* variables
@@ -59,19 +78,43 @@ class DatabaseConfig:
59
78
  def create_connection_pool(self) -> psycopg2.pool.ThreadedConnectionPool:
60
79
  """Create a connection pool for psycopg2"""
61
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
+
62
86
  pool = psycopg2.pool.ThreadedConnectionPool(
63
87
  minconn=1,
64
- maxconn=self.pool_size,
88
+ maxconn=pool_size,
65
89
  **self.get_connection_params()
66
90
  )
67
- 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
+ )
68
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
69
112
  except Exception as e:
70
113
  logger.error(f"Failed to create database connection pool: {str(e)}")
71
114
  raise
72
115
 
73
116
  def test_connection(self) -> bool:
74
- """Test database connection"""
117
+ """Test database connection (only used at startup)"""
75
118
  try:
76
119
  with psycopg2.connect(**self.get_connection_params()) as conn:
77
120
  with conn.cursor() as cursor:
@@ -91,190 +134,104 @@ db_config = DatabaseConfig()
91
134
 
92
135
 
93
136
  def initialize_database():
94
- """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
+ """
95
144
  global _connection_pool
96
145
 
97
- # Close existing pool if it exists (cleanup before reinitializing)
98
- if _connection_pool is not None:
99
- try:
100
- _connection_pool.closeall()
101
- logger.info("Closed existing connection pool before reinitialization")
102
- except Exception as e:
103
- logger.warning(f"Error closing existing pool: {str(e)}")
104
- _connection_pool = None
105
-
106
- try:
107
- # Test connection first
108
- if not db_config.test_connection():
109
- raise Exception("Database connection test failed")
110
-
111
- # Create connection pool
112
- _connection_pool = db_config.create_connection_pool()
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
113
151
 
114
- # Verify pool was created successfully
115
- if _connection_pool is None:
116
- raise Exception("Connection pool creation returned None")
117
-
118
- logger.info("✅ Database initialization completed successfully")
119
-
120
- except Exception as e:
121
- logger.error(f"❌ Database initialization failed: {str(e)}")
122
- _connection_pool = None # Ensure pool is None on failure
123
- raise
124
-
152
+ try:
153
+ # Test connection first (only at startup)
154
+ if not db_config.test_connection():
155
+ raise Exception("Database connection test failed")
125
156
 
126
- def _is_pool_valid(pool) -> bool:
127
- """Check if the connection pool is valid and usable"""
128
- if pool is None:
129
- return False
130
- try:
131
- # ThreadedConnectionPool doesn't expose a direct "closed" attribute
132
- # Check if pool has the necessary internal structures
133
- if not hasattr(pool, '_pool'):
134
- return False
135
- if pool._pool is None:
136
- return False
137
- # Additional check: verify pool has connection parameters
138
- if not hasattr(pool, '_kwargs'):
139
- return False
140
- return True
141
- except (AttributeError, Exception) as e:
142
- logger.debug(f"Pool validation check: {str(e)}")
143
- return False
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")
144
163
 
164
+ logger.info("✅ Database initialization completed successfully")
145
165
 
146
- def _recover_connection_pool() -> bool:
147
- """Attempt to recover the connection pool with retry logic"""
148
- global _connection_pool, _initialization_lock
149
- import time
150
-
151
- with _initialization_lock:
152
- # Double-check after acquiring lock
153
- if _connection_pool is not None and _is_pool_valid(_connection_pool):
154
- return True
155
-
156
- # Close invalid pool if it exists
157
- if _connection_pool is not None:
158
- try:
159
- _connection_pool.closeall()
160
- logger.info("Closed invalid connection pool")
161
- except Exception as e:
162
- logger.warning(f"Error closing invalid pool: {str(e)}")
163
- _connection_pool = None
164
-
165
- # Retry with exponential backoff
166
- max_retries = 3
167
- base_delay = 1 # Start with 1 second
168
-
169
- for attempt in range(1, max_retries + 1):
170
- try:
171
- logger.warning(f"Attempting to reinitialize connection pool (attempt {attempt}/{max_retries})...")
172
- initialize_database()
173
-
174
- if _connection_pool is not None and _is_pool_valid(_connection_pool):
175
- logger.info(f"✅ Connection pool reinitialized successfully (attempt {attempt})")
176
- return True
177
- else:
178
- logger.warning(f"Pool initialized but validation failed (attempt {attempt})")
179
-
180
- except Exception as e:
181
- logger.error(f"Pool reinitialization attempt {attempt} failed: {str(e)}")
182
- if attempt < max_retries:
183
- delay = base_delay * (2 ** (attempt - 1)) # Exponential backoff: 1s, 2s, 4s
184
- logger.info(f"Retrying in {delay} seconds...")
185
- time.sleep(delay)
186
-
187
- logger.error("❌ Failed to reinitialize connection pool after all retries")
188
- return False
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
189
170
 
190
171
 
191
172
  def get_connection_pool() -> psycopg2.pool.ThreadedConnectionPool:
192
- """Get the database connection pool, with automatic reinitialization if needed"""
173
+ """
174
+ Get the database connection pool.
175
+ Pool must be initialized at startup. This function will raise if pool is None.
176
+ """
193
177
  global _connection_pool
194
178
 
195
- # Fast path: pool exists and is valid
196
- if _connection_pool is not None and _is_pool_valid(_connection_pool):
197
- return _connection_pool
198
-
199
- # Pool is None or invalid, attempt recovery
200
- if not _recover_connection_pool():
179
+ if _connection_pool is None:
201
180
  error_msg = (
202
- "Database connection pool is unavailable. This usually means:\n"
203
- "1. Database server is unreachable or down\n"
204
- "2. Network connectivity issues\n"
205
- "3. Database credentials are incorrect\n"
206
- "4. Connection pool exhausted or closed\n"
207
- "5. Database initialization failed\n"
208
- "Please check the startup logs and database status."
181
+ "Database connection pool is not initialized. "
182
+ "Please ensure initialize_database() was called at application startup."
209
183
  )
210
184
  logger.error(error_msg)
211
185
  raise Exception(error_msg)
212
186
 
213
- if _connection_pool is None:
214
- error_msg = "Connection pool recovery completed but pool is still None"
215
- logger.error(error_msg)
216
- raise Exception(error_msg)
217
-
218
187
  return _connection_pool
219
188
 
220
189
 
221
- def _validate_connection(conn) -> bool:
222
- """Validate if a connection is still alive"""
223
- try:
224
- # Check if connection is closed first
225
- if conn.closed:
226
- return False
227
-
228
- # Test if connection is alive with a simple query
229
- with conn.cursor() as cursor:
230
- cursor.execute("SELECT 1")
231
- cursor.fetchone()
232
- return True
233
- except (psycopg2.OperationalError, psycopg2.InterfaceError, psycopg2.DatabaseError) as e:
234
- logger.warning(f"Connection validation failed: {str(e)}")
235
- return False
236
- except Exception as e:
237
- logger.warning(f"Unexpected error during connection validation: {str(e)}")
238
- return False
239
-
240
-
241
190
  @contextmanager
242
191
  def get_db_connection():
243
- """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
+ """
244
198
  pool = get_connection_pool()
245
199
  conn = None
246
200
  try:
201
+ # Get connection from pool - this will block if pool is exhausted
202
+ # That's the correct behavior - let backpressure happen naturally
247
203
  conn = pool.getconn()
248
-
249
- # Validate connection before using it
250
- if not _validate_connection(conn):
251
- logger.warning("Stale connection detected, getting new connection")
252
- pool.putconn(conn, close=True)
253
- conn = pool.getconn()
254
-
255
204
  logger.debug("Database connection acquired from pool")
256
205
  yield conn
257
206
  except Exception as e:
258
- logger.error(f"Database connection error: {str(e)}")
259
- if conn:
207
+ # If connection exists and isn't closed, rollback transaction
208
+ if conn and not conn.closed:
260
209
  try:
261
- # Only rollback if connection is still open
262
- if not conn.closed:
263
- conn.rollback()
264
- except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
265
- 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
266
214
  raise
267
215
  finally:
216
+ # Always return connection to pool
268
217
  if conn:
269
218
  try:
270
- # If connection is broken, close it instead of returning to pool
271
219
  if conn.closed:
220
+ # If connection is closed, tell pool to close it instead of returning
272
221
  pool.putconn(conn, close=True)
273
222
  else:
223
+ # Return connection to pool normally
274
224
  pool.putconn(conn)
275
225
  logger.debug("Database connection returned to pool")
276
226
  except Exception as put_error:
227
+ # Log error but don't fail - connection will be cleaned up by pool
277
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
278
235
 
279
236
 
280
237
  @contextmanager
@@ -347,6 +304,7 @@ class DatabaseManager:
347
304
  cursor.execute("INSERT INTO table2 ...")
348
305
  # Auto-commits on success, auto-rollbacks on exception
349
306
  """
307
+ # Use get_db_connection() instead of directly accessing pool
350
308
  with get_db_connection() as conn:
351
309
  cursor = conn.cursor(cursor_factory=RealDictCursor)
352
310
  try:
@@ -370,7 +328,10 @@ class DatabaseManager:
370
328
 
371
329
  @staticmethod
372
330
  def health_check() -> dict:
373
- """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
+ """
374
335
  try:
375
336
  with get_db_cursor() as cursor:
376
337
  cursor.execute("SELECT version(), current_database(), current_user")
@@ -406,9 +367,9 @@ class DatabaseManager:
406
367
  }
407
368
 
408
369
 
409
- # NOTE: Database initialization is NOT automatic
370
+ # NOTE: Database initialization is NOT automatic for package version
410
371
  # You must call initialize_database() explicitly in your application startup
411
372
  # Example in FastAPI:
412
373
  # @app.on_event("startup")
413
374
  # async def startup_event():
414
- # initialize_database()
375
+ # initialize_database()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trovesuite
3
- Version: 1.0.29
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
File without changes
File without changes
File without changes
File without changes