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.
- trovesuite/__init__.py +12 -5
- trovesuite/auth/__init__.py +2 -5
- trovesuite/auth/auth_controller.py +6 -5
- trovesuite/auth/auth_read_dto.py +3 -3
- trovesuite/auth/auth_service.py +223 -80
- trovesuite/auth/auth_write_dto.py +1 -1
- trovesuite/configs/database.py +212 -58
- trovesuite/configs/settings.py +75 -132
- trovesuite/entities/health.py +4 -4
- trovesuite/notification/__init__.py +14 -0
- trovesuite/notification/notification_base.py +13 -0
- trovesuite/notification/notification_controller.py +21 -0
- trovesuite/notification/notification_read_dto.py +21 -0
- trovesuite/notification/notification_service.py +73 -0
- trovesuite/notification/notification_write_dto.py +21 -0
- trovesuite/storage/__init__.py +42 -0
- trovesuite/storage/storage_base.py +63 -0
- trovesuite/storage/storage_controller.py +198 -0
- trovesuite/storage/storage_read_dto.py +74 -0
- trovesuite/storage/storage_service.py +529 -0
- trovesuite/storage/storage_write_dto.py +70 -0
- trovesuite/utils/__init__.py +3 -1
- trovesuite/utils/helper.py +714 -5
- trovesuite/utils/templates.py +487 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/METADATA +184 -9
- trovesuite-1.0.31.dist-info/RECORD +34 -0
- trovesuite-1.0.1.dist-info/RECORD +0 -21
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.1.dist-info → trovesuite-1.0.31.dist-info}/top_level.txt +0 -0
trovesuite/configs/database.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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=
|
|
88
|
+
maxconn=pool_size,
|
|
50
89
|
**self.get_connection_params()
|
|
51
90
|
)
|
|
52
|
-
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
|
+
)
|
|
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
|
-
"""
|
|
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
|
-
|
|
83
|
-
#
|
|
84
|
-
if not
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
124
|
-
if conn:
|
|
125
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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.
|
|
244
|
+
if not conn.closed:
|
|
245
|
+
conn.commit()
|
|
141
246
|
except Exception as e:
|
|
142
|
-
conn.
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
221
|
-
#
|
|
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
|
@@ -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
|
-
#
|
|
17
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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()
|
|
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
|
-
#
|
|
33
|
+
# SHARED TABLES (core_platform schema)
|
|
49
34
|
# =============================================================================
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
#
|
|
55
|
+
# CORE PLATFORM TABLES (prefixed with cp_, now in core_platform schema with tenant_id)
|
|
61
56
|
# =============================================================================
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
#
|
|
150
|
-
|
|
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()
|
trovesuite/entities/health.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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
|