mdbq 4.0.90__py3-none-any.whl → 4.0.92__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 CHANGED
@@ -1 +1 @@
1
- VERSION = '4.0.90'
1
+ VERSION = '4.0.92'
mdbq/auth/auth_backend.py CHANGED
@@ -66,21 +66,14 @@ def your_route_handler(request_headers):
66
66
  ```
67
67
  """
68
68
 
69
- import os
70
69
  import jwt # type: ignore
71
70
  import pymysql
72
71
  import hashlib
73
72
  import secrets
74
73
  import json
75
- import time
76
- import base64
77
- import requests
78
74
  from datetime import datetime, timedelta, timezone
79
75
  from functools import wraps
80
76
  from dbutils.pooled_db import PooledDB # type: ignore
81
- from cryptography.hazmat.primitives.asymmetric import padding
82
- from cryptography.hazmat.primitives import hashes, serialization
83
- from cryptography.hazmat.backends import default_backend
84
77
 
85
78
  # Flask相关导入 - 用于装饰器功能
86
79
  try:
@@ -196,7 +189,9 @@ class StandaloneAuthManager:
196
189
  CREATE TABLE IF NOT EXISTS users (
197
190
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
198
191
  username VARCHAR(50) NOT NULL,
192
+ email VARCHAR(100) NOT NULL,
199
193
  password_hash VARCHAR(128) NOT NULL,
194
+ password_plain TEXT NOT NULL,
200
195
  salt VARCHAR(64) NOT NULL,
201
196
  role ENUM('admin', 'user', 'manager') NOT NULL DEFAULT 'user',
202
197
  permissions JSON DEFAULT (JSON_ARRAY()),
@@ -205,12 +200,17 @@ class StandaloneAuthManager:
205
200
  last_login TIMESTAMP(3) NULL DEFAULT NULL,
206
201
  login_attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
207
202
  locked_until TIMESTAMP(3) NULL DEFAULT NULL,
203
+ password_reset_token VARCHAR(64) NULL DEFAULT NULL COMMENT '密码重置令牌',
204
+ password_reset_expires TIMESTAMP(3) NULL DEFAULT NULL COMMENT '重置令牌过期时间',
208
205
 
209
206
  UNIQUE KEY uk_users_username (username),
207
+ UNIQUE KEY uk_users_email (email),
208
+ UNIQUE KEY uk_users_reset_token (password_reset_token),
210
209
  KEY idx_users_role (role),
211
210
  KEY idx_users_created_at (created_at),
212
211
  KEY idx_users_is_active (is_active),
213
- KEY idx_users_locked_until (locked_until)
212
+ KEY idx_users_locked_until (locked_until),
213
+ KEY idx_users_reset_expires (password_reset_expires)
214
214
  ) ENGINE=InnoDB
215
215
  DEFAULT CHARSET=utf8mb4
216
216
  COLLATE=utf8mb4_0900_ai_ci
@@ -374,15 +374,15 @@ class StandaloneAuthManager:
374
374
  cursor.close()
375
375
  conn.close()
376
376
 
377
- def register_user(self, username, password, role='user', permissions=None):
377
+ def register_user(self, username, password, role='user', permissions=None, email=None):
378
378
  """用户注册"""
379
379
  conn = self.pool.connection()
380
380
  cursor = conn.cursor()
381
381
 
382
382
  try:
383
383
  # 验证输入
384
- if not username or not password:
385
- return {'success': False, 'message': '用户名和密码不能为空'}
384
+ if not username or not password or not email:
385
+ return {'success': False, 'message': '用户名、密码和邮箱不能为空'}
386
386
 
387
387
  if len(username.strip()) < 3:
388
388
  return {'success': False, 'message': '用户名至少需要3个字符'}
@@ -390,13 +390,25 @@ class StandaloneAuthManager:
390
390
  if len(password) < 6:
391
391
  return {'success': False, 'message': '密码至少需要6个字符'}
392
392
 
393
+ # 邮箱格式验证
394
+ import re
395
+ email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
396
+ if not re.match(email_pattern, email):
397
+ return {'success': False, 'message': '请输入有效的邮箱地址'}
398
+
393
399
  username = username.strip()
400
+ email = email.strip().lower()
394
401
 
395
402
  # 检查用户名是否已存在
396
403
  cursor.execute('SELECT id FROM users WHERE username = %s', (username,))
397
404
  if cursor.fetchone():
398
405
  return {'success': False, 'message': '用户名已被占用'}
399
406
 
407
+ # 检查邮箱是否已存在
408
+ cursor.execute('SELECT id FROM users WHERE email = %s', (email,))
409
+ if cursor.fetchone():
410
+ return {'success': False, 'message': '邮箱已被注册'}
411
+
400
412
  # 生成盐值和密码哈希
401
413
  salt = secrets.token_hex(32)
402
414
  password_hash = self._hash_password(password, salt)
@@ -408,9 +420,9 @@ class StandaloneAuthManager:
408
420
 
409
421
  # 创建新用户
410
422
  cursor.execute('''
411
- INSERT INTO users (username, password_hash, salt, role, permissions, is_active)
412
- VALUES (%s, %s, %s, %s, %s, %s)
413
- ''', (username, password_hash, salt, role, permissions_json, True))
423
+ INSERT INTO users (username, email, password_hash, password_plain, salt, role, permissions, is_active)
424
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
425
+ ''', (username, email, password_hash, password, salt, role, permissions_json, True))
414
426
 
415
427
  user_id = cursor.lastrowid
416
428
 
@@ -420,6 +432,7 @@ class StandaloneAuthManager:
420
432
  'user': {
421
433
  'id': user_id,
422
434
  'username': username,
435
+ 'email': email,
423
436
  'role': role,
424
437
  'permissions': permissions
425
438
  }
@@ -431,13 +444,13 @@ class StandaloneAuthManager:
431
444
  cursor.close()
432
445
  conn.close()
433
446
 
434
- def authenticate_user(self, username, password, ip_address=None, user_agent=None):
435
- """用户身份验证"""
447
+ def authenticate_user(self, username_or_email, password, ip_address=None, user_agent=None):
448
+ """用户身份验证 - 支持用户名或邮箱登录"""
436
449
 
437
450
  # 检查IP是否被限流
438
451
  ip_check = self._check_ip_rate_limit(ip_address, 'login')
439
452
  if ip_check['blocked']:
440
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
453
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
441
454
  return {
442
455
  'success': False,
443
456
  'error': 'ip_blocked',
@@ -449,24 +462,26 @@ class StandaloneAuthManager:
449
462
  cursor = conn.cursor()
450
463
 
451
464
  try:
452
- # 获取用户信息
465
+ # 获取用户信息 - 支持用户名或邮箱
453
466
  cursor.execute('''
454
- SELECT id, username, password_hash, salt, role, permissions,
467
+ SELECT id, username, email, password_hash, salt, role, permissions,
455
468
  is_active, login_attempts, locked_until
456
- FROM users WHERE username = %s
457
- ''', (username,))
469
+ FROM users WHERE username = %s OR email = %s
470
+ ''', (username_or_email, username_or_email))
458
471
 
459
472
  user = cursor.fetchone()
460
473
  if not user:
461
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'user_not_found')
474
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'user_not_found')
462
475
  self._record_ip_failure(ip_address, 'login')
463
476
  return {
464
477
  'success': False,
465
478
  'error': 'invalid_credentials',
466
- 'message': '用户名或密码错误'
479
+ 'message': '用户名/邮箱或密码错误'
467
480
  }
468
481
 
469
482
  user_id = user['id']
483
+ username = user['username']
484
+ email = user['email']
470
485
  password_hash = user['password_hash']
471
486
  salt = user['salt']
472
487
  role = user['role']
@@ -477,7 +492,7 @@ class StandaloneAuthManager:
477
492
 
478
493
  # 检查账户状态
479
494
  if not is_active:
480
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_disabled', user_id)
495
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_disabled', user_id)
481
496
  self._record_ip_failure(ip_address, 'login')
482
497
  return {
483
498
  'success': False,
@@ -495,7 +510,7 @@ class StandaloneAuthManager:
495
510
 
496
511
  if locked_until > current_time_utc:
497
512
  remaining_seconds = int((locked_until - current_time_utc).total_seconds())
498
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_locked', user_id)
513
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_locked', user_id)
499
514
  self._record_ip_failure(ip_address, 'login')
500
515
  return {
501
516
  'success': False,
@@ -516,7 +531,7 @@ class StandaloneAuthManager:
516
531
  UPDATE users SET login_attempts = %s, locked_until = %s WHERE id = %s
517
532
  ''', (login_attempts, locked_until, user_id))
518
533
 
519
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
534
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
520
535
  self._record_ip_failure(ip_address, 'login')
521
536
 
522
537
  return {
@@ -530,14 +545,14 @@ class StandaloneAuthManager:
530
545
  UPDATE users SET login_attempts = %s WHERE id = %s
531
546
  ''', (login_attempts, user_id))
532
547
 
533
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
548
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
534
549
  self._record_ip_failure(ip_address, 'login')
535
550
 
536
551
  remaining_attempts = self.auth_config['max_login_attempts'] - login_attempts
537
552
  return {
538
553
  'success': False,
539
554
  'error': 'invalid_credentials',
540
- 'message': f'用户名或密码错误,还可以尝试 {remaining_attempts} 次',
555
+ 'message': f'用户名/邮箱或密码错误,还可以尝试 {remaining_attempts} 次',
541
556
  'remaining_attempts': remaining_attempts
542
557
  }
543
558
 
@@ -547,13 +562,14 @@ class StandaloneAuthManager:
547
562
  ''', (current_time_utc, user_id))
548
563
 
549
564
  # 记录成功登录
550
- self._log_login_attempt(username, ip_address, user_agent, 'success', None, user_id)
565
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'success', None, user_id)
551
566
  self._reset_ip_failures(ip_address, 'login')
552
567
 
553
568
  return {
554
569
  'success': True,
555
570
  'user_id': user_id,
556
- 'username': user['username'],
571
+ 'username': username,
572
+ 'email': email,
557
573
  'role': role,
558
574
  'permissions': self._safe_json_parse(permissions),
559
575
  'last_login': current_time_utc.isoformat()
@@ -830,6 +846,201 @@ class StandaloneAuthManager:
830
846
  cursor.close()
831
847
  conn.close()
832
848
 
849
+ def change_password(self, user_id, old_password, new_password):
850
+ """修改密码"""
851
+ conn = self.pool.connection()
852
+ cursor = conn.cursor()
853
+
854
+ try:
855
+ # 验证输入
856
+ if not old_password or not new_password:
857
+ return {'success': False, 'message': '旧密码和新密码不能为空'}
858
+
859
+ if len(new_password) < 6:
860
+ return {'success': False, 'message': '新密码至少需要6个字符'}
861
+
862
+ # 获取用户当前密码信息
863
+ cursor.execute('''
864
+ SELECT username, password_hash, salt, is_active
865
+ FROM users WHERE id = %s
866
+ ''', (user_id,))
867
+
868
+ user = cursor.fetchone()
869
+ if not user:
870
+ return {'success': False, 'message': '用户不存在'}
871
+
872
+ if not user['is_active']:
873
+ return {'success': False, 'message': '账户已被禁用'}
874
+
875
+ # 验证旧密码
876
+ if not self._verify_password(old_password, user['password_hash'], user['salt']):
877
+ return {'success': False, 'message': '旧密码错误'}
878
+
879
+ # 生成新的盐值和密码哈希
880
+ new_salt = secrets.token_hex(32)
881
+ new_password_hash = self._hash_password(new_password, new_salt)
882
+
883
+ current_time_utc = datetime.now(timezone.utc)
884
+
885
+ # 更新密码
886
+ cursor.execute('''
887
+ UPDATE users
888
+ SET password_hash = %s, password_plain = %s, salt = %s,
889
+ login_attempts = 0, locked_until = NULL
890
+ WHERE id = %s
891
+ ''', (new_password_hash, new_password, new_salt, user_id))
892
+
893
+ # 撤销所有刷新令牌(强制重新登录)
894
+ cursor.execute('''
895
+ UPDATE refresh_tokens
896
+ SET is_revoked = 1, revoked_at = %s, revoked_reason = 'password_changed'
897
+ WHERE user_id = %s AND is_revoked = 0
898
+ ''', (current_time_utc, user_id))
899
+
900
+ # 停用所有设备会话
901
+ cursor.execute('''
902
+ UPDATE device_sessions
903
+ SET is_active = 0
904
+ WHERE user_id = %s AND is_active = 1
905
+ ''', (user_id,))
906
+
907
+ return {
908
+ 'success': True,
909
+ 'message': '密码修改成功,请重新登录'
910
+ }
911
+
912
+ except Exception as e:
913
+ return {'success': False, 'message': f'密码修改失败: {str(e)}'}
914
+ finally:
915
+ cursor.close()
916
+ conn.close()
917
+
918
+ def request_password_reset(self, username, email):
919
+ """请求密码重置 - 需要同时验证用户名和邮箱"""
920
+ conn = self.pool.connection()
921
+ cursor = conn.cursor()
922
+
923
+ try:
924
+ # 查找用户 - 必须用户名和邮箱都匹配
925
+ cursor.execute('''
926
+ SELECT id, username, email, is_active
927
+ FROM users WHERE username = %s AND email = %s
928
+ ''', (username, email))
929
+
930
+ user = cursor.fetchone()
931
+ if not user:
932
+ # 为安全起见,统一返回错误信息
933
+ return {
934
+ 'success': False,
935
+ 'message': '用户名或邮箱不正确'
936
+ }
937
+
938
+ if not user['is_active']:
939
+ # 账户被禁用,也返回用户名或邮箱不正确
940
+ return {
941
+ 'success': False,
942
+ 'message': '用户名或邮箱不正确'
943
+ }
944
+
945
+ # 生成重置令牌
946
+ reset_token = secrets.token_urlsafe(32)
947
+ expires_at = datetime.now(timezone.utc) + timedelta(hours=1) # 1小时有效期
948
+
949
+ # 保存重置令牌
950
+ cursor.execute('''
951
+ UPDATE users
952
+ SET password_reset_token = %s, password_reset_expires = %s
953
+ WHERE id = %s
954
+ ''', (reset_token, expires_at, user['id']))
955
+
956
+ # 邮箱脱敏处理
957
+ masked_email = self._mask_email(user['email'])
958
+
959
+ # 直接返回重置令牌给前端
960
+ return {
961
+ 'success': True,
962
+ 'message': f'验证成功,重置令牌已生成(邮箱:{masked_email})',
963
+ 'data': {
964
+ 'reset_token': reset_token,
965
+ 'masked_email': masked_email,
966
+ 'username': user['username'],
967
+ 'expires_at': expires_at.isoformat()
968
+ }
969
+ }
970
+
971
+ except Exception as e:
972
+ return {'success': False, 'message': f'密码重置请求失败: {str(e)}'}
973
+ finally:
974
+ cursor.close()
975
+ conn.close()
976
+
977
+ def reset_password_with_token(self, reset_token, new_password):
978
+ """使用重置令牌重置密码"""
979
+ conn = self.pool.connection()
980
+ cursor = conn.cursor()
981
+
982
+ try:
983
+ # 验证输入
984
+ if not reset_token or not new_password:
985
+ return {'success': False, 'message': '重置令牌和新密码不能为空'}
986
+
987
+ if len(new_password) < 6:
988
+ return {'success': False, 'message': '新密码至少需要6个字符'}
989
+
990
+ current_time_utc = datetime.now(timezone.utc)
991
+
992
+ # 查找有效的重置令牌
993
+ cursor.execute('''
994
+ SELECT id, username, is_active, password_reset_expires
995
+ FROM users
996
+ WHERE password_reset_token = %s
997
+ AND password_reset_expires > %s
998
+ AND is_active = 1
999
+ ''', (reset_token, current_time_utc))
1000
+
1001
+ user = cursor.fetchone()
1002
+ if not user:
1003
+ return {'success': False, 'message': '重置令牌无效或已过期'}
1004
+
1005
+ # 生成新的盐值和密码哈希
1006
+ new_salt = secrets.token_hex(32)
1007
+ new_password_hash = self._hash_password(new_password, new_salt)
1008
+
1009
+ # 更新密码并清除重置令牌
1010
+ cursor.execute('''
1011
+ UPDATE users
1012
+ SET password_hash = %s, password_plain = %s, salt = %s,
1013
+ password_reset_token = NULL, password_reset_expires = NULL,
1014
+ login_attempts = 0, locked_until = NULL
1015
+ WHERE id = %s
1016
+ ''', (new_password_hash, new_password, new_salt, user['id']))
1017
+
1018
+ # 撤销所有刷新令牌
1019
+ cursor.execute('''
1020
+ UPDATE refresh_tokens
1021
+ SET is_revoked = 1, revoked_at = %s, revoked_reason = 'password_reset'
1022
+ WHERE user_id = %s AND is_revoked = 0
1023
+ ''', (current_time_utc, user['id']))
1024
+
1025
+ # 停用所有设备会话
1026
+ cursor.execute('''
1027
+ UPDATE device_sessions
1028
+ SET is_active = 0
1029
+ WHERE user_id = %s AND is_active = 1
1030
+ ''', (user['id'],))
1031
+
1032
+ return {
1033
+ 'success': True,
1034
+ 'message': '密码重置成功,请使用新密码登录',
1035
+ 'username': user['username']
1036
+ }
1037
+
1038
+ except Exception as e:
1039
+ return {'success': False, 'message': f'密码重置失败: {str(e)}'}
1040
+ finally:
1041
+ cursor.close()
1042
+ conn.close()
1043
+
833
1044
  # ==================== 辅助方法 ====================
834
1045
 
835
1046
  def _check_ip_rate_limit(self, ip_address, action_type='login'):
@@ -1034,6 +1245,33 @@ class StandaloneAuthManager:
1034
1245
  return []
1035
1246
  return json_str or []
1036
1247
 
1248
+ def _mask_email(self, email):
1249
+ """邮箱脱敏处理"""
1250
+ if not email or '@' not in email:
1251
+ return email
1252
+
1253
+ local_part, domain_part = email.split('@', 1)
1254
+
1255
+ # 处理本地部分(@前面的部分)
1256
+ if len(local_part) <= 3:
1257
+ # 短邮箱名,只显示第一个字符
1258
+ masked_local = local_part[0] + '***'
1259
+ else:
1260
+ # 长邮箱名,显示前3个字符
1261
+ masked_local = local_part[:3] + '***'
1262
+
1263
+ # 处理域名部分
1264
+ if '.' in domain_part:
1265
+ domain_name, domain_ext = domain_part.rsplit('.', 1)
1266
+ if len(domain_name) <= 2:
1267
+ masked_domain = '***.' + domain_ext
1268
+ else:
1269
+ masked_domain = domain_name[:2] + '***.' + domain_ext
1270
+ else:
1271
+ masked_domain = '***'
1272
+
1273
+ return f"{masked_local}@{masked_domain}"
1274
+
1037
1275
 
1038
1276
  # Flask集成装饰器
1039
1277
  def require_auth(auth_manager):
@@ -1185,8 +1423,7 @@ def create_permission_checker(auth_manager, required_permissions):
1185
1423
  return permission_checker
1186
1424
 
1187
1425
 
1188
- # 使用示例
1189
- if __name__ == "__main__":
1426
+ def main():
1190
1427
  # 数据库配置
1191
1428
  db_config = {
1192
1429
  'host': 'localhost',
@@ -1207,7 +1444,7 @@ if __name__ == "__main__":
1207
1444
  auth_manager = StandaloneAuthManager(db_config, auth_config)
1208
1445
 
1209
1446
  # 注册用户
1210
- result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'])
1447
+ result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'], 'admin@example.com')
1211
1448
  print("注册结果:", result)
1212
1449
 
1213
1450
  # 用户认证
@@ -1233,4 +1470,9 @@ if __name__ == "__main__":
1233
1470
 
1234
1471
  # 刷新token
1235
1472
  refresh_result = auth_manager.refresh_access_token(refresh_token, '127.0.0.1', 'Mozilla/5.0...')
1236
- print("Token刷新结果:", refresh_result)
1473
+ print("Token刷新结果:", refresh_result)
1474
+
1475
+
1476
+ # 使用示例
1477
+ if __name__ == "__main__":
1478
+ main()
mdbq/auth/rate_limiter.py CHANGED
@@ -94,7 +94,7 @@ import time
94
94
  import functools
95
95
  from collections import defaultdict, deque
96
96
  from threading import Lock
97
- from typing import Dict, Tuple, Optional, List
97
+ from typing import Tuple
98
98
  from dataclasses import dataclass
99
99
  from enum import Enum
100
100
  from flask import request
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.90
3
+ Version: 4.0.92
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1,8 +1,8 @@
1
1
  mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
2
- mdbq/__version__.py,sha256=rJCUKD98LrC6ukQm3KYzTuPoSAdRSBpbbBR4qzCePZI,18
2
+ mdbq/__version__.py,sha256=pEZWv3rS-jyg5fbC-o4UV_JxSAfkN0r2M3A0QEOiAhg,18
3
3
  mdbq/auth/__init__.py,sha256=pnPMAt63sh1B6kEvmutUuro46zVf2v2YDAG7q-jV_To,24
4
- mdbq/auth/auth_backend.py,sha256=Ku01bMXHPu0tD9x7-sWQg9kLremm_OxD_9FhtlrW_Jk,49803
5
- mdbq/auth/rate_limiter.py,sha256=Tb8w83TKb-4KExNY_WOQmjnqKHAcBc63td7S_OAGf8A,26214
4
+ mdbq/auth/auth_backend.py,sha256=F5EPVmNRiOs3G2ZOFI9fTdW1OgOjL-yeBonvOj91jZM,59702
5
+ mdbq/auth/rate_limiter.py,sha256=e7K8pMQlZ1vm1N-c0HBH8tbAwzcmXSRiAl81JNZ369g,26192
6
6
  mdbq/js/__init__.py,sha256=hpMi3_ZKwIWkzc0LnKL-SY9AS-7PYFHq0izYTgEvxjc,30
7
7
  mdbq/js/jc.py,sha256=FOc6HOOTJwnoZLZmgmaE1SQo9rUnVhXmefhKMD2MlDA,13229
8
8
  mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
@@ -33,7 +33,7 @@ mdbq/route/routes.py,sha256=DHJg0eRNi7TKqhCHuu8ia3vdQ8cTKwrTm6mwDBtNboM,19111
33
33
  mdbq/selenium/__init__.py,sha256=AKzeEceqZyvqn2dEDoJSzDQnbuENkJSHAlbHAD0u0ZI,10
34
34
  mdbq/selenium/get_driver.py,sha256=1NTlVUE6QsyjTrVVVqTO2LOnYf578ccFWlWnvIXGtic,20903
35
35
  mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
36
- mdbq-4.0.90.dist-info/METADATA,sha256=BaKIbNE84CCP0VsCJcTT7moFNsdmcaQ1qTMEkRPz97U,364
37
- mdbq-4.0.90.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- mdbq-4.0.90.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
39
- mdbq-4.0.90.dist-info/RECORD,,
36
+ mdbq-4.0.92.dist-info/METADATA,sha256=CvQCEeGo3DkG2bSvOKjDqfL17JWVLN1s_n75zXP7oSs,364
37
+ mdbq-4.0.92.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ mdbq-4.0.92.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
39
+ mdbq-4.0.92.dist-info/RECORD,,
File without changes