mdbq 4.0.21__py3-none-any.whl → 4.0.23__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.23'
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
  """获取原始节名称"""
@@ -416,73 +233,50 @@ class ConfigParser:
416
233
  self._comments_cache.clear()
417
234
  self._section_map.clear()
418
235
 
419
- 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
- """
236
+ def _convert_value(self, value: str, target_type: Type[T], file_path: Optional[Union[str, Path]] = None, key: Optional[str] = None) -> T:
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
- elif target_type == set:
456
- # 支持集合类型
255
+ elif target_type == set or target_type == frozenset:
457
256
  if not value.strip():
458
- return set()
459
- # 尝试不同的分隔符
257
+ return set() if target_type == set else frozenset()
460
258
  for sep in [',', ';', '|', ' ']:
461
259
  if sep in value:
462
- return {item.strip() for item in value.split(sep) if item.strip()}
463
- # 如果没有分隔符,则作为单个元素返回
464
- return {value.strip()}
260
+ items = [item.strip() for item in value.split(sep) if item.strip()]
261
+ return set(items) if target_type == set else frozenset(items)
262
+ return set([value.strip()]) if target_type == set else frozenset([value.strip()])
465
263
  elif target_type == dict:
466
- # 支持字典类型,格式:key1=value1,key2=value2
467
264
  if not value.strip():
468
265
  return {}
469
266
  result = {}
470
- # 尝试不同的分隔符
471
267
  for sep in [',', ';', '|']:
472
268
  if sep in value:
473
269
  pairs = [pair.strip() for pair in value.split(sep) if pair.strip()]
474
270
  for pair in pairs:
475
271
  if '=' in pair:
476
- key, val = pair.split('=', 1)
477
- result[key.strip()] = val.strip()
272
+ key_, val = pair.split('=', 1)
273
+ result[key_.strip()] = val.strip()
478
274
  return result
479
- # 如果没有分隔符,尝试单个键值对
480
275
  if '=' in value:
481
- key, val = value.split('=', 1)
482
- return {key.strip(): val.strip()}
276
+ key_, val = value.split('=', 1)
277
+ return {key_.strip(): val.strip()}
483
278
  return {}
484
279
  elif target_type == int:
485
- # 支持十六进制、八进制、二进制
486
280
  value = value.strip().lower()
487
281
  if value.startswith('0x'):
488
282
  return int(value, 16)
@@ -499,12 +293,7 @@ class ConfigParser:
499
293
  return value.encode('utf-8')
500
294
  elif target_type == bytearray:
501
295
  return bytearray(value.encode('utf-8'))
502
- elif target_type == set:
503
- return set(value.split(','))
504
- elif target_type == frozenset:
505
- return frozenset(value.split(','))
506
296
  elif target_type == range:
507
- # 支持 range 类型,格式:start:stop:step 或 start:stop
508
297
  parts = value.split(':')
509
298
  if len(parts) == 2:
510
299
  return range(int(parts[0]), int(parts[1]))
@@ -515,79 +304,41 @@ class ConfigParser:
515
304
  except (ValueError, TypeError) as e:
516
305
  raise ConfigValueError(
517
306
  f"无法将值 '{value}' 转换为类型 {target_type.__name__}",
518
- file_path=None,
519
- key=None
307
+ file_path=file_path,
308
+ key=key
520
309
  )
521
310
 
522
311
  def get_value(self, file_path: Optional[Union[str, Path]] = None, key: str = None,
523
312
  section: Optional[str] = None, default: Any = None,
524
313
  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
- """
314
+ """获取指定配置项的值"""
542
315
  if file_path is None:
543
316
  self._ensure_file_open()
544
317
  file_path = self._current_file
545
318
  if not self._validate_key(key):
546
319
  raise ConfigValueError(f"无效的键名: {key}", file_path=file_path, key=key)
547
-
548
320
  config = self.read(file_path)
549
321
  section = section or self.options.default_section
550
322
  normalized_section = self._normalize_section(section)
551
-
552
- # 获取原始节名称
553
323
  original_section = self._get_original_section(str(file_path), normalized_section)
554
324
  if original_section is None:
555
325
  if default is not None:
556
326
  return default
557
327
  raise ConfigSectionNotFoundError(file_path, section)
558
-
559
328
  if key not in config[original_section]:
560
329
  if default is not None:
561
330
  return default
562
331
  raise ConfigKeyNotFoundError(file_path, original_section, key)
563
-
564
332
  value = config[original_section][key]
565
-
566
333
  if value_type is not None:
567
- return self._convert_value(value, value_type)
568
-
334
+ return self._convert_value(value, value_type, file_path=file_path, key=key)
569
335
  return value
570
336
 
