mdbq 4.0.21__py3-none-any.whl → 4.0.22__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.21'
1
+ VERSION = '4.0.22'
mdbq/myconf/myconf2.py CHANGED
@@ -1,24 +1,10 @@
1
1
  import re
2
- from typing import Dict, Any, Optional, Union, List, Tuple, Type, TypeVar, Callable
2
+ from typing import Dict, Any, Optional, Union, List, Tuple, Type, TypeVar
3
3
  from pathlib import Path
4
4
  from mdbq.log import mylogger
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum
7
7
  import time
8
- import json
9
- import yaml
10
- import toml
11
- import os
12
- from jsonschema import validate, ValidationError
13
- from cryptography.fernet import Fernet
14
- import base64
15
- from typing_extensions import TypedDict
16
- import threading
17
- import queue
18
- import watchdog.observers
19
- import watchdog.events
20
- import jinja2
21
- from datetime import datetime
22
8
 
23
9
  logger = mylogger.MyLogger(
24
10
  logging_mode='both',
@@ -36,14 +22,7 @@ T = TypeVar('T') # 类型变量
36
22
 
37
23
 
38
24
  class ConfigError(Exception):
39
- """配置相关的基础异常类
40
-
41
- Attributes:
42
- message: 错误消息
43
- file_path: 配置文件路径
44
- section: 配置节名称
45
- key: 配置键名称
46
- """
25
+ """配置相关的基础异常类"""
47
26
  def __init__(self, message: str, file_path: Optional[Union[str, Path]] = None,
48
27
  section: Optional[str] = None, key: Optional[str] = None):
49
28
  self.message = message
@@ -65,17 +44,13 @@ class ConfigError(Exception):
65
44
 
66
45
 
67
46
  class ConfigFileNotFoundError(ConfigError):
68
- """当指定的配置文件不存在时抛出的异常"""
47
+ """配置文件不存在异常"""
69
48
  def __init__(self, file_path: Union[str, Path]):
70
49
  super().__init__("配置文件不存在", file_path=file_path)
71
50
 
72
51
 
73
52
  class ConfigReadError(ConfigError):
74
- """当读取配置文件失败时抛出的异常
75
-
76
- Attributes:
77
- original_error: 原始错误对象
78
- """
53
+ """读取配置文件失败异常"""
79
54
  def __init__(self, file_path: Union[str, Path], original_error: Exception):
80
55
  super().__init__(
81
56
  f"读取配置文件失败: {str(original_error)}",
@@ -85,11 +60,7 @@ class ConfigReadError(ConfigError):
85
60
 
86
61
 
87
62
  class ConfigWriteError(ConfigError):
88
- """当写入配置文件失败时抛出的异常
89
-
90
- Attributes:
91
- original_error: 原始错误对象
92
- """
63
+ """写入配置文件失败异常"""
93
64
  def __init__(self, file_path: Union[str, Path], original_error: Exception):
94
65
  super().__init__(
95
66
  f"写入配置文件失败: {str(original_error)}",
@@ -99,14 +70,14 @@ class ConfigWriteError(ConfigError):
99
70
 
100
71
 
101
72
  class ConfigValueError(ConfigError):
102
- """当配置值无效时抛出的异常"""
73
+ """配置值无效异常"""
103
74
  def __init__(self, message: str, file_path: Union[str, Path],
104
75
  section: Optional[str] = None, key: Optional[str] = None):
105
76
  super().__init__(message, file_path=file_path, section=section, key=key)
106
77
 
107
78
 
108
79
  class ConfigSectionNotFoundError(ConfigError):
109
- """当指定的配置节不存在时抛出的异常"""
80
+ """配置节不存在异常"""
110
81
  def __init__(self, file_path: Union[str, Path], section: str):
111
82
  super().__init__(
112
83
  f"配置节不存在",
@@ -116,7 +87,7 @@ class ConfigSectionNotFoundError(ConfigError):
116
87
 
117
88
 
118
89
  class ConfigKeyNotFoundError(ConfigError):
119
- """当指定的配置键不存在时抛出的异常"""
90
+ """配置键不存在异常"""
120
91
  def __init__(self, file_path: Union[str, Path], section: str, key: str):
121
92
  super().__init__(
122
93
  f"配置键不存在",
@@ -133,32 +104,9 @@ class CommentStyle(Enum):
133
104
  SEMICOLON = ';' # INI风格注释
134
105
 
135
106
 
107
+ @dataclass
136
108
  class ConfigOptions:
137
- """配置解析器的选项类
138
-
139
- Attributes:
140
- comment_styles: 支持的注释风格列表
141
- encoding: 文件编码
142
- auto_create: 是否自动创建不存在的配置文件
143
- strip_values: 是否去除配置值的首尾空白
144
- preserve_comments: 是否保留注释
145
- default_section: 默认配置节名称
146
- separators: 支持的分隔符列表
147
- cache_ttl: 缓存过期时间(秒)
148
- validate_keys: 是否验证键名
149
- key_pattern: 键名正则表达式模式
150
- case_sensitive: 是否区分大小写
151
- schema_validation: 是否启用模式验证
152
- schema: JSON Schema 模式定义
153
- format_handlers: 自定义格式处理器
154
- env_prefix: 环境变量前缀
155
- env_override: 是否允许环境变量覆盖配置
156
- encryption_key: 加密密钥
157
- inherit_from: 继承的配置文件路径
158
- watch_changes: 是否监视配置文件变化
159
- template_vars: 模板变量
160
- template_loader: 模板加载器
161
- """
109
+ """配置解析器选项"""
162
110
  comment_styles: List[CommentStyle] = field(default_factory=lambda: [CommentStyle.HASH, CommentStyle.DOUBLE_SLASH])
163
111
  encoding: str = 'utf-8'
164
112
  auto_create: bool = False
@@ -166,80 +114,14 @@ class ConfigOptions:
166
114
  preserve_comments: bool = True
167
115
  default_section: str = 'DEFAULT'
168
116
  separators: List[str] = field(default_factory=lambda: ['=', ':', ':'])
169
- cache_ttl: int = 300
117
+ cache_ttl: int = 300 # 5分钟缓存过期
170
118
  validate_keys: bool = True
171
119
  key_pattern: str = r'^[a-zA-Z0-9_\-\.]+$'
172
120
  case_sensitive: bool = False
173
- schema_validation: bool = False
174
- schema: Optional[Dict[str, Any]] = None
175
- format_handlers: Dict[str, Callable] = field(default_factory=dict)
176
- env_prefix: str = 'CONFIG_'
177
- env_override: bool = True
178
- encryption_key: Optional[bytes] = None
179
- inherit_from: Optional[Union[str, Path]] = None
180
- watch_changes: bool = False
181
- template_vars: Dict[str, Any] = field(default_factory=dict)
182
- template_loader: Optional[jinja2.Environment] = None
183
-
184
-
185
- class ConfigValue(TypedDict):
186
- """配置值类型定义"""
187
- value: Any
188
- encrypted: bool
189
- source: str # 'file', 'env', 'default'
190
-
191
-
192
- class ConfigChangeEvent:
193
- """配置变更事件"""
194
- def __init__(self, file_path: Path, event_type: str, timestamp: float):
195
- self.file_path = file_path
196
- self.event_type = event_type
197
- self.timestamp = timestamp
198
-
199
-
200
- class ConfigChangeHandler(watchdog.events.FileSystemEventHandler):
201
- """配置文件变更处理器"""
202
- def __init__(self, parser: 'ConfigParser'):
203
- self.parser = parser
204
- self.event_queue = queue.Queue()
205
-
206
- def on_modified(self, event):
207
- if not event.is_directory:
208
- self.event_queue.put(ConfigChangeEvent(
209
- Path(event.src_path),
210
- 'modified',
211
- time.time()
212
- ))
213
-
214
- def on_created(self, event):
215
- if not event.is_directory:
216
- self.event_queue.put(ConfigChangeEvent(
217
- Path(event.src_path),
218
- 'created',
219
- time.time()
220
- ))
221
-
222
- def on_deleted(self, event):
223
- if not event.is_directory:
224
- self.event_queue.put(ConfigChangeEvent(
225
- Path(event.src_path),
226
- 'deleted',
227
- time.time()
228
- ))
229
121
 
230
122
 
231
123
  class ConfigParser:
232
- """配置文件解析器,用于读取和写入配置文件
233
-
234
- Attributes:
235
- options: 解析器配置选项
236
- _config_cache: 配置缓存,用于存储已读取的配置
237
- _cache_timestamps: 缓存时间戳,用于管理缓存过期
238
- _comments_cache: 注释缓存,用于存储每个配置节的注释
239
- _section_map: 用于存储大小写映射
240
- _current_file: 当前正在处理的文件路径
241
- _format_handlers: 自定义格式处理器
242
- """
124
+ """配置文件解析器"""
243
125
 
244
126
  def __init__(self, options: Optional[ConfigOptions] = None):
245
127
  self.options = options or ConfigOptions()
@@ -248,62 +130,17 @@ class ConfigParser:
248
130
  self._comments_cache: Dict[str, Dict[str, List[str]]] = {}
249
131
  self._section_map: Dict[str, Dict[str, str]] = {}
250
132
  self._current_file: Optional[Path] = None
251
- self._format_handlers = {
252
- 'json': self._handle_json,
253
- 'yaml': self._handle_yaml,
254
- 'toml': self._handle_toml,
255
- **self.options.format_handlers
256
- }
257
- self._fernet = None
258
- if self.options.encryption_key:
259
- self._fernet = Fernet(self.options.encryption_key)
260
-
261
- # 配置监听相关
262
- self._observer = None
263
- self._change_handler = None
264
- self._watch_thread = None
265
- self._stop_watching = threading.Event()
266
- self._change_callbacks: List[Callable[[ConfigChangeEvent], None]] = []
267
-
268
- # 模板相关
269
- if self.options.template_loader is None:
270
- self.options.template_loader = jinja2.Environment(
271
- loader=jinja2.FileSystemLoader('.'),
272
- autoescape=True
273
- )
274
-
133
+
275
134
  def __enter__(self) -> 'ConfigParser':
276
- """进入上下文管理器
277
-
278
- Returns:
279
- ConfigParser: 返回当前实例
280
- """
281
135
  return self
282
136
 
283
137
  def __exit__(self, exc_type: Optional[Type[BaseException]],
284
138
  exc_val: Optional[BaseException],
285
139
  exc_tb: Optional[Any]) -> None:
286
- """退出上下文管理器
287
-
288
- Args:
289
- exc_type: 异常类型
290
- exc_val: 异常值
291
- exc_tb: 异常追踪信息
292
- """
293
140
  self._current_file = None
294
141
 
295
142
  def open(self, file_path: Union[str, Path]) -> 'ConfigParser':
296
- """打开配置文件
297
-
298
- Args:
299
- file_path: 配置文件路径
300
-
301
- Returns:
302
- ConfigParser: 返回当前实例,支持链式调用
303
-
304
- Raises:
305
- ConfigFileNotFoundError: 当配置文件不存在且未启用自动创建时
306
- """
143
+ """打开配置文件"""
307
144
  file_path = Path(file_path)
308
145
  if not file_path.exists() and not self.options.auto_create:
309
146
  raise ConfigFileNotFoundError(file_path)
@@ -311,25 +148,17 @@ class ConfigParser:
311
148
  return self
312
149
 
313
150
  def _ensure_file_open(self) -> None:
314
- """确保文件已打开
315
-
316
- Raises:
317
- ConfigError: 当文件未打开时
318
- """
151
+ """确保文件已打开"""
319
152
  if self._current_file is None:
320
153
  raise ConfigError("未打开任何配置文件,请先调用 open() 方法")
321
154
 
322
155
  def _is_comment_line(self, line: str) -> bool:
323
- """判断一行是否为注释行"""
156
+ """判断是否为注释行"""
324
157
  stripped = line.strip()
325
158
  return any(stripped.startswith(style.value) for style in self.options.comment_styles)
326
159
 
327
160
  def _extract_comment(self, line: str) -> Tuple[str, str]:
328
- """从行中提取注释
329
-
330
- Returns:
331
- Tuple[str, str]: (去除注释后的行内容, 注释内容)
332
- """
161
+ """从行中提取注释"""
333
162
  for style in self.options.comment_styles:
334
163
  comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
335
164
  if comment_match:
@@ -337,14 +166,7 @@ class ConfigParser:
337
166
  return line.strip(), ''
338
167
 
339
168
  def _split_key_value(self, line: str) -> Optional[Tuple[str, str]]:
340
- """分割配置行为键值对
341
-
342
- Args:
343
- line: 要分割的配置行
344
-
345
- Returns:
346
- Optional[Tuple[str, str]]: 键值对元组,如果无法分割则返回None
347
- """
169
+ """分割配置行为键值对"""
348
170
  for sep in self.options.separators:
349
171
  if sep in line:
350
172
  key_part, value_part = line.split(sep, 1)
@@ -366,11 +188,8 @@ class ConfigParser:
366
188
  return bool(re.match(self.options.key_pattern, key))
367
189
 
368
190
  def _get_cached_config(self, file_path: str) -> Optional[Dict[str, Any]]:
369
- """获取缓存的配置,如果过期则返回None"""
370
- if file_path not in self._config_cache:
371
- return None
372
-
373
- if file_path not in self._cache_timestamps:
191
+ """获取缓存的配置"""
192
+ if file_path not in self._config_cache or file_path not in self._cache_timestamps:
374
193
  return None
375
194
 
376
195
  if time.time() - self._cache_timestamps[file_path] > self.options.cache_ttl:
@@ -384,10 +203,8 @@ class ConfigParser:
384
203
  self._cache_timestamps[file_path] = time.time()
385
204
 
386
205
  def _normalize_section(self, section: str) -> str:
387
- """标准化节名称(处理大小写)"""
388
- if self.options.case_sensitive:
389
- return section
390
- return section.lower()
206
+ """标准化节名称"""
207
+ return section if self.options.case_sensitive else section.lower()
391
208
 
392
209
  def _get_original_section(self, file_path: str, normalized_section: str) -> Optional[str]:
393
210
  """获取原始节名称"""
@@ -417,57 +234,35 @@ class ConfigParser:
417
234
  self._section_map.clear()
418
235
 
419
236
  def _convert_value(self, value: str, target_type: Type[T]) -> T:
420
- """转换配置值到指定类型
421
-
422
- Args:
423
- value: 要转换的值
424
- target_type: 目标类型
425
-
426
- Returns:
427
- T: 转换后的值
428
-
429
- Raises:
430
- ConfigValueError: 当值无法转换为指定类型时
431
- """
237
+ """转换配置值到指定类型"""
432
238
  try:
433
239
  if target_type == bool:
434
240
  return bool(value.lower() in ('true', 'yes', '1', 'on'))
435
241
  elif target_type == list:
436
- # 支持多种分隔符的列表
437
242
  if not value.strip():
438
243
  return []
439
- # 尝试不同的分隔符
440
244
  for sep in [',', ';', '|', ' ']:
441
245
  if sep in value:
442
246
  return [item.strip() for item in value.split(sep) if item.strip()]
443
- # 如果没有分隔符,则作为单个元素返回
444
247
  return [value.strip()]
445
248
  elif target_type == tuple:
446
- # 支持元组类型
447
249
  if not value.strip():
448
250
  return ()
449
- # 尝试不同的分隔符
450
251
  for sep in [',', ';', '|', ' ']:
451
252
  if sep in value:
452
253
  return tuple(item.strip() for item in value.split(sep) if item.strip())
453
- # 如果没有分隔符,则作为单个元素返回
454
254
  return (value.strip(),)
455
255
  elif target_type == set:
456
- # 支持集合类型
457
256
  if not value.strip():
458
257
  return set()
459
- # 尝试不同的分隔符
460
258
  for sep in [',', ';', '|', ' ']:
461
259
  if sep in value:
462
260
  return {item.strip() for item in value.split(sep) if item.strip()}
463
- # 如果没有分隔符,则作为单个元素返回
464
261
  return {value.strip()}
465
262
  elif target_type == dict:
466
- # 支持字典类型,格式:key1=value1,key2=value2
467
263
  if not value.strip():
468
264
  return {}
469
265
  result = {}
470
- # 尝试不同的分隔符
471
266
  for sep in [',', ';', '|']:
472
267
  if sep in value:
473
268
  pairs = [pair.strip() for pair in value.split(sep) if pair.strip()]
@@ -476,13 +271,11 @@ class ConfigParser:
476
271
  key, val = pair.split('=', 1)
477
272
  result[key.strip()] = val.strip()
478
273
  return result
479
- # 如果没有分隔符,尝试单个键值对
480
274
  if '=' in value:
481
275
  key, val = value.split('=', 1)
482
276
  return {key.strip(): val.strip()}
483
277
  return {}
484
278
  elif target_type == int:
485
- # 支持十六进制、八进制、二进制
486
279
  value = value.strip().lower()
487
280
  if value.startswith('0x'):
488
281
  return int(value, 16)
@@ -504,7 +297,6 @@ class ConfigParser:
504
297
  elif target_type == frozenset:
505
298
  return frozenset(value.split(','))
506
299
  elif target_type == range:
507
- # 支持 range 类型,格式:start:stop:step 或 start:stop
508
300
  parts = value.split(':')
509
301
  if len(parts) == 2:
510
302
  return range(int(parts[0]), int(parts[1]))
@@ -522,23 +314,7 @@ class ConfigParser:
522
314
  def get_value(self, file_path: Optional[Union[str, Path]] = None, key: str = None,
523
315
  section: Optional[str] = None, default: Any = None,
524
316
  value_type: Optional[Type[T]] = None) -> T:
525
- """获取指定配置项的值
526
-
527
- Args:
528
- file_path: 配置文件路径,如果为None则使用当前打开的文件
529
- key: 配置键
530
- section: 配置节名称,如果为None则使用默认节
531
- default: 当配置项不存在时返回的默认值
532
- value_type: 期望的值的类型
533
-
534
- Returns:
535
- T: 配置值
536
-
537
- Raises:
538
- ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
539
- ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
540
- ConfigValueError: 当值无法转换为指定类型时
541
- """
317
+ """获取指定配置项的值"""
542
318
  if file_path is None:
543
319
  self._ensure_file_open()
544
320
  file_path = self._current_file
@@ -549,7 +325,6 @@ class ConfigParser:
549
325
  section = section or self.options.default_section
550
326
  normalized_section = self._normalize_section(section)
551
327
 
552
- # 获取原始节名称
553
328
  original_section = self._get_original_section(str(file_path), normalized_section)
554
329
  if original_section is None:
555
330
  if default is not None:
@@ -572,22 +347,7 @@ class ConfigParser:
572
347
  file_path: Optional[Union[str, Path]] = None,
573
348
  defaults: Optional[Dict[str, Any]] = None,
574
349
  value_types: Optional[Dict[str, Type]] = None) -> Dict[str, Any]:
575
- """批量获取多个配置项的值
576
-
577
- Args:
578
- keys: 配置项列表,每个元素为 (section, key) 元组
579
- file_path: 配置文件路径,如果为None则使用当前打开的文件
580
- defaults: 默认值字典,格式为 {key: default_value}
581
- value_types: 值类型字典,格式为 {key: type}
582
-
583
- Returns:
584
- Dict[str, Any]: 配置值字典,格式为 {key: value}
585
-
586
- Raises:
587
- ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
588
- ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
589
- ConfigValueError: 当值无法转换为指定类型时
590
- """
350
+ """批量获取多个配置项的值"""
591
351
  if file_path is None:
592
352
  self._ensure_file_open()
593
353
  file_path = self._current_file
@@ -618,23 +378,7 @@ class ConfigParser:
618
378
  file_path: Optional[Union[str, Path]] = None,
619
379
  defaults: Optional[Dict[str, Any]] = None,
620
380
  value_types: Optional[Dict[str, Type]] = None) -> Tuple[Any, ...]:
621
- """获取指定节点下多个键的值元组
622
-
623
- Args:
624
- keys: 要获取的键列表
625
- section: 配置节名称,默认为 DEFAULT
626
- file_path: 配置文件路径,如果为None则使用当前打开的文件
627
- defaults: 默认值字典,格式为 {key: default_value}
628
- value_types: 值类型字典,格式为 {key: type}
629
-
630
- Returns:
631
- Tuple[Any, ...]: 按键列表顺序返回的值元组
632
-
633
- Raises:
634
- ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
635
- ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
636
- ConfigValueError: 当值无法转换为指定类型时
637
- """
381
+ """获取指定节点下多个键的值元组"""
638
382
  if file_path is None:
639
383
  self._ensure_file_open()
640
384
  file_path = self._current_file
@@ -664,32 +408,18 @@ class ConfigParser:
664
408
  section: Optional[str] = None,
665
409
  file_path: Optional[Union[str, Path]] = None,
666
410
  value_type: Optional[Type] = None) -> None:
667
- """设置指定配置项的值,保持原始文件的格式和注释
668
-
669
- Args:
670
- key: 配置键
671
- value: 要设置的值
672
- section: 配置节名称,如果为None则使用默认节
673
- file_path: 配置文件路径,如果为None则使用当前打开的文件
674
- value_type: 值的类型,用于验证和转换
675
-
676
- Raises:
677
- ConfigValueError: 当值无法转换为指定类型时
678
- ConfigError: 当其他配置错误发生时
679
- """
411
+ """设置指定配置项的值"""
680
412
  if file_path is None:
681
413
  self._ensure_file_open()
682
414
  file_path = self._current_file
683
415
  if not self._validate_key(key):
684
416
  raise ConfigValueError(f"无效的键名: {key}", file_path=file_path, key=key)
685
417
 
686
- # 读取原始文件內容
687
418
  original_lines = []
688
419
  if file_path.exists():
689
420
  with open(file_path, 'r', encoding=self.options.encoding) as file:
690
421
  original_lines = file.readlines()
691
422
 
692
- # 读取当前配置
693
423
  config = self.read(file_path)
694
424
 
695
425
  if section not in config:
@@ -717,61 +447,53 @@ class ConfigParser:
717
447
  else:
718
448
  value = str(value)
719
449
 
720
- # 更新配置
721
450
  config[section][key] = value
722
451
 
723
- # 写入文件,保持原始格式
724
452
  try:
725
453
  file_path.parent.mkdir(parents=True, exist_ok=True)
726
454
 
727
455
  with open(file_path, 'w', encoding=self.options.encoding) as file:
728
456
  current_section = self.options.default_section
729
- section_separators = {} # 用于存储每个section使用的分隔符
457
+ section_separators = {}
730
458
 
731
- # 解析原始文件,提取格式信息
732
459
  for line in original_lines:
733
460
  stripped_line = line.strip()
734
461
 
735
462
  if not stripped_line:
736
- file.write(line) # 保持空行
463
+ file.write(line)
737
464
  continue
738
465
 
739
466
  if stripped_line.startswith('[') and stripped_line.endswith(']'):
740
467
  current_section = stripped_line[1:-1]
741
- file.write(line) # 保持节标记的原始格式
468
+ file.write(line)
742
469
  continue
743
470
 
744
471
  if self._is_comment_line(stripped_line):
745
- file.write(line) # 保持注释的原始格式
472
+ file.write(line)
746
473
  continue
747
474
 
748
475
  key_value = self._split_key_value(stripped_line)
749
476
  if key_value:
750
477
  orig_key, orig_value = key_value
751
- # 检测使用的分隔符
752
478
  for sep in self.options.separators:
753
479
  if sep in line:
754
480
  section_separators.setdefault(current_section, {})[orig_key] = sep
755
481
  break
756
482
 
757
- # 如果是当前要修改的键,则写入新值
758
483
  if current_section == section and orig_key == key:
759
484
  separator = section_separators.get(current_section, {}).get(orig_key, self.options.separators[0])
760
- # 提取行尾注释
761
485
  comment = ''
762
486
  for style in self.options.comment_styles:
763
487
  comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
764
488
  if comment_match:
765
489
  comment = comment_match.group(0)
766
490
  break
767
- # 写入新值并保留注释
768
491
  file.write(f'{key}{separator}{value}{comment}\n')
769
492
  else:
770
- file.write(line) # 保持其他行的原始格式
493
+ file.write(line)
771
494
  else:
772
- file.write(line) # 保持无法解析的行的原始格式
495
+ file.write(line)
773
496
 
774
- # 如果section不存在,则添加新的section
775
497
  if section not in [line.strip()[1:-1] for line in original_lines if line.strip().startswith('[') and line.strip().endswith(']')]:
776
498
  file.write(f'\n[{section}]\n')
777
499
  file.write(f'{key}={value}\n')
@@ -781,506 +503,117 @@ class ConfigParser:
781
503
  except Exception as e:
782
504
  raise ConfigWriteError(file_path, e)
783
505
 
784
- def _handle_json(self, content: str) -> Dict[str, Any]:
785
- """处理 JSON 格式的配置"""
786
- return json.loads(content)
787
-
788
- def _handle_yaml(self, content: str) -> Dict[str, Any]:
789
- """处理 YAML 格式的配置"""
790
- return yaml.safe_load(content)
791
-
792
- def _handle_toml(self, content: str) -> Dict[str, Any]:
793
- """处理 TOML 格式的配置"""
794
- return toml.loads(content)
795
-
796
- def _validate_schema(self, config: Dict[str, Any]) -> None:
797
- """验证配置是否符合模式定义"""
798
- if not self.options.schema_validation or not self.options.schema:
799
- return
800
- try:
801
- validate(instance=config, schema=self.options.schema)
802
- except ValidationError as e:
803
- raise ConfigValueError(
804
- f"配置验证失败: {str(e)}",
805
- file_path=self._current_file
806
- )
807
-
808
- def _encrypt_value(self, value: str) -> str:
809
- """加密配置值"""
810
- if not self._fernet:
811
- return value
812
- return base64.b64encode(self._fernet.encrypt(value.encode())).decode()
813
-
814
- def _decrypt_value(self, value: str) -> str:
815
- """解密配置值"""
816
- if not self._fernet:
817
- return value
818
- try:
819
- return self._fernet.decrypt(base64.b64decode(value)).decode()
820
- except Exception:
821
- return value
822
-
823
- def _get_env_value(self, section: str, key: str) -> Optional[str]:
824
- """从环境变量获取配置值"""
825
- env_key = f"{self.options.env_prefix}{section}_{key}".upper()
826
- return os.environ.get(env_key)
827
-
828
- def _merge_configs(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]:
829
- """合并配置,支持深度合并"""
830
- result = base_config.copy()
831
- for section, items in override_config.items():
832
- if section not in result:
833
- result[section] = {}
834
- for key, value in items.items():
835
- if isinstance(value, dict) and isinstance(result[section].get(key), dict):
836
- result[section][key] = self._merge_configs(result[section][key], value)
837
- else:
838
- result[section][key] = value
839
- return result
840
-
841
- def _process_template(self, content: str) -> str:
842
- """处理配置模板"""
843
- template = self.options.template_loader.from_string(content)
844
- return template.render(
845
- **self.options.template_vars,
846
- now=datetime.now,
847
- env=os.environ
848
- )
849
-
850
- def watch(self, callback: Callable[[ConfigChangeEvent], None]) -> None:
851
- """开始监视配置文件变化
852
-
853
- Args:
854
- callback: 配置变更回调函数
855
- """
856
- if not self.options.watch_changes:
857
- return
858
-
859
- self._change_callbacks.append(callback)
860
-
861
- if self._observer is None:
862
- self._observer = watchdog.observers.Observer()
863
- self._change_handler = ConfigChangeHandler(self)
864
-
865
- if self._current_file:
866
- self._observer.schedule(
867
- self._change_handler,
868
- str(self._current_file.parent),
869
- recursive=False
870
- )
871
-
872
- self._observer.start()
873
-
874
- def watch_loop():
875
- while not self._stop_watching.is_set():
876
- try:
877
- event = self._change_handler.event_queue.get(timeout=1)
878
- if event.file_path == self._current_file:
879
- self._clear_cache(str(event.file_path))
880
- for callback in self._change_callbacks:
881
- callback(event)
882
- except queue.Empty:
883
- continue
884
-
885
- self._watch_thread = threading.Thread(target=watch_loop, daemon=True)
886
- self._watch_thread.start()
887
-
888
- def stop_watching(self) -> None:
889
- """停止监视配置文件变化"""
890
- if self._observer:
891
- self._stop_watching.set()
892
- self._observer.stop()
893
- self._observer.join()
894
- self._observer = None
895
- self._change_handler = None
896
- if self._watch_thread:
897
- self._watch_thread.join()
898
- self._watch_thread = None
899
- self._change_callbacks.clear()
900
-
901
506
  def read(self, file_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
902
- """读取配置文件内容,支持继承、环境变量覆盖和模板处理"""
507
+ """读取配置文件内容"""
903
508
  if file_path is None:
904
509
  self._ensure_file_open()
905
510
  file_path = self._current_file
906
511
  else:
907
512
  file_path = Path(file_path)
908
-
909
- # 检查缓存
513
+
910
514
  cached_config = self._get_cached_config(str(file_path))
911
515
  if cached_config is not None:
912
516
  return cached_config
913
-
914
- # 读取基础配置
915
- base_config = {}
916
- if self.options.inherit_from:
917
- base_config = self.read(self.options.inherit_from)
918
-
517
+
919
518
  if not file_path.exists():
920
519
  if not self.options.auto_create:
921
520
  raise ConfigFileNotFoundError(file_path)
922
521
  logger.info(f'配置文件不存在,将创建: {file_path}')
923
522
  file_path.parent.mkdir(parents=True, exist_ok=True)
924
523
  file_path.touch()
925
- return base_config
926
-
524
+ return {}
525
+
927
526
  try:
928
527
  with open(file_path, 'r', encoding=self.options.encoding) as file:
929
- content = file.read()
930
-
931
- # 处理模板
932
- content = self._process_template(content)
528
+ config = {}
529
+ current_section = self.options.default_section
530
+ section_comments = []
933
531
 
934
- # 根据文件扩展名选择处理器
935
- suffix = file_path.suffix.lower()
936
- if suffix in self._format_handlers:
937
- config = self._format_handlers[suffix](content)
938
- else:
939
- # 默认使用 INI 格式处理
940
- config = {}
941
- current_section = self.options.default_section
942
- section_comments = []
943
-
944
- for line in content.splitlines():
945
- stripped_line = line.strip()
532
+ for line in file:
533
+ stripped_line = line.strip()
534
+
535
+ if not stripped_line or self._is_comment_line(stripped_line):
536
+ if self.options.preserve_comments:
537
+ section_comments.append(line.rstrip())
538
+ continue
539
+
540
+ if stripped_line.startswith('[') and stripped_line.endswith(']'):
541
+ current_section = stripped_line[1:-1]
542
+ if not self._validate_key(current_section):
543
+ raise ConfigValueError(
544
+ f"无效的节名: {current_section}",
545
+ file_path=file_path,
546
+ section=current_section
547
+ )
548
+ self._update_section_map(str(file_path), current_section)
549
+ if current_section not in config:
550
+ config[current_section] = {}
551
+ if self.options.preserve_comments:
552
+ self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).extend(section_comments)
553
+ section_comments = []
554
+ continue
555
+
556
+ key_value = self._split_key_value(stripped_line)
557
+ if key_value:
558
+ key, value = key_value
559
+ if not self._validate_key(key):
560
+ raise ConfigValueError(
561
+ f"无效的键名: {key}",
562
+ file_path=file_path,
563
+ section=current_section,
564
+ key=key
565
+ )
566
+ value, comment = self._extract_comment(value)
946
567
 
947
- if not stripped_line or self._is_comment_line(stripped_line):
948
- if self.options.preserve_comments:
949
- section_comments.append(line.rstrip())
950
- continue
568
+ if self.options.strip_values:
569
+ value = value.strip()
951
570
 
952
- if stripped_line.startswith('[') and stripped_line.endswith(']'):
953
- current_section = stripped_line[1:-1]
954
- if not self._validate_key(current_section):
955
- raise ConfigValueError(
956
- f"无效的节名: {current_section}",
957
- file_path=file_path,
958
- section=current_section
959
- )
960
- self._update_section_map(str(file_path), current_section)
961
- if current_section not in config:
962
- config[current_section] = {}
963
- if self.options.preserve_comments:
964
- self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).extend(section_comments)
965
- section_comments = []
966
- continue
571
+ if current_section not in config:
572
+ config[current_section] = {}
967
573
 
968
- key_value = self._split_key_value(stripped_line)
969
- if key_value:
970
- key, value = key_value
971
- if not self._validate_key(key):
972
- raise ConfigValueError(
973
- f"无效的键名: {key}",
974
- file_path=file_path,
975
- section=current_section,
976
- key=key
977
- )
978
- value, comment = self._extract_comment(value)
979
-
980
- if self.options.strip_values:
981
- value = value.strip()
982
-
983
- if current_section not in config:
984
- config[current_section] = {}
985
-
986
- # 检查是否是加密值
987
- if value.startswith('ENC(') and value.endswith(')'):
988
- value = self._decrypt_value(value[4:-1])
989
-
990
- config[current_section][key] = value
991
- if self.options.preserve_comments and comment:
992
- self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).append(comment)
993
-
994
- # 合并基础配置
995
- config = self._merge_configs(base_config, config)
996
-
997
- # 应用环境变量覆盖
998
- if self.options.env_override:
999
- for section in config:
1000
- for key in config[section]:
1001
- env_value = self._get_env_value(section, key)
1002
- if env_value is not None:
1003
- config[section][key] = env_value
1004
-
1005
- self._validate_schema(config)
574
+ config[current_section][key] = value
575
+ if self.options.preserve_comments and comment:
576
+ self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).append(comment)
577
+
1006
578
  self._update_cache(str(file_path), config)
