mdbq 4.0.100__tar.gz → 4.0.102__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.

Potentially problematic release.


This version of mdbq might be problematic. Click here for more details.

Files changed (44) hide show
  1. {mdbq-4.0.100 → mdbq-4.0.102}/PKG-INFO +1 -1
  2. mdbq-4.0.102/mdbq/__version__.py +1 -0
  3. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/auth/auth_backend.py +66 -15
  4. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/auth/rate_limiter.py +183 -73
  5. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq.egg-info/PKG-INFO +1 -1
  6. mdbq-4.0.100/mdbq/__version__.py +0 -1
  7. {mdbq-4.0.100 → mdbq-4.0.102}/README.txt +0 -0
  8. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/__init__.py +0 -0
  9. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/auth/__init__.py +0 -0
  10. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/js/__init__.py +0 -0
  11. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/js/jc.py +0 -0
  12. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/log/__init__.py +0 -0
  13. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/log/mylogger.py +0 -0
  14. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/myconf/__init__.py +0 -0
  15. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/myconf/myconf.py +0 -0
  16. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/__init__.py +0 -0
  17. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/deduplicator.py +0 -0
  18. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/mysql.py +0 -0
  19. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/s_query.py +0 -0
  20. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/unique_.py +0 -0
  21. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/mysql/uploader.py +0 -0
  22. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/__init__.py +0 -0
  23. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/download_sku_picture.py +0 -0
  24. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/error_handler.py +0 -0
  25. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/otk.py +0 -0
  26. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/pov_city.py +0 -0
  27. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/other/ua_sj.py +0 -0
  28. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/pbix/__init__.py +0 -0
  29. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/pbix/pbix_refresh.py +0 -0
  30. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/pbix/refresh_all.py +0 -0
  31. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/redis/__init__.py +0 -0
  32. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/redis/getredis.py +0 -0
  33. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/route/__init__.py +0 -0
  34. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/route/analytics.py +0 -0
  35. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/route/monitor.py +0 -0
  36. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/route/routes.py +0 -0
  37. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/selenium/__init__.py +0 -0
  38. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/selenium/get_driver.py +0 -0
  39. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq/spider/__init__.py +0 -0
  40. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq.egg-info/SOURCES.txt +0 -0
  41. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq.egg-info/dependency_links.txt +0 -0
  42. {mdbq-4.0.100 → mdbq-4.0.102}/mdbq.egg-info/top_level.txt +0 -0
  43. {mdbq-4.0.100 → mdbq-4.0.102}/setup.cfg +0 -0
  44. {mdbq-4.0.100 → mdbq-4.0.102}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.100
3
+ Version: 4.0.102
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -0,0 +1 @@
1
+ VERSION = '4.0.102'
@@ -1427,24 +1427,74 @@ class StandaloneAuthManager:
1427
1427
  cursor.close()
1428
1428
  conn.close()
1429
1429
 
1430
- def logout_device(self, user_id, device_id):
1431
- """登出指定设备"""
1430
+ def logout_device(self, user_id, device_id=None, ip_address=None, user_agent=None, device_info=None, login_domain=''):
1431
+ """
1432
+ 登出设备(支持两种识别方式)
1433
+
1434
+ Args:
1435
+ user_id: 用户ID
1436
+ device_id: 设备ID(用于指定设备登出)
1437
+ ip_address: IP地址(用于当前设备登出)
1438
+ user_agent: 用户代理(用于当前设备登出)
1439
+ device_info: 设备信息(用于当前设备登出)
1440
+ login_domain: 登录域名(用于当前设备登出)
1441
+
1442
+ Returns:
1443
+ dict: 登出结果
1444
+ """
1432
1445
  conn = self.pool.connection()
1433
1446
  cursor = conn.cursor()
1434
1447
 
1435
1448
  try:
1436
1449
  current_time_utc = datetime.now(timezone.utc)
1437
1450
 
