mdbq 4.0.16__py3-none-any.whl → 4.0.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdbq/__version__.py +1 -1
- mdbq/myconf/__init__.py +4 -0
- mdbq/myconf/myconf.py +735 -0
- {mdbq-4.0.16.dist-info → mdbq-4.0.17.dist-info}/METADATA +1 -1
- {mdbq-4.0.16.dist-info → mdbq-4.0.17.dist-info}/RECORD +7 -5
- {mdbq-4.0.16.dist-info → mdbq-4.0.17.dist-info}/WHEEL +0 -0
- {mdbq-4.0.16.dist-info → mdbq-4.0.17.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
1
|
+
VERSION = '4.0.17'
|
mdbq/myconf/__init__.py
ADDED
mdbq/myconf/myconf.py
ADDED
@@ -0,0 +1,735 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, Union, List, Tuple, Type, TypeVar
|
3
|
+
from pathlib import Path
|
4
|
+
from mdbq.log import mylogger
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from enum import Enum
|
7
|
+
import time
|
8
|
+
|
9
|
+
logger = mylogger.MyLogger(
|
10
|
+
logging_mode='both',
|
11
|
+
log_level='info',
|
12
|
+
log_format='json',
|
13
|
+
max_log_size=50,
|
14
|
+
backup_count=5,
|
15
|
+
enable_async=False, # 是否启用异步日志
|
16
|
+
sample_rate=1, # 采样DEBUG/INFO日志
|
17
|
+
sensitive_fields=[], # 敏感字段过滤
|
18
|
+
enable_metrics=False, # 是否启用性能指标
|
19
|
+
)
|
20
|
+
|
21
|
+
T = TypeVar('T') # 类型变量
|
22
|
+
|
23
|
+
|
24
|
+
class ConfigError(Exception):
|
25
|
+
"""配置相关的基础异常类
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
message: 错误消息
|
29
|
+
file_path: 配置文件路径
|
30
|
+
section: 配置节名称
|
31
|
+
key: 配置键名称
|
32
|
+
"""
|
33
|
+
def __init__(self, message: str, file_path: Optional[Union[str, Path]] = None,
|
34
|
+
section: Optional[str] = None, key: Optional[str] = None):
|
35
|
+
self.message = message
|
36
|
+
self.file_path = str(file_path) if file_path else None
|
37
|
+
self.section = section
|
38
|
+
self.key = key
|
39
|
+
super().__init__(self._format_message())
|
40
|
+
|
41
|
+
def _format_message(self) -> str:
|
42
|
+
"""格式化错误消息"""
|
43
|
+
parts = [self.message]
|
44
|
+
if self.file_path:
|
45
|
+
parts.append(f"文件: {self.file_path}")
|
46
|
+
if self.section:
|
47
|
+
parts.append(f"节: [{self.section}]")
|
48
|
+
if self.key:
|
49
|
+
parts.append(f"键: {self.key}")
|
50
|
+
return " | ".join(parts)
|
51
|
+
|
52
|
+
|
53
|
+
class ConfigFileNotFoundError(ConfigError):
|
54
|
+
"""当指定的配置文件不存在时抛出的异常"""
|
55
|
+
def __init__(self, file_path: Union[str, Path]):
|
56
|
+
super().__init__("配置文件不存在", file_path=file_path)
|
57
|
+
|
58
|
+
|
59
|
+
class ConfigReadError(ConfigError):
|
60
|
+
"""当读取配置文件失败时抛出的异常
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
original_error: 原始错误对象
|
64
|
+
"""
|
65
|
+
def __init__(self, file_path: Union[str, Path], original_error: Exception):
|
66
|
+
super().__init__(
|
67
|
+
f"读取配置文件失败: {str(original_error)}",
|
68
|
+
file_path=file_path
|
69
|
+
)
|
70
|
+
self.original_error = original_error
|
71
|
+
|
72
|
+
|
73
|
+
class ConfigWriteError(ConfigError):
|
74
|
+
"""当写入配置文件失败时抛出的异常
|
75
|
+
|
76
|
+
Attributes:
|
77
|
+
original_error: 原始错误对象
|
78
|
+
"""
|
79
|
+
def __init__(self, file_path: Union[str, Path], original_error: Exception):
|
80
|
+
super().__init__(
|
81
|
+
f"写入配置文件失败: {str(original_error)}",
|
82
|
+
file_path=file_path
|
83
|
+
)
|
84
|
+
self.original_error = original_error
|
85
|
+
|
86
|
+
|
87
|
+
class ConfigValueError(ConfigError):
|
88
|
+
"""当配置值无效时抛出的异常"""
|
89
|
+
def __init__(self, message: str, file_path: Union[str, Path],
|
90
|
+
section: Optional[str] = None, key: Optional[str] = None):
|
91
|
+
super().__init__(message, file_path=file_path, section=section, key=key)
|
92
|
+
|
93
|
+
|
94
|
+
class ConfigSectionNotFoundError(ConfigError):
|
95
|
+
"""当指定的配置节不存在时抛出的异常"""
|
96
|
+
def __init__(self, file_path: Union[str, Path], section: str):
|
97
|
+
super().__init__(
|
98
|
+
f"配置节不存在",
|
99
|
+
file_path=file_path,
|
100
|
+
section=section
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
class ConfigKeyNotFoundError(ConfigError):
|
105
|
+
"""当指定的配置键不存在时抛出的异常"""
|
106
|
+
def __init__(self, file_path: Union[str, Path], section: str, key: str):
|
107
|
+
super().__init__(
|
108
|
+
f"配置键不存在",
|
109
|
+
file_path=file_path,
|
110
|
+
section=section,
|
111
|
+
key=key
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
class CommentStyle(Enum):
|
116
|
+
"""配置文件支持的注释风格"""
|
117
|
+
HASH = '#' # Python风格注释
|
118
|
+
DOUBLE_SLASH = '//' # C风格注释
|
119
|
+
SEMICOLON = ';' # INI风格注释
|
120
|
+
|
121
|
+
|
122
|
+
@dataclass
|
123
|
+
class ConfigOptions:
|
124
|
+
"""配置解析器的选项类
|
125
|
+
|
126
|
+
Attributes:
|
127
|
+
comment_styles: 支持的注释风格列表
|
128
|
+
encoding: 文件编码
|
129
|
+
auto_create: 是否自动创建不存在的配置文件
|
130
|
+
strip_values: 是否去除配置值的首尾空白
|
131
|
+
preserve_comments: 是否保留注释
|
132
|
+
default_section: 默认配置节名称
|
133
|
+
separators: 支持的分隔符列表
|
134
|
+
cache_ttl: 缓存过期时间(秒)
|
135
|
+
validate_keys: 是否验证键名
|
136
|
+
key_pattern: 键名正则表达式模式
|
137
|
+
case_sensitive: 是否区分大小写
|
138
|
+
"""
|
139
|
+
comment_styles: List[CommentStyle] = field(default_factory=lambda: [CommentStyle.HASH, CommentStyle.DOUBLE_SLASH])
|
140
|
+
encoding: str = 'utf-8'
|
141
|
+
auto_create: bool = False
|
142
|
+
strip_values: bool = True
|
143
|
+
preserve_comments: bool = True
|
144
|
+
default_section: str = 'DEFAULT'
|
145
|
+
separators: List[str] = field(default_factory=lambda: ['=', ':', ':'])
|
146
|
+
cache_ttl: int = 300 # 5分钟缓存过期
|
147
|
+
validate_keys: bool = True
|
148
|
+
key_pattern: str = r'^[a-zA-Z0-9_\-\.]+$'
|
149
|
+
case_sensitive: bool = False
|
150
|
+
|
151
|
+
|
152
|
+
class ConfigParser:
|
153
|
+
"""配置文件解析器,用于读取和写入配置文件
|
154
|
+
|
155
|
+
Attributes:
|
156
|
+
options: 解析器配置选项
|
157
|
+
_config_cache: 配置缓存,用于存储已读取的配置
|
158
|
+
_cache_timestamps: 缓存时间戳,用于管理缓存过期
|
159
|
+
_comments_cache: 注释缓存,用于存储每个配置节的注释
|
160
|
+
_section_map: 用于存储大小写映射
|
161
|
+
"""
|
162
|
+
|
163
|
+
def __init__(self, options: Optional[ConfigOptions] = None):
|
164
|
+
self.options = options or ConfigOptions()
|
165
|
+
self._config_cache: Dict[str, Dict[str, Any]] = {}
|
166
|
+
self._cache_timestamps: Dict[str, float] = {}
|
167
|
+
self._comments_cache: Dict[str, Dict[str, List[str]]] = {}
|
168
|
+
self._section_map: Dict[str, Dict[str, str]] = {} # 用于存储大小写映射
|
169
|
+
|
170
|
+
def _is_comment_line(self, line: str) -> bool:
|
171
|
+
"""判断一行是否为注释行"""
|
172
|
+
stripped = line.strip()
|
173
|
+
return any(stripped.startswith(style.value) for style in self.options.comment_styles)
|
174
|
+
|
175
|
+
def _extract_comment(self, line: str) -> Tuple[str, str]:
|
176
|
+
"""从行中提取注释
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
Tuple[str, str]: (去除注释后的行内容, 注释内容)
|
180
|
+
"""
|
181
|
+
for style in self.options.comment_styles:
|
182
|
+
comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
|
183
|
+
if comment_match:
|
184
|
+
return line[:comment_match.start()].strip(), comment_match.group(0)
|
185
|
+
return line.strip(), ''
|
186
|
+
|
187
|
+
def _split_key_value(self, line: str) -> Optional[Tuple[str, str]]:
|
188
|
+
"""分割配置行为键值对
|
189
|
+
|
190
|
+
Args:
|
191
|
+
line: 要分割的配置行
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
Optional[Tuple[str, str]]: 键值对元组,如果无法分割则返回None
|
195
|
+
"""
|
196
|
+
for sep in self.options.separators:
|
197
|
+
if sep in line:
|
198
|
+
key_part, value_part = line.split(sep, 1)
|
199
|
+
return key_part.strip(), value_part
|
200
|
+
|
201
|
+
for sep in [':', ':']:
|
202
|
+
if sep in line:
|
203
|
+
pattern = fr'\s*{re.escape(sep)}\s*'
|
204
|
+
parts = re.split(pattern, line, 1)
|
205
|
+
if len(parts) == 2:
|
206
|
+
return parts[0].strip(), parts[1]
|
207
|
+
|
208
|
+
return None
|
209
|
+
|
210
|
+
def _validate_key(self, key: str) -> bool:
|
211
|
+
"""验证键名是否合法"""
|
212
|
+
if not self.options.validate_keys:
|
213
|
+
return True
|
214
|
+
return bool(re.match(self.options.key_pattern, key))
|
215
|
+
|
216
|
+
def _get_cached_config(self, file_path: str) -> Optional[Dict[str, Any]]:
|
217
|
+
"""获取缓存的配置,如果过期则返回None"""
|
218
|
+
if file_path not in self._config_cache:
|
219
|
+
return None
|
220
|
+
|
221
|
+
if file_path not in self._cache_timestamps:
|
222
|
+
return None
|
223
|
+
|
224
|
+
if time.time() - self._cache_timestamps[file_path] > self.options.cache_ttl:
|
225
|
+
return None
|
226
|
+
|
227
|
+
return self._config_cache[file_path]
|
228
|
+
|
229
|
+
def _update_cache(self, file_path: str, config: Dict[str, Any]) -> None:
|
230
|
+
"""更新配置缓存"""
|
231
|
+
self._config_cache[file_path] = config
|
232
|
+
self._cache_timestamps[file_path] = time.time()
|
233
|
+
|
234
|
+
def _normalize_section(self, section: str) -> str:
|
235
|
+
"""标准化节名称(处理大小写)"""
|
236
|
+
if self.options.case_sensitive:
|
237
|
+
return section
|
238
|
+
return section.lower()
|
239
|
+
|
240
|
+
def _get_original_section(self, file_path: str, normalized_section: str) -> Optional[str]:
|
241
|
+
"""获取原始节名称"""
|
242
|
+
if self.options.case_sensitive:
|
243
|
+
return normalized_section
|
244
|
+
return self._section_map.get(file_path, {}).get(normalized_section)
|
245
|
+
|
246
|
+
def _update_section_map(self, file_path: str, section: str) -> None:
|
247
|
+
"""更新节名称映射"""
|
248
|
+
if not self.options.case_sensitive:
|
249
|
+
normalized = self._normalize_section(section)
|
250
|
+
if file_path not in self._section_map:
|
251
|
+
self._section_map[file_path] = {}
|
252
|
+
self._section_map[file_path][normalized] = section
|
253
|
+
|
254
|
+
def _clear_cache(self, file_path: Optional[str] = None) -> None:
|
255
|
+
"""清除配置缓存"""
|
256
|
+
if file_path:
|
257
|
+
self._config_cache.pop(file_path, None)
|
258
|
+
self._cache_timestamps.pop(file_path, None)
|
259
|
+
self._comments_cache.pop(file_path, None)
|
260
|
+
self._section_map.pop(file_path, None)
|
261
|
+
else:
|
262
|
+
self._config_cache.clear()
|
263
|
+
self._cache_timestamps.clear()
|
264
|
+
self._comments_cache.clear()
|
265
|
+
self._section_map.clear()
|
266
|
+
|
267
|
+
def _convert_value(self, value: str, target_type: Type[T]) -> T:
|
268
|
+
"""转换配置值到指定类型
|
269
|
+
|
270
|
+
Args:
|
271
|
+
value: 要转换的值
|
272
|
+
target_type: 目标类型
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
T: 转换后的值
|
276
|
+
|
277
|
+
Raises:
|
278
|
+
ConfigValueError: 当值无法转换为指定类型时
|
279
|
+
"""
|
280
|
+
try:
|
281
|
+
if target_type == bool:
|
282
|
+
return bool(value.lower() in ('true', 'yes', '1', 'on'))
|
283
|
+
elif target_type == list:
|
284
|
+
# 支持多种分隔符的列表
|
285
|
+
if not value.strip():
|
286
|
+
return []
|
287
|
+
# 尝试不同的分隔符
|
288
|
+
for sep in [',', ';', '|', ' ']:
|
289
|
+
if sep in value:
|
290
|
+
return [item.strip() for item in value.split(sep) if item.strip()]
|
291
|
+
# 如果没有分隔符,则作为单个元素返回
|
292
|
+
return [value.strip()]
|
293
|
+
elif target_type == tuple:
|
294
|
+
# 支持元组类型
|
295
|
+
if not value.strip():
|
296
|
+
return ()
|
297
|
+
# 尝试不同的分隔符
|
298
|
+
for sep in [',', ';', '|', ' ']:
|
299
|
+
if sep in value:
|
300
|
+
return tuple(item.strip() for item in value.split(sep) if item.strip())
|
301
|
+
# 如果没有分隔符,则作为单个元素返回
|
302
|
+
return (value.strip(),)
|
303
|
+
elif target_type == set:
|
304
|
+
# 支持集合类型
|
305
|
+
if not value.strip():
|
306
|
+
return set()
|
307
|
+
# 尝试不同的分隔符
|
308
|
+
for sep in [',', ';', '|', ' ']:
|
309
|
+
if sep in value:
|
310
|
+
return {item.strip() for item in value.split(sep) if item.strip()}
|
311
|
+
# 如果没有分隔符,则作为单个元素返回
|
312
|
+
return {value.strip()}
|
313
|
+
elif target_type == dict:
|
314
|
+
# 支持字典类型,格式:key1=value1,key2=value2
|
315
|
+
if not value.strip():
|
316
|
+
return {}
|
317
|
+
result = {}
|
318
|
+
# 尝试不同的分隔符
|
319
|
+
for sep in [',', ';', '|']:
|
320
|
+
if sep in value:
|
321
|
+
pairs = [pair.strip() for pair in value.split(sep) if pair.strip()]
|
322
|
+
for pair in pairs:
|
323
|
+
if '=' in pair:
|
324
|
+
key, val = pair.split('=', 1)
|
325
|
+
result[key.strip()] = val.strip()
|
326
|
+
return result
|
327
|
+
# 如果没有分隔符,尝试单个键值对
|
328
|
+
if '=' in value:
|
329
|
+
key, val = value.split('=', 1)
|
330
|
+
return {key.strip(): val.strip()}
|
331
|
+
return {}
|
332
|
+
elif target_type == int:
|
333
|
+
# 支持十六进制、八进制、二进制
|
334
|
+
value = value.strip().lower()
|
335
|
+
if value.startswith('0x'):
|
336
|
+
return int(value, 16)
|
337
|
+
elif value.startswith('0o'):
|
338
|
+
return int(value, 8)
|
339
|
+
elif value.startswith('0b'):
|
340
|
+
return int(value, 2)
|
341
|
+
return int(value)
|
342
|
+
elif target_type == float:
|
343
|
+
return float(value)
|
344
|
+
elif target_type == complex:
|
345
|
+
return complex(value)
|
346
|
+
elif target_type == bytes:
|
347
|
+
return value.encode('utf-8')
|
348
|
+
elif target_type == bytearray:
|
349
|
+
return bytearray(value.encode('utf-8'))
|
350
|
+
elif target_type == set:
|
351
|
+
return set(value.split(','))
|
352
|
+
elif target_type == frozenset:
|
353
|
+
return frozenset(value.split(','))
|
354
|
+
elif target_type == range:
|
355
|
+
# 支持 range 类型,格式:start:stop:step 或 start:stop
|
356
|
+
parts = value.split(':')
|
357
|
+
if len(parts) == 2:
|
358
|
+
return range(int(parts[0]), int(parts[1]))
|
359
|
+
elif len(parts) == 3:
|
360
|
+
return range(int(parts[0]), int(parts[1]), int(parts[2]))
|
361
|
+
raise ValueError("Invalid range format")
|
362
|
+
return target_type(value)
|
363
|
+
except (ValueError, TypeError) as e:
|
364
|
+
raise ConfigValueError(
|
365
|
+
f"无法将值 '{value}' 转换为类型 {target_type.__name__}",
|
366
|
+
file_path=None,
|
367
|
+
key=None
|
368
|
+
)
|
369
|
+
|
370
|
+
def get_value(self, file_path: Union[str, Path], key: str,
|
371
|
+
section: Optional[str] = None, default: Any = None,
|
372
|
+
value_type: Optional[Type[T]] = None) -> T:
|
373
|
+
"""获取指定配置项的值
|
374
|
+
|
375
|
+
Args:
|
376
|
+
file_path: 配置文件路径
|
377
|
+
key: 配置键
|
378
|
+
section: 配置节名称,如果为None则使用默认节
|
379
|
+
default: 当配置项不存在时返回的默认值
|
380
|
+
value_type: 期望的值的类型
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
T: 配置值
|
384
|
+
|
385
|
+
Raises:
|
386
|
+
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
387
|
+
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
388
|
+
ConfigValueError: 当值无法转换为指定类型时
|
389
|
+
"""
|
390
|
+
if not self._validate_key(key):
|
391
|
+
raise ConfigValueError(f"无效的键名: {key}", file_path=file_path, key=key)
|
392
|
+
|
393
|
+
config = self.read(file_path)
|
394
|
+
section = section or self.options.default_section
|
395
|
+
normalized_section = self._normalize_section(section)
|
396
|
+
|
397
|
+
# 获取原始节名称
|
398
|
+
original_section = self._get_original_section(str(file_path), normalized_section)
|
399
|
+
if original_section is None:
|
400
|
+
if default is not None:
|
401
|
+
return default
|
402
|
+
raise ConfigSectionNotFoundError(file_path, section)
|
403
|
+
|
404
|
+
if key not in config[original_section]:
|
405
|
+
if default is not None:
|
406
|
+
return default
|
407
|
+
raise ConfigKeyNotFoundError(file_path, original_section, key)
|
408
|
+
|
409
|
+
value = config[original_section][key]
|
410
|
+
|
411
|
+
if value_type is not None:
|
412
|
+
return self._convert_value(value, value_type)
|
413
|
+
|
414
|
+
return value
|
415
|
+
|
416
|
+
def get_values(self, file_path: Union[str, Path],
|
417
|
+
keys: List[Tuple[str, str]],
|
418
|
+
defaults: Optional[Dict[str, Any]] = None,
|
419
|
+
value_types: Optional[Dict[str, Type]] = None) -> Dict[str, Any]:
|
420
|
+
"""批量获取多个配置项的值
|
421
|
+
|
422
|
+
Args:
|
423
|
+
file_path: 配置文件路径
|
424
|
+
keys: 配置项列表,每个元素为 (section, key) 元组
|
425
|
+
defaults: 默认值字典,格式为 {key: default_value}
|
426
|
+
value_types: 值类型字典,格式为 {key: type}
|
427
|
+
|
428
|
+
Returns:
|
429
|
+
Dict[str, Any]: 配置值字典,格式为 {key: value}
|
430
|
+
|
431
|
+
Raises:
|
432
|
+
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
433
|
+
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
434
|
+
ConfigValueError: 当值无法转换为指定类型时
|
435
|
+
"""
|
436
|
+
defaults = defaults or {}
|
437
|
+
value_types = value_types or {}
|
438
|
+
result = {}
|
439
|
+
|
440
|
+
for section, key in keys:
|
441
|
+
try:
|
442
|
+
value = self.get_value(
|
443
|
+
file_path=file_path,
|
444
|
+
key=key,
|
445
|
+
section=section,
|
446
|
+
default=defaults.get(key),
|
447
|
+
value_type=value_types.get(key)
|
448
|
+
)
|
449
|
+
result[key] = value
|
450
|
+
except (ConfigSectionNotFoundError, ConfigKeyNotFoundError) as e:
|
451
|
+
if key in defaults:
|
452
|
+
result[key] = defaults[key]
|
453
|
+
else:
|
454
|
+
raise e
|
455
|
+
|
456
|
+
return result
|
457
|
+
|
458
|
+
def get_section_values(self, file_path: Union[str, Path],
|
459
|
+
keys: List[str],
|
460
|
+
section: Optional[str] = None,
|
461
|
+
defaults: Optional[Dict[str, Any]] = None,
|
462
|
+
value_types: Optional[Dict[str, Type]] = None) -> Tuple[Any, ...]:
|
463
|
+
"""获取指定节点下多个键的值元组
|
464
|
+
|
465
|
+
Args:
|
466
|
+
file_path: 配置文件路径
|
467
|
+
keys: 要获取的键列表
|
468
|
+
section: 配置节名称,默认为 DEFAULT
|
469
|
+
defaults: 默认值字典,格式为 {key: default_value}
|
470
|
+
value_types: 值类型字典,格式为 {key: type}
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
Tuple[Any, ...]: 按键列表顺序返回的值元组
|
474
|
+
|
475
|
+
Raises:
|
476
|
+
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
477
|
+
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
478
|
+
ConfigValueError: 当值无法转换为指定类型时
|
479
|
+
"""
|
480
|
+
defaults = defaults or {}
|
481
|
+
value_types = value_types or {}
|
482
|
+
result = []
|
483
|
+
|
484
|
+
for key in keys:
|
485
|
+
try:
|
486
|
+
value = self.get_value(
|
487
|
+
file_path=file_path,
|
488
|
+
key=key,
|
489
|
+
section=section,
|
490
|
+
default=defaults.get(key),
|
491
|
+
value_type=value_types.get(key)
|
492
|
+
)
|
493
|
+
result.append(value)
|
494
|
+
except (ConfigSectionNotFoundError, ConfigKeyNotFoundError) as e:
|
495
|
+
if key in defaults:
|
496
|
+
result.append(defaults[key])
|
497
|
+
else:
|
498
|
+
raise e
|
499
|
+
|
500
|
+
return tuple(result)
|
501
|
+
|
502
|
+
def set_value(self, file_path: Union[str, Path], key: str, value: Any,
|
503
|
+
section: Optional[str] = None, value_type: Optional[Type] = None) -> None:
|
504
|
+
"""設置指定配置項的值,保持原始文件的格式和註釋
|
505
|
+
|
506
|
+
Args:
|
507
|
+
file_path: 配置文件路徑
|
508
|
+
key: 配置鍵
|
509
|
+
value: 要設置的值
|
510
|
+
section: 配置節名稱,如果為None則使用默認節
|
511
|
+
value_type: 值的類型,用於驗證和轉換
|
512
|
+
|
513
|
+
Raises:
|
514
|
+
ConfigValueError: 當值無法轉換為指定類型時
|
515
|
+
ConfigError: 當其他配置錯誤發生時
|
516
|
+
"""
|
517
|
+
if not self._validate_key(key):
|
518
|
+
raise ConfigValueError(f"無效的鍵名: {key}", file_path=file_path, key=key)
|
519
|
+
|
520
|
+
file_path = Path(file_path)
|
521
|
+
str_path = str(file_path)
|
522
|
+
section = section or self.options.default_section
|
523
|
+
|
524
|
+
# 讀取原始文件內容
|
525
|
+
original_lines = []
|
526
|
+
if file_path.exists():
|
527
|
+
with open(file_path, 'r', encoding=self.options.encoding) as file:
|
528
|
+
original_lines = file.readlines()
|
529
|
+
|
530
|
+
# 讀取當前配置
|
531
|
+
config = self.read(file_path)
|
532
|
+
|
533
|
+
if section not in config:
|
534
|
+
config[section] = {}
|
535
|
+
|
536
|
+
if value_type is not None:
|
537
|
+
try:
|
538
|
+
if value_type == bool:
|
539
|
+
if isinstance(value, str):
|
540
|
+
value = value.lower() in ('true', 'yes', '1', 'on')
|
541
|
+
else:
|
542
|
+
value = bool(value)
|
543
|
+
else:
|
544
|
+
value = value_type(value)
|
545
|
+
except (ValueError, TypeError) as e:
|
546
|
+
raise ConfigValueError(
|
547
|
+
f"無法將值 '{value}' 轉換為類型 {value_type.__name__}",
|
548
|
+
file_path=file_path,
|
549
|
+
section=section,
|
550
|
+
key=key
|
551
|
+
)
|
552
|
+
|
553
|
+
if isinstance(value, bool):
|
554
|
+
value = str(value).lower()
|
555
|
+
else:
|
556
|
+
value = str(value)
|
557
|
+
|
558
|
+
# 更新配置
|
559
|
+
config[section][key] = value
|
560
|
+
|
561
|
+
# 寫入文件,保持原始格式
|
562
|
+
try:
|
563
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
564
|
+
|
565
|
+
with open(file_path, 'w', encoding=self.options.encoding) as file:
|
566
|
+
current_section = self.options.default_section
|
567
|
+
section_separators = {} # 用於存儲每個section使用的分隔符
|
568
|
+
|
569
|
+
# 解析原始文件,提取格式信息
|
570
|
+
for line in original_lines:
|
571
|
+
stripped_line = line.strip()
|
572
|
+
|
573
|
+
if not stripped_line:
|
574
|
+
file.write(line) # 保持空行
|
575
|
+
continue
|
576
|
+
|
577
|
+
if stripped_line.startswith('[') and stripped_line.endswith(']'):
|
578
|
+
current_section = stripped_line[1:-1]
|
579
|
+
file.write(line) # 保持節標記的原始格式
|
580
|
+
continue
|
581
|
+
|
582
|
+
if self._is_comment_line(stripped_line):
|
583
|
+
file.write(line) # 保持註釋的原始格式
|
584
|
+
continue
|
585
|
+
|
586
|
+
key_value = self._split_key_value(stripped_line)
|
587
|
+
if key_value:
|
588
|
+
orig_key, orig_value = key_value
|
589
|
+
# 檢測使用的分隔符
|
590
|
+
for sep in self.options.separators:
|
591
|
+
if sep in line:
|
592
|
+
section_separators.setdefault(current_section, {})[orig_key] = sep
|
593
|
+
break
|
594
|
+
|
595
|
+
# 如果是當前要修改的鍵,則寫入新值
|
596
|
+
if current_section == section and orig_key == key:
|
597
|
+
separator = section_separators.get(current_section, {}).get(orig_key, self.options.separators[0])
|
598
|
+
# 提取行尾註釋
|
599
|
+
comment = ''
|
600
|
+
for style in self.options.comment_styles:
|
601
|
+
comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
|
602
|
+
if comment_match:
|
603
|
+
comment = comment_match.group(0)
|
604
|
+
break
|
605
|
+
# 寫入新值並保留註釋
|
606
|
+
file.write(f'{key}{separator}{value}{comment}\n')
|
607
|
+
else:
|
608
|
+
file.write(line) # 保持其他行的原始格式
|
609
|
+
else:
|
610
|
+
file.write(line) # 保持無法解析的行的原始格式
|
611
|
+
|
612
|
+
# 如果section不存在,則添加新的section
|
613
|
+
if section not in [line.strip()[1:-1] for line in original_lines if line.strip().startswith('[') and line.strip().endswith(']')]:
|
614
|
+
file.write(f'\n[{section}]\n')
|
615
|
+
file.write(f'{key}={value}\n')
|
616
|
+
|
617
|
+
self._clear_cache(str_path)
|
618
|
+
|
619
|
+
except Exception as e:
|
620
|
+
raise ConfigWriteError(file_path, e)
|
621
|
+
|
622
|
+
def read(self, file_path: Union[str, Path]) -> Dict[str, Any]:
|
623
|
+
"""读取配置文件内容
|
624
|
+
|
625
|
+
Args:
|
626
|
+
file_path: 配置文件路径
|
627
|
+
|
628
|
+
Returns:
|
629
|
+
Dict[str, Any]: 配置字典,格式为 {section: {key: value}}
|
630
|
+
|
631
|
+
Raises:
|
632
|
+
ConfigFileNotFoundError: 当配置文件不存在且未启用自动创建时
|
633
|
+
ConfigReadError: 当读取配置文件失败时
|
634
|
+
"""
|
635
|
+
file_path = Path(file_path)
|
636
|
+
str_path = str(file_path)
|
637
|
+
|
638
|
+
# 检查缓存
|
639
|
+
cached_config = self._get_cached_config(str_path)
|
640
|
+
if cached_config is not None:
|
641
|
+
return cached_config
|
642
|
+
|
643
|
+
if not file_path.exists():
|
644
|
+
if not self.options.auto_create:
|
645
|
+
raise ConfigFileNotFoundError(file_path)
|
646
|
+
logger.info(f'配置文件不存在,将创建: {file_path}')
|
647
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
648
|
+
file_path.touch()
|
649
|
+
return {}
|
650
|
+
|
651
|
+
try:
|
652
|
+
with open(file_path, 'r', encoding=self.options.encoding) as file:
|
653
|
+
config = {}
|
654
|
+
current_section = self.options.default_section
|
655
|
+
section_comments = []
|
656
|
+
|
657
|
+
for line in file:
|
658
|
+
stripped_line = line.strip()
|
659
|
+
|
660
|
+
if not stripped_line or self._is_comment_line(stripped_line):
|
661
|
+
if self.options.preserve_comments:
|
662
|
+
section_comments.append(line.rstrip())
|
663
|
+
continue
|
664
|
+
|
665
|
+
if stripped_line.startswith('[') and stripped_line.endswith(']'):
|
666
|
+
current_section = stripped_line[1:-1]
|
667
|
+
if not self._validate_key(current_section):
|
668
|
+
raise ConfigValueError(
|
669
|
+
f"无效的节名: {current_section}",
|
670
|
+
file_path=file_path,
|
671
|
+
section=current_section
|
672
|
+
)
|
673
|
+
self._update_section_map(str_path, current_section)
|
674
|
+
if current_section not in config:
|
675
|
+
config[current_section] = {}
|
676
|
+
if self.options.preserve_comments:
|
677
|
+
self._comments_cache.setdefault(str_path, {}).setdefault(current_section, []).extend(section_comments)
|
678
|
+
section_comments = []
|
679
|
+
continue
|
680
|
+
|
681
|
+
key_value = self._split_key_value(stripped_line)
|
682
|
+
if key_value:
|
683
|
+
key, value = key_value
|
684
|
+
if not self._validate_key(key):
|
685
|
+
raise ConfigValueError(
|
686
|
+
f"无效的键名: {key}",
|
687
|
+
file_path=file_path,
|
688
|
+
section=current_section,
|
689
|
+
key=key
|
690
|
+
)
|
691
|
+
value, comment = self._extract_comment(value)
|
692
|
+
|
693
|
+
if self.options.strip_values:
|
694
|
+
value = value.strip()
|
695
|
+
|
696
|
+
if current_section not in config:
|
697
|
+
config[current_section] = {}
|
698
|
+
|
699
|
+
config[current_section][key] = value
|
700
|
+
if self.options.preserve_comments and comment:
|
701
|
+
self._comments_cache.setdefault(str_path, {}).setdefault(current_section, []).append(comment)
|
702
|
+
|
703
|
+
self._update_cache(str_path, config)
|
704
|
+
return config
|
705
|
+
|
706
|
+
except Exception as e:
|
707
|
+
raise ConfigReadError(file_path, e)
|
708
|
+
|
709
|
+
|
710
|
+
def main() -> None:
|
711
|
+
# 使用示例
|
712
|
+
parser = ConfigParser()
|
713
|
+
config_file = Path('/Users/xigua/spd.txt')
|
714
|
+
host, port, username, password = parser.get_section_values(
|
715
|
+
file_path=config_file,
|
716
|
+
section='mysql',
|
717
|
+
keys=['host', 'port', 'username', 'password'],
|
718
|
+
)
|
719
|
+
print(host, port, username, password)
|
720
|
+
|
721
|
+
parser.set_value(file_path=config_file, section='spider', key='is_spider', value=True, value_type=bool)
|
722
|
+
# try:
|
723
|
+
# # 读取配置
|
724
|
+
# config = parser.read(config_file)
|
725
|
+
# print("\n当前配置:")
|
726
|
+
# for section, items in config.items():
|
727
|
+
# print(f"\n[{section}]")
|
728
|
+
# for key, value in items.items():
|
729
|
+
# print(f"{key} = {value}")
|
730
|
+
# except ConfigError as e:
|
731
|
+
# logger.error(str(e))
|
732
|
+
|
733
|
+
|
734
|
+
if __name__ == '__main__':
|
735
|
+
main()
|
@@ -1,9 +1,11 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
2
|
+
mdbq/__version__.py,sha256=MPNDpNwokFp1w3PA1Fclq6UiOb9sciHiBWs7VkIWDWs,18
|
3
3
|
mdbq/aggregation/__init__.py,sha256=EeDqX2Aml6SPx8363J-v1lz0EcZtgwIBYyCJV6CcEDU,40
|
4
4
|
mdbq/aggregation/query_data.py,sha256=5FLUctZ-XBbtax3mDyFRhd0kaTKhUBDW5vrVq6uGKYk,166783
|
5
5
|
mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
|
6
6
|
mdbq/log/mylogger.py,sha256=9w_o5mYB3FooIxobq_lSa6oCYTKIhPxDFox-jeLtUHI,21714
|
7
|
+
mdbq/myconf/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
|
8
|
+
mdbq/myconf/myconf.py,sha256=b9ZYNl-i9l2fEQfI0q1IhXe6aobGC2FW9V3eOCacPvA,30190
|
7
9
|
mdbq/mysql/__init__.py,sha256=A_DPJyAoEvTSFojiI2e94zP0FKtCkkwKP1kYUCSyQzo,11
|
8
10
|
mdbq/mysql/deduplicator.py,sha256=kAnkI_vnN8CchgDQAFzeh0M0vLXE2oWq9SfDPNZZ3v0,73215
|
9
11
|
mdbq/mysql/mysql.py,sha256=pDg771xBugCMSTWeskIFTi3pFLgaqgyG3smzf-86Wn8,56772
|
@@ -22,7 +24,7 @@ mdbq/redis/__init__.py,sha256=YtgBlVSMDphtpwYX248wGge1x-Ex_mMufz4-8W0XRmA,12
|
|
22
24
|
mdbq/redis/getredis.py,sha256=vpBuNc22uj9Vr-_Dh25_wpwWM1e-072EAAIBdB_IpL0,23494
|
23
25
|
mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
|
24
26
|
mdbq/spider/aikucun.py,sha256=juOqpr_dHeE1RyjCu67VcpzoJAWMO7FKv0i8KiH8WUo,21552
|
25
|
-
mdbq-4.0.
|
26
|
-
mdbq-4.0.
|
27
|
-
mdbq-4.0.
|
28
|
-
mdbq-4.0.
|
27
|
+
mdbq-4.0.17.dist-info/METADATA,sha256=GxQtKv6FYuzDfKw-P3f9GsY8lyHdIo-UECCfCFJ5w_c,364
|
28
|
+
mdbq-4.0.17.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
29
|
+
mdbq-4.0.17.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
30
|
+
mdbq-4.0.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|