1007
579
  return config
1008
-
580
+
1009
581
  except Exception as e:
1010
582
  raise ConfigReadError(file_path, e)
1011
583
 
1012
- def write(self, config: Dict[str, Any], file_path: Optional[Union[str, Path]] = None,
1013
- format: Optional[str] = None, encrypt_values: bool = False) -> None:
1014
- """写入配置到文件,支持加密敏感值"""
1015
- if file_path is None:
1016
- self._ensure_file_open()
1017
- file_path = self._current_file
1018
- else:
1019
- file_path = Path(file_path)
1020
-
1021
- self._validate_schema(config)
1022
-
1023
- try:
1024
- file_path.parent.mkdir(parents=True, exist_ok=True)
1025
-
1026
- if format is None:
1027
- format = file_path.suffix.lower()[1:]
1028
-
1029
- # 处理需要加密的值
1030
- if encrypt_values and self._fernet:
1031
- for section in config:
1032
- for key, value in config[section].items():
1033
- if isinstance(value, str) and any(sensitive in key.lower() for sensitive in ['password', 'secret', 'key', 'token']):
1034
- config[section][key] = f"ENC({self._encrypt_value(value)})"
1035
-
1036
- with open(file_path, 'w', encoding=self.options.encoding) as file:
1037
- if format == 'json':
1038
- json.dump(config, file, indent=2, ensure_ascii=False)
1039
- elif format == 'yaml':
1040
- yaml.dump(config, file, allow_unicode=True)
1041
- elif format == 'toml':
1042
- toml.dump(config, file)
1043
- else: # 默认使用 INI 格式
1044
- for section, items in config.items():
1045
- file.write(f'[{section}]\n')
1046
- for key, value in items.items():
1047
- file.write(f'{key} = {value}\n')
1048
- file.write('\n')
1049
-
1050
- self._clear_cache(str(file_path))
1051
-
1052
- except Exception as e:
1053
- raise ConfigWriteError(file_path, e)
1054
-
1055
- def __del__(self):
1056
- """析构函数,确保停止监视"""
1057
- self.stop_watching()
1058
-
1059
584
 
