logryn 1.0.0b1__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.
- logryn-1.0.0b1/Logryn/__init__.py +37 -0
- logryn-1.0.0b1/Logryn/db_core.py +553 -0
- logryn-1.0.0b1/Logryn/logger_e.py +67 -0
- logryn-1.0.0b1/Logryn/main_logger.py +725 -0
- logryn-1.0.0b1/Logryn/thread_config_manager.py +247 -0
- logryn-1.0.0b1/PKG-INFO +184 -0
- logryn-1.0.0b1/README.md +171 -0
- logryn-1.0.0b1/logryn.egg-info/PKG-INFO +184 -0
- logryn-1.0.0b1/logryn.egg-info/SOURCES.txt +12 -0
- logryn-1.0.0b1/logryn.egg-info/dependency_links.txt +1 -0
- logryn-1.0.0b1/logryn.egg-info/requires.txt +2 -0
- logryn-1.0.0b1/logryn.egg-info/top_level.txt +1 -0
- logryn-1.0.0b1/pyproject.toml +22 -0
- logryn-1.0.0b1/setup.cfg +4 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced High-Availability Logging Library
|
|
3
|
+
==========================================
|
|
4
|
+
A robust, asynchronous, and thread-safe logging framework designed for
|
|
5
|
+
long-running pipelines. It supports RAM caching, Database batch writing,
|
|
6
|
+
and graceful shutdowns under heavy concurrent loads.
|
|
7
|
+
|
|
8
|
+
Core Components:
|
|
9
|
+
- MainLog: The central async logging orchestrator.
|
|
10
|
+
- project_log_decorator: External wrapper for pipeline functions.
|
|
11
|
+
- ThreadConfigManager: Thread identity and configuration management.
|
|
12
|
+
- setup_global_error_handler: OS-level signal and crash handling.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0b1"
|
|
16
|
+
__author__ = "Elad Segev"
|
|
17
|
+
|
|
18
|
+
# 1. Core Logger & Decorators
|
|
19
|
+
from .main_logger import (
|
|
20
|
+
MainLog,
|
|
21
|
+
project_log_decorator
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 2. Thread Management
|
|
25
|
+
from .thread_config_manager import ThreadConfigManager
|
|
26
|
+
|
|
27
|
+
# 3. System Stability & Crash Handling
|
|
28
|
+
from .logger_e import setup_global_error_handler
|
|
29
|
+
|
|
30
|
+
# 4. Explicit Public API Definition
|
|
31
|
+
__all__ = [
|
|
32
|
+
"MainLog",
|
|
33
|
+
"project_log_decorator",
|
|
34
|
+
"ThreadConfigManager",
|
|
35
|
+
"setup_global_error_handler",
|
|
36
|
+
"__version__"
|
|
37
|
+
]
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, inspect, text
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
@lru_cache(maxsize=32)
|
|
5
|
+
def _get_engine(connection_string: str):
|
|
6
|
+
"""
|
|
7
|
+
Returns a cached SQLAlchemy database engine for the specified connection.
|
|
8
|
+
|
|
9
|
+
The engine is configured with connection health checks and connection
|
|
10
|
+
recycling to improve reliability for long-running database operations.
|
|
11
|
+
|
|
12
|
+
:param connection_string: Database connection string used to create the engine.
|
|
13
|
+
:type connection_string: str
|
|
14
|
+
|
|
15
|
+
:return: SQLAlchemy engine associated with the connection string.
|
|
16
|
+
:rtype: Engine
|
|
17
|
+
"""
|
|
18
|
+
return create_engine(
|
|
19
|
+
connection_string,
|
|
20
|
+
pool_pre_ping=True,
|
|
21
|
+
pool_recycle=3600,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _set_connection_string(database_name:str|None, db_config:dict):
|
|
26
|
+
"""
|
|
27
|
+
Builds a MariaDB connection string from the provided database configuration.
|
|
28
|
+
|
|
29
|
+
:param database_name: Name of the target database. If omitted, the connection
|
|
30
|
+
string points to the MariaDB server without a database.
|
|
31
|
+
:type database_name: str | None
|
|
32
|
+
:param db_config: Dictionary containing the database connection parameters,
|
|
33
|
+
including user, password, host, and port.
|
|
34
|
+
:type db_config: dict
|
|
35
|
+
|
|
36
|
+
:return: MariaDB SQLAlchemy connection string.
|
|
37
|
+
:rtype: str
|
|
38
|
+
"""
|
|
39
|
+
user = db_config.get("user")
|
|
40
|
+
password = db_config.get("password")
|
|
41
|
+
host = db_config.get("host")
|
|
42
|
+
port = db_config.get("port")
|
|
43
|
+
if database_name:
|
|
44
|
+
return f"mariadb+mariadbconnector://{user}:{password}@{host}:{port}/{database_name}"
|
|
45
|
+
return f"mariadb+mariadbconnector://{user}:{password}@{host}:{port}/"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_table_exists(table_name: str, database_name: str, db_config: dict) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Checks whether a table exists in the specified database.
|
|
51
|
+
|
|
52
|
+
:param table_name: Name of the table to check.
|
|
53
|
+
:type table_name: str
|
|
54
|
+
:param database_name: Name of the target database.
|
|
55
|
+
:type database_name: str
|
|
56
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
57
|
+
:type db_config: dict
|
|
58
|
+
|
|
59
|
+
:return: True if the table exists, otherwise False.
|
|
60
|
+
:rtype: bool
|
|
61
|
+
"""
|
|
62
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
63
|
+
inspector = inspect(engine)
|
|
64
|
+
return inspector.has_table(table_name)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_all_tables(database_name: str, db_config: dict) -> list:
|
|
68
|
+
"""
|
|
69
|
+
Retrieves all table names from the specified database.
|
|
70
|
+
|
|
71
|
+
:param database_name: Name of the target database.
|
|
72
|
+
:type database_name: str
|
|
73
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
74
|
+
:type db_config: dict
|
|
75
|
+
|
|
76
|
+
:return: List of table names, or an empty list if the operation fails.
|
|
77
|
+
:rtype: list
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
81
|
+
inspector = inspect(engine)
|
|
82
|
+
|
|
83
|
+
return inspector.get_table_names()
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Failed to fetch tables: {e}")
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_db_exists(database_name: str, db_config: dict) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Checks whether a database exists on the configured MariaDB server.
|
|
93
|
+
|
|
94
|
+
:param database_name: Name of the database to check.
|
|
95
|
+
:type database_name: str
|
|
96
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
97
|
+
:type db_config: dict
|
|
98
|
+
|
|
99
|
+
:return: True if the database exists, otherwise False.
|
|
100
|
+
:rtype: bool
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
engine = _get_engine(_set_connection_string(None, db_config))
|
|
104
|
+
|
|
105
|
+
with engine.connect() as conn:
|
|
106
|
+
query_check = text(f"SHOW DATABASES LIKE '{database_name}'")
|
|
107
|
+
result = conn.execute(query_check)
|
|
108
|
+
db_exists = result.fetchone() is not None
|
|
109
|
+
|
|
110
|
+
return db_exists
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def create_database_(database_name: str, db_config: dict) -> tuple:
|
|
114
|
+
"""
|
|
115
|
+
Creates a database on the configured MariaDB server if it does not already exist.
|
|
116
|
+
|
|
117
|
+
:param database_name: Name of the database to create.
|
|
118
|
+
:type database_name: str
|
|
119
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
120
|
+
:type db_config: dict
|
|
121
|
+
|
|
122
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
123
|
+
:rtype: tuple
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
|
|
127
|
+
if is_db_exists(database_name=database_name, db_config=db_config):
|
|
128
|
+
return True, 'Existing database'
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
|
|
132
|
+
engine = _get_engine(_set_connection_string(None, db_config))
|
|
133
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn_create:
|
|
134
|
+
conn_create.execute(text(f"CREATE DATABASE {database_name}"))
|
|
135
|
+
return True, 'Database created'
|
|
136
|
+
|
|
137
|
+
except Exception as e_create:
|
|
138
|
+
msg = f"Failed to create database '{database_name}': {e_create}"
|
|
139
|
+
return False, msg
|
|
140
|
+
|
|
141
|
+
except Exception as e_conn:
|
|
142
|
+
mag = f"Failed to connect to MariaDB server: {e_conn}"
|
|
143
|
+
return False, mag
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_table_(table_name: str, database_name: str, db_config: dict, create_query: str) -> tuple:
|
|
147
|
+
"""
|
|
148
|
+
Creates a table in the specified database if it does not already exist.
|
|
149
|
+
|
|
150
|
+
:param table_name: Name of the table to create.
|
|
151
|
+
:type table_name: str
|
|
152
|
+
:param database_name: Name of the target database.
|
|
153
|
+
:type database_name: str
|
|
154
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
155
|
+
:type db_config: dict
|
|
156
|
+
:param create_query: SQL statement used to create the table.
|
|
157
|
+
:type create_query: str
|
|
158
|
+
|
|
159
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
160
|
+
:rtype: tuple
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
exists_status = is_table_exists(table_name, database_name, db_config)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
msg = f"Failed to connect or check table: {e}"
|
|
167
|
+
return False, msg
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if exists_status is True:
|
|
171
|
+
return True, 'Existing table'
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
175
|
+
|
|
176
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
177
|
+
conn.execute(text(create_query))
|
|
178
|
+
|
|
179
|
+
return True, 'Table created'
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
mag = f"Failed to create table '{table_name}': {e}"
|
|
183
|
+
return False, mag
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def insert_row_(data_dict: dict, table_name: str, database_name: str, db_config: dict) -> tuple:
|
|
187
|
+
"""
|
|
188
|
+
Inserts a row into the specified database table.
|
|
189
|
+
|
|
190
|
+
:param data_dict: Dictionary containing column names and their corresponding
|
|
191
|
+
values for the row to insert.
|
|
192
|
+
:type data_dict: dict
|
|
193
|
+
:param table_name: Name of the target table.
|
|
194
|
+
:type table_name: str
|
|
195
|
+
:param database_name: Name of the target database.
|
|
196
|
+
:type database_name: str
|
|
197
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
198
|
+
:type db_config: dict
|
|
199
|
+
|
|
200
|
+
:return: Tuple containing a success flag and the inserted row ID or an error
|
|
201
|
+
message.
|
|
202
|
+
:rtype: tuple
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
206
|
+
|
|
207
|
+
columns = ", ".join(data_dict.keys())
|
|
208
|
+
placeholders = ", ".join([f":{k}" for k in data_dict.keys()])
|
|
209
|
+
query_string = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
|
210
|
+
|
|
211
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
212
|
+
result = conn.execute(text(query_string), data_dict)
|
|
213
|
+
inserted_id = result.lastrowid
|
|
214
|
+
|
|
215
|
+
return True, inserted_id
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
msg = f"Failed to insert row into '{table_name}': {e}"
|
|
219
|
+
return False, msg
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def insert_rows_batch_(data_list: list[dict], table_name: str, database_name: str, db_config: dict) -> tuple:
|
|
223
|
+
"""
|
|
224
|
+
Inserts multiple rows into the specified database table as a batch operation.
|
|
225
|
+
|
|
226
|
+
:param data_list: List of dictionaries containing column names and their
|
|
227
|
+
corresponding values for each row.
|
|
228
|
+
:type data_list: list[dict]
|
|
229
|
+
:param table_name: Name of the target table.
|
|
230
|
+
:type table_name: str
|
|
231
|
+
:param database_name: Name of the target database.
|
|
232
|
+
:type database_name: str
|
|
233
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
234
|
+
:type db_config: dict
|
|
235
|
+
|
|
236
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
237
|
+
:rtype: tuple
|
|
238
|
+
"""
|
|
239
|
+
if not data_list:
|
|
240
|
+
return True, 'No data to insert'
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
244
|
+
|
|
245
|
+
columns = ", ".join(data_list[0].keys())
|
|
246
|
+
placeholders = ", ".join([f":{k}" for k in data_list[0].keys()])
|
|
247
|
+
query_string = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
|
248
|
+
|
|
249
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
250
|
+
conn.execute(text(query_string), data_list)
|
|
251
|
+
|
|
252
|
+
return True, f'{len(data_list)} rows inserted successfully'
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
msg = f"Failed to insert batch into '{table_name}': {e}"
|
|
256
|
+
return False, msg
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def drop_table_(table_name: str, database_name: str, db_config: dict) -> tuple:
|
|
260
|
+
"""
|
|
261
|
+
Drops the specified table from the database if it exists.
|
|
262
|
+
|
|
263
|
+
:param table_name: Name of the table to drop.
|
|
264
|
+
:type table_name: str
|
|
265
|
+
:param database_name: Name of the target database.
|
|
266
|
+
:type database_name: str
|
|
267
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
268
|
+
:type db_config: dict
|
|
269
|
+
|
|
270
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
271
|
+
:rtype: tuple
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
275
|
+
|
|
276
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
277
|
+
conn.execute(text(f"DROP TABLE IF EXISTS {table_name}"))
|
|
278
|
+
|
|
279
|
+
return True, f"Table '{table_name}' dropped successfully"
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
msg = f"Failed to drop table '{table_name}': {e}"
|
|
283
|
+
return False, msg
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def drop_database_(database_name: str, db_config: dict) -> tuple:
|
|
287
|
+
"""
|
|
288
|
+
Drops the specified database if it exists.
|
|
289
|
+
|
|
290
|
+
:param database_name: Name of the database to drop.
|
|
291
|
+
:type database_name: str
|
|
292
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
293
|
+
:type db_config: dict
|
|
294
|
+
|
|
295
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
296
|
+
:rtype: tuple
|
|
297
|
+
"""
|
|
298
|
+
if not is_db_exists(database_name, db_config):
|
|
299
|
+
return False, f"The database '{database_name}' doesn't exsist "
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
303
|
+
|
|
304
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
305
|
+
conn.execute(text(f"DROP DATABASE IF EXISTS {database_name}"))
|
|
306
|
+
|
|
307
|
+
return True, f"Database '{database_name}' dropped successfully"
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
msg = f"Failed to drop database '{database_name}': {e}"
|
|
311
|
+
return False, msg
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def alter_table_(table_name: str, database_name: str, db_config: dict, alter_query: str) -> tuple:
|
|
315
|
+
"""
|
|
316
|
+
Alters the specified table using the provided SQL statement.
|
|
317
|
+
|
|
318
|
+
:param table_name: Name of the table to alter.
|
|
319
|
+
:type table_name: str
|
|
320
|
+
:param database_name: Name of the target database.
|
|
321
|
+
:type database_name: str
|
|
322
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
323
|
+
:type db_config: dict
|
|
324
|
+
:param alter_query: SQL statement used to alter the table.
|
|
325
|
+
:type alter_query: str
|
|
326
|
+
|
|
327
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
328
|
+
:rtype: tuple
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
332
|
+
|
|
333
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
334
|
+
conn.execute(text(alter_query))
|
|
335
|
+
|
|
336
|
+
return True, f"Table '{table_name}' altered successfully"
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
msg = f"Failed to alter table '{table_name}': {e}"
|
|
340
|
+
return False, msg
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_row_count_(table_name: str, database_name: str, db_config: dict) -> int:
|
|
344
|
+
"""
|
|
345
|
+
Returns the number of rows in the specified database table.
|
|
346
|
+
|
|
347
|
+
:param table_name: Name of the table to count rows from.
|
|
348
|
+
:type table_name: str
|
|
349
|
+
:param database_name: Name of the target database.
|
|
350
|
+
:type database_name: str
|
|
351
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
352
|
+
:type db_config: dict
|
|
353
|
+
|
|
354
|
+
:return: Number of rows in the table, or 0 if the operation fails.
|
|
355
|
+
:rtype: int
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
359
|
+
|
|
360
|
+
with engine.connect() as conn:
|
|
361
|
+
query = text(f"SELECT COUNT(*) FROM {table_name}")
|
|
362
|
+
result = conn.execute(query)
|
|
363
|
+
count = result.scalar()
|
|
364
|
+
|
|
365
|
+
return int(count) if count is not None else 0
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
print(f"Failed to get row count for '{table_name}': {e}")
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def delete_table_(table_name: str, database_name: str, db_config: dict) -> tuple:
|
|
373
|
+
"""
|
|
374
|
+
Deletes all rows from the specified database table.
|
|
375
|
+
|
|
376
|
+
:param table_name: Name of the table to clear.
|
|
377
|
+
:type table_name: str
|
|
378
|
+
:param database_name: Name of the target database.
|
|
379
|
+
:type database_name: str
|
|
380
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
381
|
+
:type db_config: dict
|
|
382
|
+
|
|
383
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
384
|
+
:rtype: tuple
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
388
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
389
|
+
conn.execute(text(f"DELETE FROM {table_name}"))
|
|
390
|
+
return True, f"Table '{table_name}' cleared successfully"
|
|
391
|
+
except Exception as e:
|
|
392
|
+
return False, f"Failed to clear table '{table_name}': {e}"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def upsert_rows_batch_(data_list: list[dict], table_name: str, database_name: str, db_config: dict) -> tuple:
|
|
396
|
+
"""
|
|
397
|
+
Inserts multiple rows into the specified database table and updates existing
|
|
398
|
+
rows when a duplicate key is encountered.
|
|
399
|
+
|
|
400
|
+
:param data_list: List of dictionaries containing column names and their
|
|
401
|
+
corresponding values for each row.
|
|
402
|
+
:type data_list: list[dict]
|
|
403
|
+
:param table_name: Name of the target table.
|
|
404
|
+
:type table_name: str
|
|
405
|
+
:param database_name: Name of the target database.
|
|
406
|
+
:type database_name: str
|
|
407
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
408
|
+
:type db_config: dict
|
|
409
|
+
|
|
410
|
+
:return: Tuple containing a success flag and a status or error message.
|
|
411
|
+
:rtype: tuple
|
|
412
|
+
"""
|
|
413
|
+
if not data_list:
|
|
414
|
+
return True, 'No data to upsert'
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
418
|
+
|
|
419
|
+
columns = ", ".join(data_list[0].keys())
|
|
420
|
+
placeholders = ", ".join([f":{k}" for k in data_list[0].keys()])
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
update_cols = [k for k in data_list[0].keys() if k != 'station_id']
|
|
424
|
+
update_clause = ", ".join([f"{k} = VALUES({k})" for k in update_cols])
|
|
425
|
+
|
|
426
|
+
query_string = f"""
|
|
427
|
+
INSERT INTO {table_name} ({columns})
|
|
428
|
+
VALUES ({placeholders})
|
|
429
|
+
ON DUPLICATE KEY UPDATE {update_clause}
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
with engine.execution_options(isolation_level="AUTOCOMMIT").connect() as conn:
|
|
433
|
+
conn.execute(text(query_string), data_list)
|
|
434
|
+
|
|
435
|
+
return True, f'{len(data_list)} rows upserted successfully'
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
msg = f"Failed to upsert batch into '{table_name}': {e}"
|
|
439
|
+
return False, msg
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def select_(table_name: str, database_name: str, db_config: dict, where_dict: str | None = None, logical_op: str = "AND") -> tuple:
|
|
443
|
+
"""
|
|
444
|
+
Retrieves rows from the specified database table with an optional filter.
|
|
445
|
+
|
|
446
|
+
:param table_name: Name of the table to query.
|
|
447
|
+
:type table_name: str
|
|
448
|
+
:param database_name: Name of the target database.
|
|
449
|
+
:type database_name: str
|
|
450
|
+
:param db_config: Dictionary containing the database connection parameters.
|
|
451
|
+
:type db_config: dict
|
|
452
|
+
:param where_dict: Optional filter definition used to build the SQL WHERE
|
|
453
|
+
clause.
|
|
454
|
+
:type where_dict: str | None
|
|
455
|
+
:param logical_op: Logical operator used to combine multiple filter
|
|
456
|
+
conditions.
|
|
457
|
+
:type logical_op: str
|
|
458
|
+
|
|
459
|
+
:return: Tuple containing a success flag and the retrieved rows or an error
|
|
460
|
+
message.
|
|
461
|
+
:rtype: tuple
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
query_str = f"SELECT * FROM {table_name}"
|
|
465
|
+
|
|
466
|
+
where_string, query_params = _build_where_clause(where_dict, logical_op)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if where_string:
|
|
470
|
+
query_str += f" WHERE {where_string}"
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
474
|
+
with engine.connect() as conn:
|
|
475
|
+
result = conn.execute(text(query_str), query_params)
|
|
476
|
+
rows = result.fetchall()
|
|
477
|
+
|
|
478
|
+
return True, rows
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
return False, str(e)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _build_where_clause(conditions: dict | None, logical_op: str = "AND") -> tuple[str, dict]:
|
|
485
|
+
"""
|
|
486
|
+
Constructs a parameterized SQL WHERE clause from a dictionary of conditions.
|
|
487
|
+
|
|
488
|
+
The function converts each condition into a SQL comparison using named
|
|
489
|
+
placeholders and stores the corresponding values separately for safe query
|
|
490
|
+
execution. Conditions may use the default equality operator or explicitly
|
|
491
|
+
specify a supported SQL operator.
|
|
492
|
+
|
|
493
|
+
:param conditions: Dictionary mapping column names to values or
|
|
494
|
+
``(operator, value)`` tuples.
|
|
495
|
+
:type conditions: dict | None
|
|
496
|
+
:param logical_op: Logical operator used to combine multiple conditions.
|
|
497
|
+
Supported values are ``"AND"`` and ``"OR"``. Defaults to
|
|
498
|
+
``"AND"``.
|
|
499
|
+
:type logical_op: str
|
|
500
|
+
|
|
501
|
+
:return: A tuple containing the generated WHERE clause string and a
|
|
502
|
+
dictionary of parameter values. Returns an empty clause and
|
|
503
|
+
parameter dictionary when no conditions are provided or the logical
|
|
504
|
+
operator is invalid.
|
|
505
|
+
:rtype: tuple[str, dict]
|
|
506
|
+
|
|
507
|
+
:raises ValueError: If a condition specifies an unsupported SQL operator.
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
if not conditions:
|
|
511
|
+
return "", {}
|
|
512
|
+
|
|
513
|
+
if logical_op.lower not in ['and','or']:
|
|
514
|
+
return "", {}
|
|
515
|
+
|
|
516
|
+
ALLOWED_OPERATORS = {"=", "!=", ">", "<", ">=", "<=", "LIKE", "IN"}
|
|
517
|
+
|
|
518
|
+
clauses = []
|
|
519
|
+
params = {}
|
|
520
|
+
|
|
521
|
+
for i, (col_name, value) in enumerate(conditions.items()):
|
|
522
|
+
op = "="
|
|
523
|
+
val = value
|
|
524
|
+
|
|
525
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
526
|
+
op, val = value
|
|
527
|
+
op = str(op).upper()
|
|
528
|
+
if op not in ALLOWED_OPERATORS:
|
|
529
|
+
raise ValueError(f"Security Alert: Unsupported SQL operator '{op}'")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
placeholder = f"{col_name}_{i}"
|
|
533
|
+
|
|
534
|
+
clauses.append(f"{col_name} {op} :{placeholder}")
|
|
535
|
+
|
|
536
|
+
params[placeholder] = val
|
|
537
|
+
|
|
538
|
+
where_string = f" {logical_op.upper()} ".join(clauses)
|
|
539
|
+
return where_string, params
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def get_table_columns_(table_name: str, database_name: str, db_config: dict) -> list:
|
|
544
|
+
try:
|
|
545
|
+
engine = _get_engine(_set_connection_string(database_name, db_config))
|
|
546
|
+
inspector = inspect(engine)
|
|
547
|
+
|
|
548
|
+
columns = inspector.get_columns(table_name)
|
|
549
|
+
|
|
550
|
+
return True, [col['name'] for col in columns]
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
return False, f"Failed to fetch columns for '{table_name}': {e}"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
import asyncio
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def setup_global_error_handler(logger_instance, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
8
|
+
"""
|
|
9
|
+
Configures global handlers for signals, uncaught exceptions, and thread
|
|
10
|
+
exceptions to ensure that process errors are logged and the logger is
|
|
11
|
+
properly flushed before shutdown.
|
|
12
|
+
|
|
13
|
+
:param logger_instance: Logger instance used to record errors and shutdown
|
|
14
|
+
events.
|
|
15
|
+
:type logger_instance: Any
|
|
16
|
+
:param loop: Asyncio event loop used to schedule logger shutdown and signal
|
|
17
|
+
handling. Defaults to the current event loop.
|
|
18
|
+
:type loop: asyncio.AbstractEventLoop | None
|
|
19
|
+
|
|
20
|
+
:return: None.
|
|
21
|
+
:rtype: None
|
|
22
|
+
"""
|
|
23
|
+
loop = loop or asyncio.get_event_loop()
|
|
24
|
+
|
|
25
|
+
def _flush_and_stop(details: str):
|
|
26
|
+
async def _do():
|
|
27
|
+
logger_instance.write(
|
|
28
|
+
event={'process': 'System Interruption', 'type': 'User/System Kill',
|
|
29
|
+
'parameters': 'Signal received', 'thread_id': None},
|
|
30
|
+
scenario='error', details=details,
|
|
31
|
+
)
|
|
32
|
+
await logger_instance.shutdown() # the piece that was never called
|
|
33
|
+
loop.stop()
|
|
34
|
+
asyncio.ensure_future(_do(), loop=loop)
|
|
35
|
+
|
|
36
|
+
def signal_handler():
|
|
37
|
+
_flush_and_stop("Signal received. Flushing queue before exit.")
|
|
38
|
+
|
|
39
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
40
|
+
try:
|
|
41
|
+
loop.add_signal_handler(sig, signal_handler) # POSIX: safe, runs on the loop
|
|
42
|
+
except NotImplementedError:
|
|
43
|
+
signal.signal(sig, lambda s, f: signal_handler()) # Windows fallback
|
|
44
|
+
|
|
45
|
+
def exception_handler(exc_type, exc_value, exc_traceback):
|
|
46
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
47
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
48
|
+
return
|
|
49
|
+
logger_instance.write(
|
|
50
|
+
event={'process': 'Fatal Crash', 'type': 'Unhandled Exception',
|
|
51
|
+
'parameters': f"Type: {exc_type.__name__}", 'thread_id': None},
|
|
52
|
+
scenario='error', details=f"CRASH LOGIC: {exc_value}",
|
|
53
|
+
local_start_time=datetime.now(),
|
|
54
|
+
)
|
|
55
|
+
asyncio.ensure_future(logger_instance.shutdown(), loop=loop)
|
|
56
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
57
|
+
|
|
58
|
+
def thread_exception_handler(args: threading.ExceptHookArgs):
|
|
59
|
+
logger_instance.write(
|
|
60
|
+
event={'process': f"Thread Crash [{args.thread.name}]",
|
|
61
|
+
'type': 'Unhandled Thread Exception',
|
|
62
|
+
'parameters': f"Type: {args.exc_type.__name__}", 'thread_id': None},
|
|
63
|
+
scenario='error', details=str(args.exc_value),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
sys.excepthook = exception_handler
|
|
67
|
+
threading.excepthook = thread_exception_handler # <-- previously silent
|