mdbq 3.10.10__py3-none-any.whl → 3.10.12__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.
- mdbq/__version__.py +1 -1
- mdbq/mysql/deduplicator.py +177 -53
- mdbq/mysql/uploader.py +263 -327
- {mdbq-3.10.10.dist-info → mdbq-3.10.12.dist-info}/METADATA +1 -1
- {mdbq-3.10.10.dist-info → mdbq-3.10.12.dist-info}/RECORD +7 -8
- mdbq/aggregation/optimize.py +0 -475
- {mdbq-3.10.10.dist-info → mdbq-3.10.12.dist-info}/WHEEL +0 -0
- {mdbq-3.10.10.dist-info → mdbq-3.10.12.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '3.10.
|
1
|
+
VERSION = '3.10.12'
|
mdbq/mysql/deduplicator.py
CHANGED
@@ -12,6 +12,7 @@ import threading
|
|
12
12
|
import concurrent.futures
|
13
13
|
from collections import defaultdict
|
14
14
|
import sys
|
15
|
+
from datetime import datetime
|
15
16
|
|
16
17
|
|
17
18
|
warnings.filterwarnings('ignore')
|
@@ -77,15 +78,18 @@ class MySQLDeduplicator:
|
|
77
78
|
date_range: Optional[List[str]] = None,
|
78
79
|
recent_month: Optional[int] = None,
|
79
80
|
date_column: str = '日期',
|
80
|
-
exclude_columns: Optional[List[str]] = None
|
81
|
+
exclude_columns: Optional[List[str]] = None,
|
82
|
+
exclude_databases: Optional[List[str]] = None,
|
83
|
+
exclude_tables: Optional[Dict[str, List[str]]] = None
|
81
84
|
) -> None:
|
82
85
|
"""
|
83
86
|
初始化去重处理器
|
84
|
-
新增参数:
|
85
87
|
:param date_range: 指定去重的日期区间 [start_date, end_date],格式'YYYY-MM-DD'
|
86
88
|
:param recent_month: 最近N个月的数据去重(与date_range互斥,优先生效)
|
87
89
|
:param date_column: 时间列名,默认为'日期'
|
88
90
|
:param exclude_columns: 去重时排除的列名列表,默认为['id', '更新时间']
|
91
|
+
:param exclude_databases: 排除的数据库名列表
|
92
|
+
:param exclude_tables: 排除的表名字典 {数据库名: [表名, ...]}
|
89
93
|
"""
|
90
94
|
# 连接池状态标志
|
91
95
|
self._closed = False
|
@@ -127,11 +131,28 @@ class MySQLDeduplicator:
|
|
127
131
|
self.exclude_columns = list(default_exclude | {'更新时间'})
|
128
132
|
else:
|
129
133
|
self.exclude_columns = list(set(exclude_columns) | default_exclude)
|
130
|
-
#
|
134
|
+
# 解析时间范围并智能校正date_range
|
131
135
|
if self.date_range and len(self.date_range) == 2:
|
132
|
-
|
136
|
+
try:
|
137
|
+
start, end = self.date_range
|
138
|
+
start_dt = datetime.strptime(start, "%Y-%m-%d")
|
139
|
+
end_dt = datetime.strptime(end, "%Y-%m-%d")
|
140
|
+
if start_dt > end_dt:
|
141
|
+
logger.warning(
|
142
|
+
"date_range顺序不正确,自动交换开始和结束日期。",
|
143
|
+
{"start": start, "end": end}
|
144
|
+
)
|
145
|
+
start_dt, end_dt = end_dt, start_dt
|
146
|
+
self._dedup_start_date = start_dt.strftime("%Y-%m-%d")
|
147
|
+
self._dedup_end_date = end_dt.strftime("%Y-%m-%d")
|
148
|
+
except Exception as e:
|
149
|
+
logger.error(
|
150
|
+
"date_range参数格式错误,应为['YYYY-MM-DD', 'YYYY-MM-DD'],已忽略时间范围。",
|
151
|
+
{"date_range": self.date_range, "error": str(e)}
|
152
|
+
)
|
153
|
+
self._dedup_start_date = None
|
154
|
+
self._dedup_end_date = None
|
133
155
|
elif self.recent_month:
|
134
|
-
from datetime import datetime, timedelta
|
135
156
|
today = datetime.today()
|
136
157
|
month = today.month - self.recent_month
|
137
158
|
year = today.year
|
@@ -148,8 +169,19 @@ class MySQLDeduplicator:
|
|
148
169
|
# 系统数据库列表
|
149
170
|
self.SYSTEM_DATABASES = {'information_schema', 'mysql', 'performance_schema', 'sys'}
|
150
171
|
|
172
|
+
# 排除数据库和表的逻辑
|
173
|
+
self.exclude_databases = set([db.lower() for db in exclude_databases]) if exclude_databases else set()
|
174
|
+
self.exclude_tables = {k.lower(): set([t.lower() for t in v]) for k, v in (exclude_tables or {}).items()}
|
175
|
+
|
151
176
|
def _get_connection(self) -> pymysql.connections.Connection:
|
152
|
-
"""
|
177
|
+
"""
|
178
|
+
从连接池获取一个数据库连接。
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
pymysql.connections.Connection: 数据库连接对象。
|
182
|
+
Raises:
|
183
|
+
ConnectionError: 如果连接池已关闭或获取连接失败。
|
184
|
+
"""
|
153
185
|
if self._closed:
|
154
186
|
logger.error('尝试获取连接但连接池已关闭')
|
155
187
|
raise ConnectionError("连接池已关闭")
|
@@ -163,7 +195,17 @@ class MySQLDeduplicator:
|
|
163
195
|
|
164
196
|
@staticmethod
|
165
197
|
def _retry_on_failure(func: Any) -> Any:
|
166
|
-
"""
|
198
|
+
"""
|
199
|
+
装饰器:为数据库操作方法提供自动重试机制。
|
200
|
+
仅捕获pymysql的连接相关异常,重试指定次数后抛出最后一次异常。
|
201
|
+
|
202
|
+
Args:
|
203
|
+
func (Any): 被装饰的函数。
|
204
|
+
Returns:
|
205
|
+
Any: 被装饰函数的返回值。
|
206
|
+
Raises:
|
207
|
+
Exception: 多次重试后依然失败时抛出。
|
208
|
+
"""
|
167
209
|
@wraps(func)
|
168
210
|
def wrapper(self, *args, **kwargs):
|
169
211
|
last_exception = None
|
@@ -192,21 +234,31 @@ class MySQLDeduplicator:
|
|
192
234
|
|
193
235
|
@_retry_on_failure
|
194
236
|
def _get_databases(self) -> List[str]:
|
195
|
-
"""
|
237
|
+
"""
|
238
|
+
获取所有非系统数据库列表,排除exclude_databases。
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
List[str]: 数据库名列表。
|
242
|
+
"""
|
196
243
|
sql = "SHOW DATABASES"
|
197
|
-
|
198
244
|
with self._get_connection() as conn:
|
199
245
|
with conn.cursor() as cursor:
|
200
246
|
cursor.execute(sql)
|
201
247
|
all_dbs = [row['Database'] for row in cursor.fetchall()]
|
202
|
-
|
203
|
-
if self.skip_system_dbs
|
204
|
-
|
205
|
-
return all_dbs
|
248
|
+
# 排除系统库和用户指定的库
|
249
|
+
filtered = [db for db in all_dbs if db.lower() not in self.SYSTEM_DATABASES and db.lower() not in self.exclude_databases] if self.skip_system_dbs else [db for db in all_dbs if db.lower() not in self.exclude_databases]
|
250
|
+
return filtered
|
206
251
|
|
207
252
|
@_retry_on_failure
|
208
253
|
def _get_tables(self, database: str) -> List[str]:
|
209
|
-
"""
|
254
|
+
"""
|
255
|
+
获取指定数据库的所有表名。
|
256
|
+
|
257
|
+
Args:
|
258
|
+
database (str): 数据库名。
|
259
|
+
Returns:
|
260
|
+
List[str]: 表名列表。
|
261
|
+
"""
|
210
262
|
sql = "SHOW TABLES"
|
211
263
|
|
212
264
|
with self._get_connection() as conn:
|
@@ -217,7 +269,15 @@ class MySQLDeduplicator:
|
|
217
269
|
|
218
270
|
@_retry_on_failure
|
219
271
|
def _get_table_columns(self, database: str, table: str) -> List[str]:
|
220
|
-
"""
|
272
|
+
"""
|
273
|
+
获取指定表的所有列名(排除主键列)。
|
274
|
+
|
275
|
+
Args:
|
276
|
+
database (str): 数据库名。
|
277
|
+
table (str): 表名。
|
278
|
+
Returns:
|
279
|
+
List[str]: 列名列表。
|
280
|
+
"""
|
221
281
|
sql = """
|
222
282
|
SELECT COLUMN_NAME
|
223
283
|
FROM INFORMATION_SCHEMA.COLUMNS
|
@@ -232,7 +292,15 @@ class MySQLDeduplicator:
|
|
232
292
|
if row['COLUMN_NAME'].lower() != self.primary_key.lower()]
|
233
293
|
|
234
294
|
def _acquire_table_lock(self, database: str, table: str) -> bool:
|
235
|
-
"""
|
295
|
+
"""
|
296
|
+
获取表处理锁,防止并发处理同一张表。
|
297
|
+
|
298
|
+
Args:
|
299
|
+
database (str): 数据库名。
|
300
|
+
table (str): 表名。
|
301
|
+
Returns:
|
302
|
+
bool: 是否成功获取锁。
|
303
|
+
"""
|
236
304
|
key = f"{database}.{table}"
|
237
305
|
|
238
306
|
with self._lock:
|
@@ -243,7 +311,13 @@ class MySQLDeduplicator:
|
|
243
311
|
return True
|
244
312
|
|
245
313
|
def _release_table_lock(self, database: str, table: str) -> None:
|
246
|
-
"""
|
314
|
+
"""
|
315
|
+
释放表处理锁。
|
316
|
+
|
317
|
+
Args:
|
318
|
+
database (str): 数据库名。
|
319
|
+
table (str): 表名。
|
320
|
+
"""
|
247
321
|
key = f"{database}.{table}"
|
248
322
|
|
249
323
|
with self._lock:
|
@@ -258,13 +332,15 @@ class MySQLDeduplicator:
|
|
258
332
|
dry_run: bool = False
|
259
333
|
) -> Tuple[int, int]:
|
260
334
|
"""
|
261
|
-
|
262
|
-
|
263
|
-
:
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
335
|
+
执行单表去重。
|
336
|
+
|
337
|
+
Args:
|
338
|
+
database (str): 数据库名。
|
339
|
+
table (str): 表名。
|
340
|
+
columns (Optional[List[str]]): 用于去重的列名列表(为None时使用所有列)。
|
341
|
+
dry_run (bool): 是否为模拟运行(只统计不实际删除)。
|
342
|
+
Returns:
|
343
|
+
Tuple[int, int]: (重复组数, 实际删除行数)。
|
268
344
|
"""
|
269
345
|
if not self._acquire_table_lock(database, table):
|
270
346
|
return (0, 0)
|
@@ -385,14 +461,19 @@ class MySQLDeduplicator:
|
|
385
461
|
dry_run: bool = False
|
386
462
|
) -> Tuple[int, int]:
|
387
463
|
"""
|
388
|
-
|
389
|
-
|
390
|
-
:
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
464
|
+
对指定表进行去重。
|
465
|
+
|
466
|
+
Args:
|
467
|
+
database (str): 数据库名。
|
468
|
+
table (str): 表名。
|
469
|
+
columns (Optional[List[str]]): 用于去重的列名列表(为None时使用所有列)。
|
470
|
+
dry_run (bool): 是否为模拟运行(只统计不实际删除)。
|
471
|
+
Returns:
|
472
|
+
Tuple[int, int]: (重复组数, 实际删除行数)。
|
395
473
|
"""
|
474
|
+
if database.lower() in self.exclude_tables and table.lower() in self.exclude_tables[database.lower()]:
|
475
|
+
logger.info('表被排除', {"库": database, "表": table, "操作": "跳过"})
|
476
|
+
return (0, 0)
|
396
477
|
try:
|
397
478
|
if not self._check_table_exists(database, table):
|
398
479
|
logger.warning('表不存在', {"库": database, "表": table, "warning": "跳过"})
|
@@ -414,14 +495,16 @@ class MySQLDeduplicator:
|
|
414
495
|
parallel: bool = False
|
415
496
|
) -> Dict[str, Tuple[int, int]]:
|
416
497
|
"""
|
417
|
-
|
418
|
-
|
419
|
-
:
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
498
|
+
对指定数据库的所有表进行去重。
|
499
|
+
|
500
|
+
Args:
|
501
|
+
database (str): 数据库名。
|
502
|
+
tables (Optional[List[str]]): 要处理的表列表(为None时处理所有表)。
|
503
|
+
columns_map (Optional[Dict[str, List[str]]]): 各表使用的去重列 {表名: [列名]}。
|
504
|
+
dry_run (bool): 是否为模拟运行。
|
505
|
+
parallel (bool): 是否并行处理。
|
506
|
+
Returns:
|
507
|
+
Dict[str, Tuple[int, int]]: {表名: (重复组数, 实际删除行数)}。
|
425
508
|
"""
|
426
509
|
results = {}
|
427
510
|
try:
|
@@ -429,6 +512,8 @@ class MySQLDeduplicator:
|
|
429
512
|
logger.warning('数据库不存在', {"库": database})
|
430
513
|
return results
|
431
514
|
target_tables = tables or self._get_tables(database)
|
515
|
+
exclude_tbls = self.exclude_tables.get(database.lower(), set())
|
516
|
+
target_tables = [t for t in target_tables if t.lower() not in exclude_tbls]
|
432
517
|
logger.debug('获取目标表', {'库': database, 'tables': target_tables})
|
433
518
|
if not target_tables:
|
434
519
|
logger.info('数据库中没有表', {"库": database, "操作": "跳过"})
|
@@ -482,18 +567,21 @@ class MySQLDeduplicator:
|
|
482
567
|
parallel: bool = False
|
483
568
|
) -> Dict[str, Dict[str, Tuple[int, int]]]:
|
484
569
|
"""
|
485
|
-
|
486
|
-
|
487
|
-
:
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
570
|
+
对所有数据库进行去重。
|
571
|
+
|
572
|
+
Args:
|
573
|
+
databases (Optional[List[str]]): 要处理的数据库列表。如果为 None,则处理所有非系统数据库。
|
574
|
+
tables_map (Optional[Dict[str, List[str]]]): 指定每个数据库要处理的表,格式为 {数据库名: [表名, ...]}。如果为 None,则处理所有表。
|
575
|
+
columns_map (Optional[Dict[str, Dict[str, List[str]]]]): 指定每个表去重时使用的列,格式为 {数据库名: {表名: [列名, ...]}}。如果为 None,则使用所有列。
|
576
|
+
dry_run (bool): 是否为模拟运行模式。为 True 时只统计重复行数,不实际删除。
|
577
|
+
parallel (bool): 是否并行处理多个数据库。为 True 时使用线程池并发处理。
|
578
|
+
Returns:
|
579
|
+
Dict[str, Dict[str, Tuple[int, int]]]: 嵌套字典,格式为 {数据库名: {表名: (重复组数, 实际删除行数)}}。
|
493
580
|
"""
|
494
|
-
all_results = defaultdict(dict)
|
581
|
+
all_results: Dict[str, Dict[str, Tuple[int, int]]] = defaultdict(dict)
|
495
582
|
try:
|
496
|
-
target_dbs = databases or self._get_databases()
|
583
|
+
target_dbs: List[str] = databases or self._get_databases()
|
584
|
+
target_dbs = [db for db in target_dbs if db.lower() not in self.exclude_databases]
|
497
585
|
logger.debug('获取目标数据库', {'databases': target_dbs})
|
498
586
|
if not target_dbs:
|
499
587
|
logger.warning('没有可处理的数据库')
|
@@ -504,7 +592,7 @@ class MySQLDeduplicator:
|
|
504
592
|
with concurrent.futures.ThreadPoolExecutor(
|
505
593
|
max_workers=self.max_workers
|
506
594
|
) as executor:
|
507
|
-
futures = {}
|
595
|
+
futures: Dict[concurrent.futures.Future, str] = {}
|
508
596
|
for db in target_dbs:
|
509
597
|
tables = tables_map.get(db) if tables_map else None
|
510
598
|
db_columns_map = columns_map.get(db) if columns_map else None
|
@@ -545,7 +633,14 @@ class MySQLDeduplicator:
|
|
545
633
|
|
546
634
|
@_retry_on_failure
|
547
635
|
def _check_database_exists(self, database: str) -> bool:
|
548
|
-
"""
|
636
|
+
"""
|
637
|
+
检查数据库是否存在。
|
638
|
+
|
639
|
+
Args:
|
640
|
+
database (str): 数据库名。
|
641
|
+
Returns:
|
642
|
+
bool: 数据库是否存在。
|
643
|
+
"""
|
549
644
|
sql = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = %s"
|
550
645
|
|
551
646
|
with self._get_connection() as conn:
|
@@ -555,7 +650,15 @@ class MySQLDeduplicator:
|
|
555
650
|
|
556
651
|
@_retry_on_failure
|
557
652
|
def _check_table_exists(self, database: str, table: str) -> bool:
|
558
|
-
"""
|
653
|
+
"""
|
654
|
+
检查表是否存在。
|
655
|
+
|
656
|
+
Args:
|
657
|
+
database (str): 数据库名。
|
658
|
+
table (str): 表名。
|
659
|
+
Returns:
|
660
|
+
bool: 表是否存在。
|
661
|
+
"""
|
559
662
|
sql = """
|
560
663
|
SELECT TABLE_NAME
|
561
664
|
FROM INFORMATION_SCHEMA.TABLES
|
@@ -568,7 +671,12 @@ class MySQLDeduplicator:
|
|
568
671
|
return bool(cursor.fetchone())
|
569
672
|
|
570
673
|
def close(self) -> None:
|
571
|
-
"""
|
674
|
+
"""
|
675
|
+
关闭连接池。
|
676
|
+
|
677
|
+
Returns:
|
678
|
+
None
|
679
|
+
"""
|
572
680
|
try:
|
573
681
|
if hasattr(self, 'pool') and self.pool and not self._closed:
|
574
682
|
self.pool.close()
|
@@ -580,9 +688,25 @@ class MySQLDeduplicator:
|
|
580
688
|
logger.error(f"关闭连接池时出错", {'error_type': type(e).__name__, 'error': str(e)})
|
581
689
|
|
582
690
|
def __enter__(self) -> 'MySQLDeduplicator':
|
691
|
+
"""
|
692
|
+
上下文管理器进入方法。
|
693
|
+
|
694
|
+
Returns:
|
695
|
+
MySQLDeduplicator: 实例自身。
|
696
|
+
"""
|
583
697
|
return self
|
584
698
|
|
585
699
|
def __exit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> None:
|
700
|
+
"""
|
701
|
+
上下文管理器退出方法,自动关闭连接池。
|
702
|
+
|
703
|
+
Args:
|
704
|
+
exc_type (Optional[type]): 异常类型。
|
705
|
+
exc_val (Optional[BaseException]): 异常值。
|
706
|
+
exc_tb (Optional[Any]): 异常追踪。
|
707
|
+
Returns:
|
708
|
+
None
|
709
|
+
"""
|
586
710
|
self.close()
|
587
711
|
|
588
712
|
|