1060
585
  def main() -> None:
1061
- """配置解析器使用示例"""
1062
-
1063
- # 示例1:基本使用
1064
- print("\n=== 示例1:基本使用 ===")
586
+ """示例用法"""
1065
587
  config_file = Path('/Users/xigua/spd.txt')
1066
588
 
589
+ # 方式1:使用上下文管理器
1067
590
  with ConfigParser() as parser:
1068
591
  parser.open(config_file)
1069
592
  host, port, username, password = parser.get_section_values(
1070
593
  keys=['host', 'port', 'username', 'password'],
1071
594
  section='mysql'
1072
595
  )
1073
- print("基本配置:", host, port, username, password)
1074
-
1075
- # 示例2:使用JSON Schema验证
1076
- print("\n=== 示例2:使用JSON Schema验证 ===")
1077
- schema = {
1078
- "type": "object",
1079
- "properties": {
1080
- "database": {
1081
- "type": "object",
1082
- "required": ["host", "port", "username", "password"],
1083
- "properties": {
1084
- "host": {"type": "string"},
1085
- "port": {"type": "integer"},
1086
- "username": {"type": "string"},
1087
- "password": {"type": "string"}
1088
- }
1089
- }
1090
- }
1091
- }
1092
-
1093
- options = ConfigOptions(
1094
- schema_validation=True,
1095
- schema=schema
1096
- )
1097
-
1098
- with ConfigParser(options) as parser:
1099
- try:
1100
- config = parser.read(config_file)
1101
- print("配置验证通过")
1102
- except ConfigValueError as e:
1103
- print(f"配置验证失败: {e}")
1104
-
1105
- # 示例3:使用环境变量
1106
- print("\n=== 示例3:使用环境变量 ===")
1107
- os.environ['CONFIG_MYSQL_HOST'] = 'env_host'
1108
- os.environ['CONFIG_MYSQL_PORT'] = '3307'
1109
-
1110
- options = ConfigOptions(
1111
- env_prefix='CONFIG_',
1112
- env_override=True
1113
- )
1114
-
1115
- with ConfigParser(options) as parser:
1116
- config = parser.read(config_file)
1117
- print("环境变量覆盖后的配置:", config['mysql'])
1118
-
1119
- # 示例4:配置继承
1120
- print("\n=== 示例4:配置继承 ===")
1121
- base_config = Path('/Users/xigua/base_config.ini')
1122
- child_config = Path('/Users/xigua/child_config.ini')
1123
-
1124
- # 创建基础配置
1125
- with ConfigParser() as parser:
1126
- parser.write({
1127
- 'database': {
1128
- 'host': 'base_host',
1129
- 'port': '3306',
1130
- 'username': 'base_user'
1131
- }
1132
- }, base_config)
1133
-
1134
- # 创建子配置
1135
- options = ConfigOptions(inherit_from=base_config)
1136
- with ConfigParser(options) as parser:
1137
- parser.write({
1138
- 'database': {
1139
- 'host': 'child_host',
1140
- 'password': 'child_pass'
1141
- }
1142
- }, child_config)
1143
-
1144
- # 读取子配置
1145
- config = parser.read(child_config)
1146
- print("继承后的配置:", config['database'])
1147
-
1148
- # 示例5:配置加密
1149
- print("\n=== 示例5:配置加密 ===")
1150
- encryption_key = Fernet.generate_key()
1151
- options = ConfigOptions(encryption_key=encryption_key)
1152
-
1153
- with ConfigParser(options) as parser:
1154
- # 写入加密配置
1155
- parser.write({
1156
- 'database': {
1157
- 'host': 'localhost',
1158
- 'port': '3306',
1159
- 'username': 'admin',
1160
- 'password': 'secret123'
1161
- }
1162
- }, config_file, encrypt_values=True)
596
+ print("方式1结果:", host, port, username, password)
1163
597
 
