trovesuite 1.0.9__py3-none-any.whl → 1.0.10__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 +10 -5
- trovesuite/auth/__init__.py +3 -5
- trovesuite/auth/auth_base.py +1 -0
- trovesuite/auth/auth_controller.py +6 -5
- trovesuite/auth/auth_read_dto.py +1 -1
- trovesuite/auth/auth_service.py +7 -7
- trovesuite/auth/auth_write_dto.py +2 -1
- trovesuite/configs/database.py +130 -36
- trovesuite/configs/settings.py +36 -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-1.0.9.dist-info → trovesuite-1.0.10.dist-info}/METADATA +188 -11
- trovesuite-1.0.10.dist-info/RECORD +33 -0
- trovesuite-1.0.9.dist-info/RECORD +0 -21
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.10.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.10.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.10.dist-info}/top_level.txt +0 -0
trovesuite/__init__.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
|
-
TroveSuite
|
|
2
|
+
TroveSuite Package
|
|
3
3
|
|
|
4
|
-
A comprehensive authentication and
|
|
5
|
-
Provides JWT token validation, user authorization,
|
|
4
|
+
A comprehensive authentication, authorization, notification, and storage service for ERP systems.
|
|
5
|
+
Provides JWT token validation, user authorization, permission checking, notification capabilities,
|
|
6
|
+
and Azure Storage blob management.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from .auth import AuthService
|
|
10
|
+
from .notification import NotificationService
|
|
11
|
+
from .storage import StorageService
|
|
9
12
|
|
|
10
|
-
__version__ = "1.0.
|
|
13
|
+
__version__ = "1.0.7"
|
|
11
14
|
__author__ = "Bright Debrah Owusu"
|
|
12
15
|
__email__ = "owusu.debrah@deladetech.com"
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
15
|
-
"AuthService"
|
|
18
|
+
"AuthService",
|
|
19
|
+
"NotificationService",
|
|
20
|
+
"StorageService"
|
|
16
21
|
]
|
trovesuite/auth/__init__.py
CHANGED
|
@@ -5,12 +5,10 @@ Authentication and authorization services for ERP systems.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .auth_service import AuthService
|
|
8
|
-
from .
|
|
9
|
-
from .auth_read_dto import AuthServiceReadDto, AuthControllerReadDto
|
|
8
|
+
from .auth_write_dto import AuthServiceWriteDto
|
|
10
9
|
|
|
11
10
|
__all__ = [
|
|
12
11
|
"AuthService",
|
|
13
|
-
"
|
|
14
|
-
"AuthServiceReadDto",
|
|
15
|
-
"AuthControllerReadDto"
|
|
12
|
+
"AuthServiceWriteDto"
|
|
16
13
|
]
|
|
14
|
+
|
trovesuite/auth/auth_base.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from
|
|
2
|
+
from .auth_write_dto import AuthControllerWriteDto
|
|
3
|
+
from .auth_read_dto import AuthControllerReadDto
|
|
4
|
+
from .auth_service import AuthService
|
|
5
|
+
from ..entities.sh_response import Respons
|
|
5
6
|
|
|
6
|
-
auth_router = APIRouter()
|
|
7
|
+
auth_router = APIRouter(tags=["Auth"])
|
|
7
8
|
|
|
8
|
-
@auth_router.post("/auth", response_model=AuthControllerReadDto)
|
|
9
|
+
@auth_router.post("/auth", response_model=Respons[AuthControllerReadDto])
|
|
9
10
|
async def authorize(data: AuthControllerWriteDto):
|
|
10
11
|
return AuthService.authorize(data=data)
|
trovesuite/auth/auth_read_dto.py
CHANGED
|
@@ -11,8 +11,8 @@ class AuthControllerReadDto(BaseModel):
|
|
|
11
11
|
role_id: Optional[str] = None
|
|
12
12
|
tenant_id: Optional[str] = None
|
|
13
13
|
permissions: Optional[List[str]] = None
|
|
14
|
-
shared_resource_id: Optional[str] = None
|
|
15
14
|
resource_id: Optional[str] = None
|
|
16
15
|
|
|
17
16
|
class AuthServiceReadDto(AuthControllerReadDto):
|
|
18
17
|
pass
|
|
18
|
+
|
trovesuite/auth/auth_service.py
CHANGED
|
@@ -48,7 +48,7 @@ class AuthService:
|
|
|
48
48
|
|
|
49
49
|
user_id: str = data.user_id
|
|
50
50
|
tenant_id: str = data.tenant_id
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
"""Check if a user is authorized based on login settings and roles"""
|
|
53
53
|
# Input validation
|
|
54
54
|
if not user_id or not isinstance(user_id, str):
|
|
@@ -68,14 +68,14 @@ class AuthService:
|
|
|
68
68
|
status_code=400,
|
|
69
69
|
error="INVALID_TENANT_ID"
|
|
70
70
|
)
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
try:
|
|
73
73
|
|
|
74
74
|
is_tenant_verified = DatabaseManager.execute_query(
|
|
75
75
|
f"SELECT is_verified FROM {db_settings.MAIN_TENANTS_TABLE} WHERE delete_status = 'NOT_DELETED' AND id = %s",
|
|
76
76
|
(tenant_id,),
|
|
77
77
|
)
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
if not is_tenant_verified or len(is_tenant_verified) == 0:
|
|
80
80
|
logger.warning("Login failed - tenant not found: %s", tenant_id)
|
|
81
81
|
return Respons[AuthServiceReadDto](
|
|
@@ -156,7 +156,7 @@ class AuthService:
|
|
|
156
156
|
|
|
157
157
|
# 1️⃣ Get all groups the user belongs to
|
|
158
158
|
user_groups = DatabaseManager.execute_query(
|
|
159
|
-
f"""SELECT group_id FROM "{tenant_id}".{db_settings.
|
|
159
|
+
f"""SELECT group_id FROM "{tenant_id}".{db_settings.TENANT_USER_GROUPS_TABLE}
|
|
160
160
|
WHERE delete_status = 'NOT_DELETED' AND is_active = true AND user_id = %s""",(user_id,),
|
|
161
161
|
)
|
|
162
162
|
|
|
@@ -169,7 +169,7 @@ class AuthService:
|
|
|
169
169
|
f"""
|
|
170
170
|
SELECT DISTINCT ON (org_id, group_id, bus_id, app_id, shared_resource_id, resource_id, user_id, role_id)
|
|
171
171
|
org_id, group_id, bus_id, app_id, shared_resource_id, resource_id, user_id, role_id
|
|
172
|
-
FROM "{tenant_id}".{db_settings.
|
|
172
|
+
FROM "{tenant_id}".{db_settings.TENANT_ASSIGN_ROLES_TABLE}
|
|
173
173
|
WHERE delete_status = 'NOT_DELETED'
|
|
174
174
|
AND is_active = true
|
|
175
175
|
AND (user_id = %s OR group_id = ANY(%s))
|
|
@@ -183,7 +183,7 @@ class AuthService:
|
|
|
183
183
|
f"""
|
|
184
184
|
SELECT DISTINCT ON (org_id, bus_id, app_id, shared_resource_id, resource_id, user_id, role_id)
|
|
185
185
|
org_id, bus_id, app_id, shared_resource_id, resource_id, user_id, role_id
|
|
186
|
-
FROM "{tenant_id}".{db_settings.
|
|
186
|
+
FROM "{tenant_id}".{db_settings.TENANT_ASSIGN_ROLES_TABLE}
|
|
187
187
|
WHERE delete_status = 'NOT_DELETED'
|
|
188
188
|
AND is_active = true
|
|
189
189
|
AND user_id = %s
|
|
@@ -196,7 +196,7 @@ class AuthService:
|
|
|
196
196
|
get_user_roles_with_tenant_and_permissions = []
|
|
197
197
|
for role in get_user_roles:
|
|
198
198
|
permissions = DatabaseManager.execute_query(
|
|
199
|
-
f"""SELECT permission_id FROM {db_settings.
|
|
199
|
+
f"""SELECT permission_id FROM {db_settings.MAIN_ROLE_PERMISSIONS_TABLE} WHERE role_id = %s""",
|
|
200
200
|
params=(role["role_id"],),)
|
|
201
201
|
|
|
202
202
|
role_dict = {**role, "tenant_id": tenant_id, "permissions": [p['permission_id'] for p in permissions]}
|
trovesuite/configs/database.py
CHANGED
|
@@ -13,24 +13,32 @@ logger = get_logger("database")
|
|
|
13
13
|
|
|
14
14
|
# Database connection pool
|
|
15
15
|
_connection_pool: Optional[psycopg2.pool.ThreadedConnectionPool] = None
|
|
16
|
-
_sqlmodel_engine = None
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class DatabaseConfig:
|
|
20
19
|
"""Database configuration and connection management"""
|
|
21
|
-
|
|
20
|
+
|
|
22
21
|
def __init__(self):
|
|
23
22
|
self.settings = db_settings
|
|
23
|
+
self.database_url = self.settings.database_url
|
|
24
24
|
self.pool_size = 5
|
|
25
25
|
self.max_overflow = 10
|
|
26
|
-
|
|
27
|
-
@property
|
|
28
|
-
def database_url(self):
|
|
29
|
-
"""Get database URL (lazy evaluation)"""
|
|
30
|
-
return self.settings.database_url
|
|
31
|
-
|
|
26
|
+
|
|
32
27
|
def get_connection_params(self) -> dict:
|
|
33
28
|
"""Get database connection parameters"""
|
|
29
|
+
if self.settings.DATABASE_URL:
|
|
30
|
+
# Use full DATABASE_URL if available
|
|
31
|
+
return {
|
|
32
|
+
"dsn": self.settings.DATABASE_URL,
|
|
33
|
+
"cursor_factory": RealDictCursor,
|
|
34
|
+
"keepalives": 1,
|
|
35
|
+
"keepalives_idle": 30,
|
|
36
|
+
"keepalives_interval": 10,
|
|
37
|
+
"keepalives_count": 5,
|
|
38
|
+
"connect_timeout": 10
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# fallback to individual DB_* variables
|
|
34
42
|
return {
|
|
35
43
|
"host": self.settings.DB_HOST,
|
|
36
44
|
"port": self.settings.DB_PORT,
|
|
@@ -38,9 +46,14 @@ class DatabaseConfig:
|
|
|
38
46
|
"user": self.settings.DB_USER,
|
|
39
47
|
"password": self.settings.DB_PASSWORD,
|
|
40
48
|
"cursor_factory": RealDictCursor,
|
|
41
|
-
"application_name": f"{self.settings.APP_NAME}_{self.settings.ENVIRONMENT}"
|
|
49
|
+
"application_name": f"{self.settings.APP_NAME}_{self.settings.ENVIRONMENT}",
|
|
50
|
+
"keepalives": 1,
|
|
51
|
+
"keepalives_idle": 30,
|
|
52
|
+
"keepalives_interval": 10,
|
|
53
|
+
"keepalives_count": 5,
|
|
54
|
+
"connect_timeout": 10
|
|
42
55
|
}
|
|
43
|
-
|
|
56
|
+
|
|
44
57
|
def create_connection_pool(self) -> psycopg2.pool.ThreadedConnectionPool:
|
|
45
58
|
"""Create a connection pool for psycopg2"""
|
|
46
59
|
try:
|
|
@@ -54,7 +67,7 @@ class DatabaseConfig:
|
|
|
54
67
|
except Exception as e:
|
|
55
68
|
logger.error(f"Failed to create database connection pool: {str(e)}")
|
|
56
69
|
raise
|
|
57
|
-
|
|
70
|
+
|
|
58
71
|
def test_connection(self) -> bool:
|
|
59
72
|
"""Test database connection"""
|
|
60
73
|
try:
|
|
@@ -78,17 +91,17 @@ db_config = DatabaseConfig()
|
|
|
78
91
|
def initialize_database():
|
|
79
92
|
"""Initialize database connections and pool"""
|
|
80
93
|
global _connection_pool
|
|
81
|
-
|
|
94
|
+
|
|
82
95
|
try:
|
|
83
96
|
# Test connection first
|
|
84
97
|
if not db_config.test_connection():
|
|
85
98
|
raise Exception("Database connection test failed")
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
# Create connection pool
|
|
88
101
|
_connection_pool = db_config.create_connection_pool()
|
|
89
|
-
|
|
102
|
+
|
|
90
103
|
logger.info("Database initialization completed successfully")
|
|
91
|
-
|
|
104
|
+
|
|
92
105
|
except Exception as e:
|
|
93
106
|
logger.error(f"Database initialization failed: {str(e)}")
|
|
94
107
|
raise
|
|
@@ -98,16 +111,28 @@ def get_connection_pool() -> psycopg2.pool.ThreadedConnectionPool:
|
|
|
98
111
|
"""Get the database connection pool"""
|
|
99
112
|
global _connection_pool
|
|
100
113
|
if _connection_pool is None:
|
|
101
|
-
|
|
114
|
+
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."
|
|
121
|
+
)
|
|
122
|
+
logger.error(error_msg)
|
|
123
|
+
raise Exception(error_msg)
|
|
102
124
|
return _connection_pool
|
|
103
125
|
|
|
104
126
|
|
|
105
|
-
def
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
111
136
|
|
|
112
137
|
|
|
113
138
|
@contextmanager
|
|
@@ -117,52 +142,79 @@ def get_db_connection():
|
|
|
117
142
|
conn = None
|
|
118
143
|
try:
|
|
119
144
|
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
|
+
|
|
120
152
|
logger.debug("Database connection acquired from pool")
|
|
121
153
|
yield conn
|
|
122
154
|
except Exception as e:
|
|
123
155
|
logger.error(f"Database connection error: {str(e)}")
|
|
124
156
|
if conn:
|
|
125
|
-
|
|
157
|
+
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)}")
|
|
126
163
|
raise
|
|
127
164
|
finally:
|
|
128
165
|
if conn:
|
|
129
|
-
|
|
130
|
-
|
|
166
|
+
try:
|
|
167
|
+
# If connection is broken, close it instead of returning to pool
|
|
168
|
+
if conn.closed:
|
|
169
|
+
pool.putconn(conn, close=True)
|
|
170
|
+
else:
|
|
171
|
+
pool.putconn(conn)
|
|
172
|
+
logger.debug("Database connection returned to pool")
|
|
173
|
+
except Exception as put_error:
|
|
174
|
+
logger.error(f"Error returning connection to pool: {str(put_error)}")
|
|
131
175
|
|
|
132
176
|
|
|
133
177
|
@contextmanager
|
|
134
178
|
def get_db_cursor():
|
|
135
179
|
"""Get a database cursor (context manager)"""
|
|
136
180
|
with get_db_connection() as conn:
|
|
137
|
-
cursor = conn.cursor()
|
|
181
|
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
|
138
182
|
try:
|
|
139
183
|
yield cursor
|
|
140
|
-
conn.
|
|
184
|
+
if not conn.closed:
|
|
185
|
+
conn.commit()
|
|
141
186
|
except Exception as e:
|
|
142
|
-
conn.
|
|
187
|
+
if not conn.closed:
|
|
188
|
+
try:
|
|
189
|
+
conn.rollback()
|
|
190
|
+
except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
|
|
191
|
+
logger.warning(f"Could not rollback transaction on closed connection: {str(rollback_error)}")
|
|
143
192
|
logger.error(f"Database cursor error: {str(e)}")
|
|
144
193
|
raise
|
|
145
194
|
finally:
|
|
146
|
-
|
|
195
|
+
try:
|
|
196
|
+
cursor.close()
|
|
197
|
+
except Exception as close_error:
|
|
198
|
+
logger.warning(f"Error closing cursor: {str(close_error)}")
|
|
147
199
|
|
|
148
200
|
|
|
149
201
|
class DatabaseManager:
|
|
150
202
|
"""Database manager for common operations"""
|
|
151
|
-
|
|
203
|
+
|
|
152
204
|
@staticmethod
|
|
153
205
|
def execute_query(query: str, params: tuple = None) -> list:
|
|
154
206
|
"""Execute a SELECT query and return results"""
|
|
155
207
|
with get_db_cursor() as cursor:
|
|
156
208
|
cursor.execute(query, params)
|
|
157
209
|
return cursor.fetchall()
|
|
158
|
-
|
|
210
|
+
|
|
159
211
|
@staticmethod
|
|
160
212
|
def execute_update(query: str, params: tuple = None) -> int:
|
|
161
213
|
"""Execute an INSERT/UPDATE/DELETE query and return affected rows"""
|
|
162
214
|
with get_db_cursor() as cursor:
|
|
163
215
|
cursor.execute(query, params)
|
|
164
216
|
return cursor.rowcount
|
|
165
|
-
|
|
217
|
+
|
|
166
218
|
@staticmethod
|
|
167
219
|
def execute_scalar(query: str, params: tuple = None):
|
|
168
220
|
"""Execute a query and return a single value"""
|
|
@@ -178,7 +230,41 @@ class DatabaseManager:
|
|
|
178
230
|
# Handle tuple result
|
|
179
231
|
return result[0] if len(result) > 0 else None
|
|
180
232
|
return None
|
|
181
|
-
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
@contextmanager
|
|
236
|
+
def transaction():
|
|
237
|
+
"""
|
|
238
|
+
Context manager for database transactions.
|
|
239
|
+
Wraps multiple operations in a single transaction.
|
|
240
|
+
|
|
241
|
+
Usage:
|
|
242
|
+
with DatabaseManager.transaction() as cursor:
|
|
243
|
+
cursor.execute("INSERT INTO table1 ...")
|
|
244
|
+
cursor.execute("INSERT INTO table2 ...")
|
|
245
|
+
# Auto-commits on success, auto-rollbacks on exception
|
|
246
|
+
"""
|
|
247
|
+
with get_db_connection() as conn:
|
|
248
|
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
|
249
|
+
try:
|
|
250
|
+
yield cursor
|
|
251
|
+
if not conn.closed:
|
|
252
|
+
conn.commit()
|
|
253
|
+
logger.debug("Transaction committed successfully")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
if not conn.closed:
|
|
256
|
+
try:
|
|
257
|
+
conn.rollback()
|
|
258
|
+
logger.warning(f"Transaction rolled back due to error: {str(e)}")
|
|
259
|
+
except (psycopg2.OperationalError, psycopg2.InterfaceError) as rollback_error:
|
|
260
|
+
logger.error(f"Could not rollback transaction: {str(rollback_error)}")
|
|
261
|
+
raise
|
|
262
|
+
finally:
|
|
263
|
+
try:
|
|
264
|
+
cursor.close()
|
|
265
|
+
except Exception as close_error:
|
|
266
|
+
logger.warning(f"Error closing transaction cursor: {str(close_error)}")
|
|
267
|
+
|
|
182
268
|
@staticmethod
|
|
183
269
|
def health_check() -> dict:
|
|
184
270
|
"""Perform database health check"""
|
|
@@ -186,7 +272,7 @@ class DatabaseManager:
|
|
|
186
272
|
with get_db_cursor() as cursor:
|
|
187
273
|
cursor.execute("SELECT version(), current_database(), current_user")
|
|
188
274
|
result = cursor.fetchone()
|
|
189
|
-
|
|
275
|
+
|
|
190
276
|
if result:
|
|
191
277
|
# Handle RealDictRow (dictionary-like) result
|
|
192
278
|
if hasattr(result, 'get'):
|
|
@@ -217,5 +303,13 @@ class DatabaseManager:
|
|
|
217
303
|
}
|
|
218
304
|
|
|
219
305
|
|
|
220
|
-
# Database initialization
|
|
221
|
-
|
|
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
|
trovesuite/configs/settings.py
CHANGED
|
@@ -1,153 +1,57 @@
|
|
|
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
3
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
# =============================================================================
|
|
11
|
-
DATABASE_URL: str = os.getenv(
|
|
12
|
-
"DATABASE_URL",
|
|
13
|
-
"postgresql://username:password@localhost:5432/database_name"
|
|
14
|
-
)
|
|
4
|
+
# Database URL
|
|
5
|
+
DATABASE_URL: str = os.getenv("DATABASE_URL")
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
DB_PASSWORD: Optional[str] = os.getenv("DB_PASSWORD")
|
|
22
|
-
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development")
|
|
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")
|
|
23
12
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
|
29
|
-
|
|
30
|
-
# =============================================================================
|
|
31
|
-
# SECURITY SETTINGS
|
|
32
|
-
# =============================================================================
|
|
33
|
-
ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
|
|
34
|
-
SECRET_KEY: str = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
|
|
35
|
-
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
|
|
13
|
+
# Application settings
|
|
14
|
+
APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
|
|
15
|
+
DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
|
|
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
|
+
ENVIRONMENT: str = os.getenv("ENVIRONMENT")
|
|
28
|
+
ALGORITHM: str = os.getenv("ALGORITHM")
|
|
29
|
+
SECRET_KEY: str = os.getenv("SECRET_KEY")
|
|
30
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
|
|
31
|
+
|
|
47
32
|
# =============================================================================
|
|
48
|
-
#
|
|
33
|
+
# SHARED TABLES (main schema)
|
|
49
34
|
# =============================================================================
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
TENANT_LOGIN_SETTINGS_TABLE: str = os.getenv("TENANT_LOGIN_SETTINGS_TABLE", "login_settings")
|
|
56
|
-
USER_GROUPS_TABLE: str = os.getenv("USER_GROUPS_TABLE", "user_groups")
|
|
57
|
-
ASSIGN_ROLES_TABLE: str = os.getenv("ASSIGN_ROLES_TABLE", "assign_roles")
|
|
35
|
+
MAIN_TENANTS_TABLE = os.getenv("MAIN_TENANTS_TABLE")
|
|
36
|
+
MAIN_ROLE_PERMISSIONS_TABLE = os.getenv("MAIN_ROLE_PERMISSIONS_TABLE")
|
|
37
|
+
MAIN_USER_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_USER_SUBSCRIPTIONS_TABLE")
|
|
38
|
+
MAIN_USER_SUBSCRIPTION_HISTORY_TABLE = os.getenv("MAIN_USER_SUBSCRIPTION_HISTORY_TABLE")
|
|
39
|
+
MAIN_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_SUBSCRIPTIONS_TABLE")
|
|
58
40
|
|
|
59
41
|
# =============================================================================
|
|
60
|
-
#
|
|
42
|
+
# TENANT-SPECIFIC TABLES (tenant schemas)
|
|
61
43
|
# =============================================================================
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
TENANT_LOGIN_SETTINGS_TABLE = os.getenv("TENANT_LOGIN_SETTINGS_TABLE")
|
|
45
|
+
TENANT_ASSIGN_ROLES_TABLE = os.getenv("TENANT_ASSIGN_ROLES_TABLE")
|
|
46
|
+
TENANT_USER_GROUPS_TABLE = os.getenv("TENANT_USER_GROUPS_TABLE")
|
|
47
|
+
|
|
65
48
|
@property
|
|
66
49
|
def database_url(self) -> str:
|
|
67
|
-
|
|
68
|
-
if self.DATABASE_URL != "postgresql://username:password@localhost:5432/database_name":
|
|
50
|
+
if self.DATABASE_URL:
|
|
69
51
|
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
52
|
|
|
146
|
-
|
|
147
|
-
|
|
53
|
+
port = int(self.DB_PORT) if self.DB_PORT else 5432
|
|
54
|
+
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{port}/{self.DB_NAME}"
|
|
148
55
|
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
db_settings.validate_configuration()
|
|
152
|
-
except Exception as e:
|
|
153
|
-
warnings.warn("Configuration validation failed: %s", str(e), UserWarning)
|
|
56
|
+
# Global settings instance
|
|
57
|
+
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
|