1438
- # 查找设备
1439
- cursor.execute('''
1440
- SELECT id, device_name FROM device_sessions
1441
- WHERE user_id = %s AND device_id = %s AND is_active = 1
1442
- ''', (user_id, device_id))
1443
-
1444
- device = cursor.fetchone()
1445
-
1446
- if not device:
1447
- return {'success': False, 'message': '设备不存在或已登出'}
1451
+ if device_id:
1452
+ # 方式1:通过device_id查找设备(用于设备管理界面)
1453
+ cursor.execute('''
1454
+ SELECT id, device_name FROM device_sessions
1455
+ WHERE user_id = %s AND device_id = %s AND is_active = 1
1456
+ ''', (user_id, device_id))
1457
+
1458
+ device = cursor.fetchone()
1459
+ logout_reason = 'single_device_logout'
1460
+
1461
+ if not device:
1462
+ return {'success': False, 'message': '设备不存在或已登出'}
1463
+
1464
+ else:
1465
+ # 方式2:通过设备指纹查找当前设备(用于当前设备登出)
1466
+ if not user_agent:
1467
+ return {'success': False, 'message': '缺少设备识别信息'}
1468
+
1469
+ # 解析用户代理获取基本信息
1470
+ parsed_ua = self._parse_user_agent(user_agent)
1471
+
1472
+ # 合并设备信息
1473
+ full_device_info = {
1474
+ 'user_agent': user_agent,
1475
+ 'platform': parsed_ua.get('platform'),
1476
+ **parsed_ua,
1477
+ **(device_info or {})
1478
+ }
1479
+
1480
+ # 生成包含域名的设备指纹来识别当前设备
1481
+ device_fingerprint = self._generate_device_fingerprint(full_device_info, login_domain)
1482
+
1483
+ # 查找当前活跃的设备会话
1484
+ cursor.execute('''
1485
+ SELECT ds.id, ds.device_name
1486
+ FROM device_sessions ds
1487
+ WHERE ds.user_id = %s AND ds.device_fingerprint = %s AND ds.login_domain = %s
1488
+ AND ds.is_active = 1
1489
+ ORDER BY ds.session_version DESC
1490
+ LIMIT 1
1491
+ ''', (user_id, device_fingerprint, login_domain))
1492
+
1493
+ device = cursor.fetchone()
1494
+ logout_reason = 'current_device_logout'
1495
+
1496
+ if not device:
1497
+ return {'success': False, 'message': '当前设备会话不存在或已失效'}
1448
1498
 
1449
1499
  device_session_id = device['id']
1450
1500
  device_name = device['device_name']
@@ -1452,9 +1502,9 @@ class StandaloneAuthManager:
1452
1502
  # 撤销该设备的刷新令牌
1453
1503
  cursor.execute('''
1454
1504
  UPDATE refresh_tokens
1455
- SET is_revoked = 1, revoked_at = %s, revoked_reason = 'single_device_logout'
1505
+ SET is_revoked = 1, revoked_at = %s, revoked_reason = %s
1456
1506
  WHERE device_session_id = %s AND is_revoked = 0
1457
- ''', (current_time_utc, device_session_id))
1507
+ ''', (current_time_utc, logout_reason, device_session_id))
1458
1508
 
1459
1509
  # 停用设备会话
1460
1510
  cursor.execute('''
@@ -1463,7 +1513,8 @@ class StandaloneAuthManager:
1463
1513
  WHERE id = %s