1164
- # 读取加密配置
1165
- config = parser.read(config_file)
1166
- print("解密后的配置:", config['database'])
1167
-
1168
- # 示例6:配置模板
1169
- print("\n=== 示例6:配置模板 ===")
1170
- template_content = """
1171
- [database]
1172
- host = {{ env.DATABASE_HOST | default('localhost') }}
1173
- port = {{ env.DATABASE_PORT | default(3306) }}
1174
- username = {{ env.DATABASE_USER | default('root') }}
1175
- password = {{ env.DATABASE_PASS | default('password') }}
598
+ parser.set_value('username', 'root', section='mysql')
599
+ parser.set_value('port', 3306, section='mysql')
1176
600
 
1177
- [app]
1178
- name = {{ app_name }}
1179
- version = {{ version }}
1180
- debug = {{ debug | default(false) }}
1181
- """
1182
-
1183
- options = ConfigOptions(
1184
- template_vars={
1185
- 'app_name': 'MyApp',
1186
- 'version': '1.0.0',
1187
- 'debug': True
1188
- }
601
+ # 方式2:链式调用
602
+ parser = ConfigParser()
603
+ host, port, username, password = parser.open(config_file).get_section_values(
604
+ keys=['host', 'port', 'username', 'password'],
605
+ section='mysql'
1189
606
  )
1190
-
1191
- with ConfigParser(options) as parser:
1192
- # 写入模板配置
1193
- with open(config_file, 'w') as f:
1194
- f.write(template_content)
1195
-
1196
- # 读取处理后的配置
1197
- config = parser.read(config_file)
1198
- print("模板处理后的配置:", config)
1199
-
1200
- # 示例7:配置监听
1201
- print("\n=== 示例7:配置监听 ===")
1202
- options = ConfigOptions(watch_changes=True)
1203
-
1204
- def on_config_change(event):
1205
- print(f"配置已更新: {event.file_path}")
1206
- print(f"事件类型: {event.event_type}")
1207
- print(f"时间戳: {event.timestamp}")
1208
-
1209
- with ConfigParser(options) as parser:
1210
- parser.open(config_file)
1211
- parser.watch(on_config_change)
1212
-
1213
- # 模拟配置更新
1214
- time.sleep(1)
1215
- with open(config_file, 'a') as f:
1216
- f.write("\n[new_section]\nkey = value\n")
1217
-
1218
- # 等待事件处理
1219
- time.sleep(2)
1220
- parser.stop_watching()
1221
-
1222
- # 示例8:多格式支持
1223
- print("\n=== 示例8:多格式支持 ===")
1224
- config_data = {
1225
- 'database': {
1226
- 'host': 'localhost',
1227
- 'port': 3306,
1228
- 'username': 'admin',
1229
- 'password': 'secret123'
1230
- }
1231
- }
1232
-
1233
- with ConfigParser() as parser:
1234
- # 写入不同格式
1235
- formats = ['json', 'yaml', 'toml', 'ini']
1236
- for fmt in formats:
1237
- file_path = config_file.with_suffix(f'.{fmt}')
1238
- parser.write(config_data, file_path, format=fmt)
1239
- print(f"\n{fmt.upper()} 格式配置:")
1240
- print(parser.read(file_path))
1241
-
1242
- # 示例9:自定义格式处理器
1243
- print("\n=== 示例9:自定义格式处理器 ===")
1244
- def handle_custom(content: str) -> Dict[str, Any]:
1245
- """自定义格式处理器示例"""
1246
- result = {}
1247
- for line in content.splitlines():
1248
- if ':' in line:
1249
- key, value = line.split(':', 1)
1250
- result[key.strip()] = value.strip()
1251
- return result
1252
-
1253
- options = ConfigOptions(
1254
- format_handlers={'custom': handle_custom}
607
+ print("\n方式2结果:", host, port, username, password)
608
+
609
+ # 方式3:传统方式
610
+ parser = ConfigParser()
611
+ host, port, username, password = parser.get_section_values(
612
+ file_path=config_file,
613
+ section='mysql',
614
+ keys=['host', 'port', 'username', 'password']
1255
615
  )
1256
-
1257
- with ConfigParser(options) as parser:
1258
- # 写入自定义格式
1259
- custom_content = """
1260
- host: localhost
1261
- port: 3306
1262
- username: admin
1263
- password: secret123
1264
- """
1265
-
1266
- custom_file = config_file.with_suffix('.custom')
1267
- with open(custom_file, 'w') as f:
1268
- f.write(custom_content)
1269
-
1270
- # 读取自定义格式
1271
- config = parser.read(custom_file)
1272
- print("自定义格式配置:", config)
1273
-
1274
- # 清理测试文件
1275
- for fmt in ['json', 'yaml', 'toml', 'ini', 'custom']:
1276
- test_file = config_file.with_suffix(f'.{fmt}')
1277
- if test_file.exists():
1278
- test_file.unlink()
1279
-
1280
- if base_config.exists():
1281
- base_config.unlink()
1282
- if child_config.exists():
1283
- child_config.unlink()
616
+ print("\n方式3结果:", host, port, username, password)
1284
617
 
1285
618
 
1286
619
  if __name__ == '__main__':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mdbq
3
- Version: 4.0.21
3
+ Version: 4.0.22
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1,12 +1,12 @@
1
1
  mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
2
- mdbq/__version__.py,sha256=POcSq32A6Q7oc-1rHdGErwirhk3WzNRY2_p6GmzXs2k,18
2
+ mdbq/__version__.py,sha256=gSoWvHL6N2Idp7W1joFJ-FzlUGBvhO24bsaGJ6I1x-Y,18
3
3
  mdbq/aggregation/__init__.py,sha256=EeDqX2Aml6SPx8363J-v1lz0EcZtgwIBYyCJV6CcEDU,40
4
4
  mdbq/aggregation/query_data.py,sha256=hdaB0vPh5BcTu9kLViRvM7OAE0b07D4jzAIipqxGI-I,166757
5
5
  mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
6
6
  mdbq/log/mylogger.py,sha256=9w_o5mYB3FooIxobq_lSa6oCYTKIhPxDFox-jeLtUHI,21714
7
7
  mdbq/myconf/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
8
8
  mdbq/myconf/myconf.py,sha256=39tLUBVlWQZzQfrwk7YoLEfipo11fpwWjaLBHcUt2qM,33341
9
- mdbq/myconf/myconf2.py,sha256=m9c_12hCuO93vhOGTArl8DUFbOawC34qLddnfI0Ho50,50110
9
+ mdbq/myconf/myconf2.py,sha256=kaHhOvKMVOilu9JYKfsefF08tUyC99B8aWUZJvc_oh8,25481
10
10
  mdbq/mysql/__init__.py,sha256=A_DPJyAoEvTSFojiI2e94zP0FKtCkkwKP1kYUCSyQzo,11
11
11
  mdbq/mysql/deduplicator.py,sha256=kAnkI_vnN8CchgDQAFzeh0M0vLXE2oWq9SfDPNZZ3v0,73215
12
12
  mdbq/mysql/mysql.py,sha256=pDg771xBugCMSTWeskIFTi3pFLgaqgyG3smzf-86Wn8,56772
@@ -25,7 +25,7 @@ mdbq/redis/__init__.py,sha256=YtgBlVSMDphtpwYX248wGge1x-Ex_mMufz4-8W0XRmA,12
25
25
  mdbq/redis/getredis.py,sha256=vpBuNc22uj9Vr-_Dh25_wpwWM1e-072EAAIBdB_IpL0,23494
26
26
  mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
27
27
  mdbq/spider/aikucun.py,sha256=juOqpr_dHeE1RyjCu67VcpzoJAWMO7FKv0i8KiH8WUo,21552
28
- mdbq-4.0.21.dist-info/METADATA,sha256=yGoEmdb_8T9J-IZkRJeAq3Ka9-m-PsK2XQ5oX_Yxi3Q,364
29
- mdbq-4.0.21.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
30
- mdbq-4.0.21.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
31
- mdbq-4.0.21.dist-info/RECORD,,
28
+ mdbq-4.0.22.dist-info/METADATA,sha256=EVHw5Bw16kqgOfKjnTmRY9SpDDpj882dD0UjB_ogT6w,364
29
+ mdbq-4.0.22.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
30
+ mdbq-4.0.22.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
31
+ mdbq-4.0.22.dist-info/RECORD,,
File without changes