571
337
  def get_values(self, keys: List[Tuple[str, str]],
572
338
  file_path: Optional[Union[str, Path]] = None,
573
339
  defaults: Optional[Dict[str, Any]] = None,
574
340
  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
- """
341
+ """批量获取多个配置项的值"""
591
342
  if file_path is None:
592
343
  self._ensure_file_open()
593
344
  file_path = self._current_file
@@ -618,23 +369,7 @@ class ConfigParser:
618
369
  file_path: Optional[Union[str, Path]] = None,
619
370
  defaults: Optional[Dict[str, Any]] = None,
620
371
  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
- """
372
+ """获取指定节点下多个键的值元组"""
638
373
  if file_path is None:
639
374
  self._ensure_file_open()
640
375
  file_path = self._current_file
@@ -664,37 +399,20 @@ class ConfigParser:
664
399
  section: Optional[str] = None,
665
400
  file_path: Optional[Union[str, Path]] = None,
666
401
  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
- """
402
+ """设置指定配置项的值"""
680
403
  if file_path is None:
681
404
  self._ensure_file_open()
682
405
  file_path = self._current_file
683
406
  if not self._validate_key(key):
684
407
  raise ConfigValueError(f"无效的键名: {key}", file_path=file_path, key=key)
685
-
686
- # 读取原始文件內容
408
+ section = section or self.options.default_section
687
409
  original_lines = []
688
410
  if file_path.exists():
689
411
  with open(file_path, 'r', encoding=self.options.encoding) as file:
690
412
  original_lines = file.readlines()
691
-
692
- # 读取当前配置
693
413
  config = self.read(file_path)
694
-
695
414
  if section not in config:
696
415
  config[section] = {}
697
-
698
416
  if value_type is not None:
699
417
  try:
700
418
  if value_type == bool:
@@ -711,576 +429,165 @@ class ConfigParser:
711
429
  section=section,
712
430
  key=key
713
431
  )
714
-
715
432
  if isinstance(value, bool):
716
433
  value = str(value).lower()
717
434
  else:
718
435
  value = str(value)
719
-
720
- # 更新配置
721
436
  config[section][key] = value
722
-
723
- # 写入文件,保持原始格式
724
437
  try:
725
438
  file_path.parent.mkdir(parents=True, exist_ok=True)
726
-
727
439
  with open(file_path, 'w', encoding=self.options.encoding) as file:
728
440
  current_section = self.options.default_section
729
- section_separators = {} # 用于存储每个section使用的分隔符
730
-
731
- # 解析原始文件,提取格式信息
441
+ section_separators = {}
732
442
  for line in original_lines:
733
443
  stripped_line = line.strip()
734
-
735
444
  if not stripped_line:
736
- file.write(line) # 保持空行
445
+ file.write(line)
737
446
  continue
738
-
739
447
  if stripped_line.startswith('[') and stripped_line.endswith(']'):
740
448
  current_section = stripped_line[1:-1]
741
- file.write(line) # 保持节标记的原始格式
449
+ file.write(line)
742
450
  continue
743
-
744
451
  if self._is_comment_line(stripped_line):
745
- file.write(line) # 保持注释的原始格式
452
+ file.write(line)
746
453
  continue
747
-
748
454
  key_value = self._split_key_value(stripped_line)
749
455
  if key_value:
750
456
  orig_key, orig_value = key_value
751
- # 检测使用的分隔符
752
457
  for sep in self.options.separators:
753
458
  if sep in line:
754
459
  section_separators.setdefault(current_section, {})[orig_key] = sep
755
460
  break
756
-
757
- # 如果是当前要修改的键,则写入新值
758
461
  if current_section == section and orig_key == key:
759
462
  separator = section_separators.get(current_section, {}).get(orig_key, self.options.separators[0])
760
- # 提取行尾注释
761
463
  comment = ''
762
464
  for style in self.options.comment_styles:
763
465
  comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
764
466
  if comment_match:
765
467
  comment = comment_match.group(0)
766
468
  break
767
- # 写入新值并保留注释
768
469
  file.write(f'{key}{separator}{value}{comment}\n')
769
470
  else:
770
- file.write(line) # 保持其他行的原始格式
471
+ file.write(line)
771
472
  else:
772
- file.write(line) # 保持无法解析的行的原始格式
773
-
774
- # 如果section不存在,则添加新的section
473
+ file.write(line)
775
474
  if section not in [line.strip()[1:-1] for line in original_lines if line.strip().startswith('[') and line.strip().endswith(']')]:
776
475
  file.write(f'\n[{section}]\n')
777
476
  file.write(f'{key}={value}\n')
778
-
779
477
  self._clear_cache(str(file_path))
780
-
781
478
  except Exception as e:
782
479
  raise ConfigWriteError(file_path, e)
783
480
 
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
481
  def read(self, file_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
902
- """读取配置文件内容,支持继承、环境变量覆盖和模板处理"""
482
+ """读取配置文件内容"""
903
483
  if file_path is None:
904
484
  self._ensure_file_open()
905
485
  file_path = self._current_file
906
486
  else:
907
487
  file_path = Path(file_path)
908
-
909
- # 检查缓存
488
+
910
489
  cached_config = self._get_cached_config(str(file_path))
911
490
  if cached_config is not None:
912
491
  return cached_config
913
-
914
- # 读取基础配置
915
- base_config = {}
916
- if self.options.inherit_from:
917
- base_config = self.read(self.options.inherit_from)
918
-
492
+
919
493
  if not file_path.exists():
920
494
  if not self.options.auto_create:
921
495
  raise ConfigFileNotFoundError(file_path)
922
496
  logger.info(f'配置文件不存在,将创建: {file_path}')
923
497
  file_path.parent.mkdir(parents=True, exist_ok=True)
924
498
  file_path.touch()
925
- return base_config
926
-
499
+ return {}
500
+
927
501
  try:
928
502
  with open(file_path, 'r', encoding=self.options.encoding) as file:
929
- content = file.read()
930
-
931
- # 处理模板
932
- content = self._process_template(content)
503
+ config = {}
504
+ current_section = self.options.default_section
505
+ section_comments = []
933
506
 
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()
507
+ for line in file:
508
+ stripped_line = line.strip()
509
+
510
+ if not stripped_line or self._is_comment_line(stripped_line):
511
+ if self.options.preserve_comments:
512
+ section_comments.append(line.rstrip())
513
+ continue
514
+
515
+ if stripped_line.startswith('[') and stripped_line.endswith(']'):
516
+ current_section = stripped_line[1:-1]
517
+ if not self._validate_key(current_section):
518
+ raise ConfigValueError(
519
+ f"无效的节名: {current_section}",
520
+ file_path=file_path,
521
+ section=current_section
522
+ )
523
+ self._update_section_map(str(file_path), current_section)
524
+ if current_section not in config:
525
+ config[current_section] = {}
526
+ if self.options.preserve_comments:
527
+ self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).extend(section_comments)
528
+ section_comments = []
529
+ continue
530
+
531
+ key_value = self._split_key_value(stripped_line)
532
+ if key_value:
533
+ key, value = key_value
534
+ if not self._validate_key(key):
535
+ raise ConfigValueError(
536
+ f"无效的键名: {key}",
537
+ file_path=file_path,
538
+ section=current_section,
539
+ key=key
540
+ )
541
+ value, comment = self._extract_comment(value)
946
542
 
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
543
+ if self.options.strip_values:
544
+ value = value.strip()
951
545
 
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
546
+ if current_section not in config:
547
+ config[current_section] = {}
967
548
 
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)
549
+ config[current_section][key] = value
550
+ if self.options.preserve_comments and comment:
551
+ self._comments_cache.setdefault(str(file_path), {}).setdefault(current_section, []).append(comment)
552
+
1006
553
  self._update_cache(str(file_path), config)
1007
554
  return config
1008
-
555
+
1009
556
  except Exception as e:
1010
557
  raise ConfigReadError(file_path, e)
1011
558
 
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
559
 
1060
560
  def main() -> None:
1061
- """配置解析器使用示例"""
1062
-
1063
- # 示例1:基本使用
1064
- print("\n=== 示例1:基本使用 ===")
561
+ """示例用法"""
1065
562
  config_file = Path('/Users/xigua/spd.txt')
1066
-
1067
- with ConfigParser() as parser:
1068
- parser.open(config_file)
1069
- host, port, username, password = parser.get_section_values(
563
+ try:
564
+ # 方式1:使用上下文管理器
565
+ with ConfigParser() as parser:
566
+ parser.open(config_file)
567
+ host, port, username, password = parser.get_section_values(
568
+ keys=['host', 'port', 'username', 'password'],
569
+ section='mysql'
570
+ )
571
+ print("方式1结果:", host, port, username, password)
572
+ parser.set_value('username', 'root', section='mysql')
573
+ parser.set_value('port', 3306, section='mysql')
574
+ # 方式2:链式调用
575
+ parser = ConfigParser()
576
+ host, port, username, password = parser.open(config_file).get_section_values(
1070
577
  keys=['host', 'port', 'username', 'password'],
1071
578
  section='mysql'
1072
579
  )
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)
1163
-
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') }}
1176
-
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
- }
1189
- )
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}
1255
- )
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()
580
+ print("\n方式2结果:", host, port, username, password)
581
+ # 方式3:传统方式
582
+ parser = ConfigParser()
583
+ host, port, username, password = parser.get_section_values(
584
+ file_path=config_file,
585
+ section='mysql',
586
+ keys=['host', 'port', 'username', 'password']
587
+ )
588
+ print("\n方式3结果:", host, port, username, password)
589
+ except Exception as e:
590
+ print(f"配置文件操作异常: {e}")
1284
591
 
1285
592
 
1286
593
  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.23
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=DL1LeguYb9YeIqFpwczMwHkRTX7qAUfv5A5WC00M-Po,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=LQPNO5c9uATglZ1IrOgGslj6aSuAfpBXgHnPwvCrHmA,25482
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.23.dist-info/METADATA,sha256=Dx9QagRQoN_ttdDOi6RnTKDgSzfqBqn0bp7ozDrZq7Q,364
29
+ mdbq-4.0.23.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
30
+ mdbq-4.0.23.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
31
+ mdbq-4.0.23.dist-info/RECORD,,
File without changes