1464
1514
  ''', (device_session_id,))
1465
1515
 
1466
- return {'success': True, 'message': f'设备 "{device_name}" 已成功登出'}
1516
+ message = f'设备 "{device_name}" 已成功登出' if device_id else f'已成功登出当前设备 "{device_name}"'
1517
+ return {'success': True, 'message': message}
1467
1518
 
1468
1519
  except Exception as e:
1469
1520
  return {'success': False, 'message': f'登出设备失败: {str(e)}'}
@@ -255,30 +255,60 @@ class AdvancedRateLimiter:
255
255
 
256
256
  def check_sliding_window_log(self, key: str, rule: RateLimitRule) -> Tuple[bool, int]:
257
257
  """滑动窗口日志算法"""
258
- with self.locks[key]:
259
- current_time = time.time()
260
- window_start = current_time - rule.window
261
-
262
- if key not in self.storage:
263
- self.storage[key] = {'requests': deque(), 'last_access': current_time}
264
-
265
- # 更新最后访问时间
266
- self.storage[key]['last_access'] = current_time
267
- requests = self.storage[key]['requests']
268
-
269
- # 清理过期请求
270
- while requests and requests[0] < window_start:
271
- requests.popleft()
272
-
273
- # 检查是否超过限制
274
- if len(requests) >= rule.requests:
275
- return False, 0
276
-
277
- # 记录当前请求
278
- requests.append(current_time)
279
- remaining = rule.requests - len(requests)
258
+ try:
259
+ if self.logger:
260
+ self.logger.debug(f"滑动窗口检查: key={key}, rule.requests={rule.requests}, rule.window={rule.window}")
280
261
 
281
- return True, remaining
262
+ with self.locks[key]:
263
+ current_time = time.time()
264
+ window_start = current_time - rule.window
265
+
266
+ if key not in self.storage:
267
+ if self.logger:
268
+ self.logger.debug(f"创建新的存储条目: key={key}")
269
+ self.storage[key] = {'requests': deque(), 'last_access': current_time}
270
+
271
+ # 更新最后访问时间
272
+ self.storage[key]['last_access'] = current_time
273
+
274
+ # 确保存储结构正确(可能被其他算法创建了不同结构)
275
+ if 'requests' not in self.storage[key]:
276
+ if self.logger:
277
+ self.logger.debug(f"修复存储结构: key={key}, 当前结构={list(self.storage[key].keys())}")
278
+ # 重新初始化为滑动窗口结构
279
+ self.storage[key] = {'requests': deque(), 'last_access': current_time}
280
+
281
+ requests = self.storage[key]['requests']
282
+
283
+ if self.logger:
284
+ self.logger.debug(f"当前请求队列长度: {len(requests)}")
285
+ # 清理过期请求
286
+ while requests and requests[0] < window_start:
287
+ requests.popleft()
288
+
289
+ # 检查是否超过限制
290
+ if len(requests) >= rule.requests:
291
+ if self.logger:
292
+ self.logger.debug(f"请求超限: {len(requests)} >= {rule.requests}")
293
+ return False, 0
294
+
295
+ # 记录当前请求
296
+ requests.append(current_time)
297
+ remaining = rule.requests - len(requests)
298
+
299
+ if self.logger:
300
+ self.logger.debug(f"请求允许: remaining={remaining}")
301
+
302
+ return True, remaining
303
+
304
+ except Exception as e:
305
+ if self.logger:
306
+ import traceback
307
+ self.logger.error(f"滑动窗口算法异常: {str(e)}")
308
+ self.logger.error(f"Key: {key}, Rule: {rule}")
309
+ self.logger.error(f"异常详情: {traceback.format_exc()}")
310
+ # 异常时允许通过
311
+ return True, 100
282
312
 
283
313
  def check_token_bucket(self, key: str, rule: RateLimitRule) -> Tuple[bool, int]:
284
314
  """令牌桶算法"""
@@ -313,59 +343,122 @@ class AdvancedRateLimiter:
313
343
 
314
344
  def check_fixed_window(self, key: str, rule: RateLimitRule) -> Tuple[bool, int]:
315
345
  """固定窗口算法"""
316
- current_time = time.time()
317
- window_start = int(current_time // rule.window) * rule.window
318
-
319
- with self.locks[key]:
320
- if key not in self.storage:
321
- self.storage[key] = {'count': 0, 'window_start': window_start, 'last_access': current_time}
322
-
323
- # 更新最后访问时间
324
- self.storage[key]['last_access'] = current_time
325
- data = self.storage[key]
326
-
327
- # 检查是否是新窗口
328
- if data['window_start'] != window_start:
329
- data['count'] = 0
330
- data['window_start'] = window_start
346
+ try:
347
+ if self.logger:
348
+ self.logger.debug(f"固定窗口检查: key={key}, rule.requests={rule.requests}, rule.window={rule.window}")
331
349
 
332
- # 检查是否超过限制
333
- if data['count'] >= rule.requests:
334
- return False, 0
350
+ current_time = time.time()
351
+ window_start = int(current_time // rule.window) * rule.window
335
352
 
336
- data['count'] += 1
337
- remaining = rule.requests - data['count']
353
+ with self.locks[key]:
354
+ if key not in self.storage:
355
+ if self.logger:
356
+ self.logger.debug(f"创建新的固定窗口存储: key={key}")
357
+ self.storage[key] = {'count': 0, 'window_start': window_start, 'last_access': current_time}
358
+
359
+ # 更新最后访问时间
360
+ self.storage[key]['last_access'] = current_time
361
+
362
+ # 确保存储结构正确(可能被滑动窗口算法创建了不同结构)
363
+ if 'count' not in self.storage[key] or 'window_start' not in self.storage[key]:
364
+ if self.logger:
365
+ self.logger.debug(f"修复固定窗口存储结构: key={key}, 当前结构={list(self.storage[key].keys())}")
366
+ # 重新初始化为固定窗口结构
367
+ self.storage[key] = {'count': 0, 'window_start': window_start, 'last_access': current_time}
368
+
369
+ data = self.storage[key]
338
370
 
339
- return True, remaining
371
+ # 检查是否是新窗口
372
+ if data['window_start'] != window_start:
373
+ if self.logger:
374
+ self.logger.debug(f"新窗口开始: {window_start}")
375
+ data['count'] = 0
376
+ data['window_start'] = window_start
377
+
378
+ # 检查是否超过限制
379
+ if data['count'] >= rule.requests:
380
+ if self.logger:
381
+ self.logger.debug(f"固定窗口请求超限: {data['count']} >= {rule.requests}")
382
+ return False, 0
383
+
384
+ data['count'] += 1
385
+ remaining = rule.requests - data['count']
386
+
387
+ if self.logger:
388
+ self.logger.debug(f"固定窗口请求允许: count={data['count']}, remaining={remaining}")
389
+
390
+ return True, remaining
391
+
392
+ except Exception as e:
393
+ if self.logger:
394
+ import traceback
395
+ self.logger.error(f"固定窗口算法异常: {str(e)}")
396
+ self.logger.error(f"Key: {key}, Rule: {rule}")
397
+ self.logger.error(f"异常详情: {traceback.format_exc()}")
398
+ # 异常时允许通过
399
+ return True, 100
340
400
 
341
401
  def check_rate_limit(self, api_type: str, key: str, level: RateLimitLevel,
342
402
  strategy: RateLimitStrategy = None) -> Tuple[bool, int, dict]:
343
403
  """核心限流检查"""
344
- # 检查是否被阻断
345
- is_blocked, block_remaining = self.is_blocked(key)
346
- if is_blocked:
347
- return False, 0, {
348
- 'error': 'blocked',
349
- 'retry_after': block_remaining,
350
- 'reason': 'IP temporarily blocked due to excessive requests'
351
- }
352
-
353
- # 获取限流规则
354
- if api_type not in self.rules or level not in self.rules[api_type]:
355
- rule = RateLimitRule(100, 60) # 默认规则
356
- else:
357
- rule = self.rules[api_type][level]
404
+ try:
405
+ if self.logger:
406
+ self.logger.debug(f"check_rate_limit: api_type={api_type}, key={key}, level={level}")
407
+
408
+ # 检查是否被阻断
409
+ is_blocked, block_remaining = self.is_blocked(key)
410
+ if is_blocked:
411
+ if self.logger:
412
+ self.logger.debug(f"Key {key} is blocked, remaining: {block_remaining}")
413
+ return False, 0, {
414
+ 'error': 'blocked',
415
+ 'retry_after': block_remaining,
416
+ 'reason': 'IP temporarily blocked due to excessive requests'
417
+ }
418
+
419
+ # 获取限流规则
420
+ if api_type not in self.rules or level not in self.rules[api_type]:
421
+ if self.logger:
422
+ self.logger.debug(f"Using default rule for api_type={api_type}, level={level}")
423
+ rule = RateLimitRule(100, 60) # 默认规则
424
+ else:
425
+ rule = self.rules[api_type][level]
426
+ if self.logger:
427
+ self.logger.debug(f"Using rule: requests={rule.requests}, window={rule.window}")
428
+ except Exception as e:
429
+ if self.logger:
430
+ import traceback
431
+ self.logger.error(f"check_rate_limit异常: {str(e)}")
432
+ self.logger.error(f"异常详情: {traceback.format_exc()}")
433
+ # 异常时返回允许通过
434
+ return True, 100, {}
358
435
 
359
436
  # 选择限流策略
360
- strategy = strategy or rule.strategy
361
-
362
- if strategy == RateLimitStrategy.SLIDING_WINDOW_LOG:
363
- allowed, remaining = self.check_sliding_window_log(key, rule)
364
- elif strategy == RateLimitStrategy.TOKEN_BUCKET:
365
- allowed, remaining = self.check_token_bucket(key, rule)
366
- else:
367
- # 默认固定窗口
368
- allowed, remaining = self.check_fixed_window(key, rule)
437
+ try:
438
+ strategy = strategy or rule.strategy
439
+
440
+ if self.logger:
441
+ self.logger.debug(f"Using strategy: {strategy}")
442
+
443
+ if strategy == RateLimitStrategy.SLIDING_WINDOW_LOG:
444
+ allowed, remaining = self.check_sliding_window_log(key, rule)
445
+ elif strategy == RateLimitStrategy.TOKEN_BUCKET:
446
+ allowed, remaining = self.check_token_bucket(key, rule)
447
+ else:
448
+ # 默认固定窗口
449
+ allowed, remaining = self.check_fixed_window(key, rule)
450
+
451
+ if self.logger:
452
+ self.logger.debug(f"Strategy result: allowed={allowed}, remaining={remaining}")
453
+
454
+ except Exception as e:
455
+ if self.logger:
456
+ import traceback
457
+ self.logger.error(f"限流策略执行异常: {str(e)}")
458
+ self.logger.error(f"策略: {strategy}, Key: {key}")
459
+ self.logger.error(f"异常详情: {traceback.format_exc()}")
460
+ # 策略执行异常时,允许通过
461
+ return True, 100, {}
369
462
 
370
463
  # 如果超过限制,考虑是否阻断
371
464
  if not allowed:
@@ -459,13 +552,22 @@ class RateLimitDecorators:
459
552
  def decorated_function(*args, **kwargs):
460
553
  try:
461
554
  # 获取客户端信息
555
+ if self.limiter.logger:
556
+ self.limiter.logger.debug(f"限流检查开始: api_type={api_type}")
557
+
462
558
  client_ip, user_level, rate_limit_key = self.limiter.get_client_info()
463
559
 
560
+ if self.limiter.logger:
561
+ self.limiter.logger.debug(f"客户端信息: ip={client_ip}, level={user_level}, key={rate_limit_key}")
562
+
464
563
  # 执行限流检查
465
564
  allowed, remaining, error_info = self.limiter.check_rate_limit(
466
565
  api_type, rate_limit_key, user_level, strategy
467
566
  )
468
567
 
568
+ if self.limiter.logger:
569
+ self.limiter.logger.debug(f"限流检查结果: allowed={allowed}, remaining={remaining}")
570
+
469
571
  if not allowed:
470
572
  # 记录限流事件
471
573
  if self.limiter.logger:
@@ -495,18 +597,26 @@ class RateLimitDecorators:
495
597
  if isinstance(response, tuple) and len(response) >= 2:
496
598
  response_data, status_code = response[0], response[1]
497
599
  if hasattr(response_data, 'headers'):
498
- rule = self.limiter.rules.get(api_type, {}).get(user_level)
499
- if rule:
500
- response_data.headers['X-RateLimit-Limit'] = str(rule.requests)
501
- response_data.headers['X-RateLimit-Remaining'] = str(remaining)
502
- response_data.headers['X-RateLimit-Reset'] = str(int(time.time() + rule.window))
503
- response_data.headers['X-RateLimit-Policy'] = f"{api_type}:{user_level.value}"
600
+ try:
601
+ rule = self.limiter.rules.get(api_type, {}).get(user_level)
602
+ if rule and hasattr(rule, 'requests') and hasattr(rule, 'window'):
603
+ response_data.headers['X-RateLimit-Limit'] = str(rule.requests)
604
+ response_data.headers['X-RateLimit-Remaining'] = str(remaining)
605
+ response_data.headers['X-RateLimit-Reset'] = str(int(time.time() + rule.window))
606
+ response_data.headers['X-RateLimit-Policy'] = f"{api_type}:{user_level.value}"
607
+ except (AttributeError, KeyError, TypeError):
608
+ # 忽略头部设置错误,不影响主要功能
609
+ pass
504
610
 
505
611
  return response
506
612
 
507
613
  except Exception as e:
508
614
  if self.limiter.logger:
615
+ import traceback
509
616
  self.limiter.logger.error(f"限流系统异常: {str(e)}")
617
+ self.limiter.logger.error(f"异常类型: {type(e).__name__}")
618
+ self.limiter.logger.error(f"异常详情: {traceback.format_exc()}")
619
+ self.limiter.logger.error(f"API类型: {api_type}, 策略: {strategy}")
510
620
  # 限流系统故障时,允许请求通过
511
621
  return f(*args, **kwargs)
512
622
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.100
3
+ Version: 4.0.102
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1 +0,0 @@
1
- VERSION = '4.0.100'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes