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 +1 -1
- mdbq/myconf/myconf2.py +104 -771
- {mdbq-4.0.21.dist-info → mdbq-4.0.22.dist-info}/METADATA +1 -1
- {mdbq-4.0.21.dist-info → mdbq-4.0.22.dist-info}/RECORD +6 -6
- {mdbq-4.0.21.dist-info → mdbq-4.0.22.dist-info}/WHEEL +0 -0
- {mdbq-4.0.21.dist-info → mdbq-4.0.22.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
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
|
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
|
-
|
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
|
-
"""
|
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 = {}
|
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
|
926
|
-
|
524
|
+
return {}
|
525
|
+
|
927
526
|
try:
|
928
527
|
with open(file_path, 'r', encoding=self.options.encoding) as file:
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
content = self._process_template(content)
|
528
|
+
config = {}
|
529
|
+
current_section = self.options.default_section
|
530
|
+
section_comments = []
|
933
531
|
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
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
|
948
|
-
|
949
|
-
section_comments.append(line.rstrip())
|
950
|
-
continue
|
568
|
+
if self.options.strip_values:
|
569
|
+
value = value.strip()
|
951
570
|
|
952
|
-
if
|
953
|
-
current_section =
|
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
|
-
|
969
|
-
if
|
970
|
-
|
971
|
-
|
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("
|
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
|
-
|
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
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
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
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
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,12 +1,12 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
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=
|
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.
|
29
|
-
mdbq-4.0.
|
30
|
-
mdbq-4.0.
|
31
|
-
mdbq-4.0.
|
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
|
File without changes
|