evm-cli 2.4.0__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.
- evm/__init__.py +55 -0
- evm/__main__.py +9 -0
- evm/_completion.py +253 -0
- evm/_crypto.py +177 -0
- evm/_groups.py +127 -0
- evm/_history.py +141 -0
- evm/_io.py +421 -0
- evm/_json.py +52 -0
- evm/_schema.py +265 -0
- evm/_typing.py +40 -0
- evm/cli.py +961 -0
- evm/exceptions.py +150 -0
- evm/formatters.py +285 -0
- evm/manager.py +644 -0
- evm_cli-2.4.0.dist-info/METADATA +748 -0
- evm_cli-2.4.0.dist-info/RECORD +20 -0
- evm_cli-2.4.0.dist-info/WHEEL +5 -0
- evm_cli-2.4.0.dist-info/entry_points.txt +2 -0
- evm_cli-2.4.0.dist-info/licenses/LICENSE +21 -0
- evm_cli-2.4.0.dist-info/top_level.txt +1 -0
evm/_schema.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
EVM Schema Mixin
|
|
4
|
+
|
|
5
|
+
变量 schema 定义和校验功能。
|
|
6
|
+
支持格式:url, email, port, integer, boolean, path, ipv4, ipv6
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ipaddress
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from ._typing import EnvironmentManagerProtocol
|
|
18
|
+
from .exceptions import SchemaError
|
|
19
|
+
|
|
20
|
+
# 内置格式校验正则
|
|
21
|
+
FORMAT_PATTERNS = {
|
|
22
|
+
'url': re.compile(
|
|
23
|
+
r'^https?://[^\s/$.?#].[^\s]*$', re.IGNORECASE
|
|
24
|
+
),
|
|
25
|
+
'email': re.compile(
|
|
26
|
+
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
27
|
+
),
|
|
28
|
+
'port': re.compile(r'^[0-9]{1,5}$'),
|
|
29
|
+
'integer': re.compile(r'^-?[0-9]+$'),
|
|
30
|
+
'boolean': re.compile(r'^(true|false|yes|no|1|0)$', re.IGNORECASE),
|
|
31
|
+
'path': re.compile(r'^[/~.].*|^[a-zA-Z]:\\.*'),
|
|
32
|
+
'ipv4': re.compile(
|
|
33
|
+
r'^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}'
|
|
34
|
+
r'(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$'
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# 环境变量 key 名校验正则
|
|
39
|
+
VALID_KEY_PATTERN = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*(?::[A-Za-z_][A-Za-z0-9_]*)*$')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_ipv6(value: str) -> bool:
|
|
43
|
+
"""#7: 使用标准库 ipaddress 校验 IPv6 地址"""
|
|
44
|
+
try:
|
|
45
|
+
ipaddress.IPv6Address(value)
|
|
46
|
+
return True
|
|
47
|
+
except (ipaddress.AddressValueError, ValueError):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SchemaMixin(EnvironmentManagerProtocol):
|
|
52
|
+
"""Schema mixin — 变量格式定义和校验"""
|
|
53
|
+
|
|
54
|
+
def _get_schema_file(self) -> Path:
|
|
55
|
+
"""获取 schema 文件路径"""
|
|
56
|
+
return self.env_file.parent / 'schema.json'
|
|
57
|
+
|
|
58
|
+
def _load_schema(self) -> dict:
|
|
59
|
+
"""加载 schema 定义
|
|
60
|
+
|
|
61
|
+
损坏时使用 warnings.warn() 通知调用者,而非直接 print(),
|
|
62
|
+
保持 mixin 不做 I/O 的设计原则。
|
|
63
|
+
"""
|
|
64
|
+
schema_file = self._get_schema_file()
|
|
65
|
+
if not schema_file.exists():
|
|
66
|
+
return {}
|
|
67
|
+
try:
|
|
68
|
+
with open(schema_file, encoding='utf-8') as f:
|
|
69
|
+
return json.load(f) # type: ignore[no-any-return]
|
|
70
|
+
except json.JSONDecodeError as e:
|
|
71
|
+
warnings.warn(
|
|
72
|
+
f"Schema file is corrupted ({e}). "
|
|
73
|
+
f"All schema definitions will be ignored until fixed.",
|
|
74
|
+
RuntimeWarning,
|
|
75
|
+
stacklevel=2,
|
|
76
|
+
)
|
|
77
|
+
return {}
|
|
78
|
+
except OSError as e:
|
|
79
|
+
warnings.warn(
|
|
80
|
+
f"Cannot read schema file ({e}). "
|
|
81
|
+
f"All schema definitions will be ignored.",
|
|
82
|
+
RuntimeWarning,
|
|
83
|
+
stacklevel=2,
|
|
84
|
+
)
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
def _save_schema(self, schema: dict) -> None:
|
|
88
|
+
"""保存 schema 定义"""
|
|
89
|
+
schema_file = self._get_schema_file()
|
|
90
|
+
try:
|
|
91
|
+
with open(schema_file, 'w', encoding='utf-8') as f:
|
|
92
|
+
json.dump(schema, f, indent=2, ensure_ascii=False)
|
|
93
|
+
os.chmod(str(schema_file), 0o600)
|
|
94
|
+
except OSError as e:
|
|
95
|
+
raise SchemaError(f"Failed to save schema: {e}")
|
|
96
|
+
|
|
97
|
+
def set_schema(
|
|
98
|
+
self,
|
|
99
|
+
key: str,
|
|
100
|
+
format: Optional[str] = None,
|
|
101
|
+
required: Optional[bool] = None,
|
|
102
|
+
pattern: Optional[str] = None,
|
|
103
|
+
description: Optional[str] = None,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""为变量设置 schema 定义
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
key: 变量名
|
|
109
|
+
format: 内置格式 (url/email/port/integer/boolean/path/ipv4/ipv6)
|
|
110
|
+
required: 是否必填
|
|
111
|
+
pattern: 自定义正则
|
|
112
|
+
description: 描述
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
确认消息
|
|
116
|
+
"""
|
|
117
|
+
available_formats = list(FORMAT_PATTERNS.keys()) + ['ipv6']
|
|
118
|
+
if format and format not in available_formats:
|
|
119
|
+
raise SchemaError(
|
|
120
|
+
f"Unknown format '{format}'. "
|
|
121
|
+
f"Available: {', '.join(sorted(available_formats))}",
|
|
122
|
+
key,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
schema = self._load_schema()
|
|
126
|
+
entry = schema.get(key, {})
|
|
127
|
+
|
|
128
|
+
if format is not None:
|
|
129
|
+
entry['format'] = format
|
|
130
|
+
if required is not None:
|
|
131
|
+
entry['required'] = required
|
|
132
|
+
if pattern is not None:
|
|
133
|
+
# 验证正则是否合法
|
|
134
|
+
try:
|
|
135
|
+
re.compile(pattern)
|
|
136
|
+
except re.error as e:
|
|
137
|
+
raise SchemaError(f"Invalid regex pattern: {e}", key)
|
|
138
|
+
entry['pattern'] = pattern
|
|
139
|
+
if description is not None:
|
|
140
|
+
entry['description'] = description
|
|
141
|
+
|
|
142
|
+
if not entry:
|
|
143
|
+
raise SchemaError("No schema properties specified", key)
|
|
144
|
+
|
|
145
|
+
schema[key] = entry
|
|
146
|
+
self._save_schema(schema)
|
|
147
|
+
return f"Schema set for '{key}': {json.dumps(entry, ensure_ascii=False)}"
|
|
148
|
+
|
|
149
|
+
def get_schema(self, key: Optional[str] = None) -> dict:
|
|
150
|
+
"""获取 schema 定义
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
key: 指定变量名(None 返回全部)
|
|
154
|
+
"""
|
|
155
|
+
schema = self._load_schema()
|
|
156
|
+
if key is not None:
|
|
157
|
+
if key not in schema:
|
|
158
|
+
raise SchemaError(f"No schema defined for '{key}'", key)
|
|
159
|
+
return {key: schema[key]}
|
|
160
|
+
return schema
|
|
161
|
+
|
|
162
|
+
def delete_schema(self, key: str) -> str:
|
|
163
|
+
"""删除变量的 schema 定义"""
|
|
164
|
+
schema = self._load_schema()
|
|
165
|
+
if key not in schema:
|
|
166
|
+
raise SchemaError(f"No schema defined for '{key}'", key)
|
|
167
|
+
del schema[key]
|
|
168
|
+
self._save_schema(schema)
|
|
169
|
+
return f"Schema removed for '{key}'"
|
|
170
|
+
|
|
171
|
+
def validate(
|
|
172
|
+
self, key: str, value: Optional[str] = None
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""校验变量值是否符合 schema
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
key: 变量名
|
|
178
|
+
value: 待校验值(None 则使用当前存储的值)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
{'valid': bool, 'errors': [...], 'warnings': [...]}
|
|
182
|
+
"""
|
|
183
|
+
schema = self._load_schema()
|
|
184
|
+
if key not in schema:
|
|
185
|
+
raise SchemaError(f"No schema defined for '{key}'", key)
|
|
186
|
+
|
|
187
|
+
if value is None:
|
|
188
|
+
if key not in self._env_vars:
|
|
189
|
+
entry = schema[key]
|
|
190
|
+
if entry.get('required', False):
|
|
191
|
+
return {
|
|
192
|
+
'valid': False,
|
|
193
|
+
'errors': [f"Required variable '{key}' is not set"],
|
|
194
|
+
'warnings': [],
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
'valid': True, 'errors': [],
|
|
198
|
+
'warnings': ['Variable not set (not required)'],
|
|
199
|
+
}
|
|
200
|
+
value = self._env_vars[key]
|
|
201
|
+
|
|
202
|
+
return self._validate_value(key, str(value), schema[key])
|
|
203
|
+
|
|
204
|
+
def validate_all(self) -> dict[str, dict]:
|
|
205
|
+
"""校验所有有 schema 定义的变量"""
|
|
206
|
+
schema = self._load_schema()
|
|
207
|
+
results = {}
|
|
208
|
+
|
|
209
|
+
for key, entry in schema.items():
|
|
210
|
+
if key in self._env_vars:
|
|
211
|
+
results[key] = self._validate_value(
|
|
212
|
+
key, str(self._env_vars[key]), entry
|
|
213
|
+
)
|
|
214
|
+
elif entry.get('required', False):
|
|
215
|
+
results[key] = {
|
|
216
|
+
'valid': False,
|
|
217
|
+
'errors': [f"Required variable '{key}' is not set"],
|
|
218
|
+
'warnings': [],
|
|
219
|
+
}
|
|
220
|
+
else:
|
|
221
|
+
results[key] = {
|
|
222
|
+
'valid': True,
|
|
223
|
+
'errors': [],
|
|
224
|
+
'warnings': ['Variable not set (not required)'],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return results
|
|
228
|
+
|
|
229
|
+
def _validate_value(
|
|
230
|
+
self, key: str, value: str, entry: dict
|
|
231
|
+
) -> dict:
|
|
232
|
+
"""内部:校验单个值"""
|
|
233
|
+
errors: list[str] = []
|
|
234
|
+
warnings: list[str] = []
|
|
235
|
+
|
|
236
|
+
# 格式校验
|
|
237
|
+
fmt = entry.get('format')
|
|
238
|
+
if fmt == 'ipv6':
|
|
239
|
+
# #7: 使用 ipaddress 标准库校验
|
|
240
|
+
if not validate_ipv6(value):
|
|
241
|
+
errors.append(
|
|
242
|
+
f"Value '{value}' does not match format 'ipv6'"
|
|
243
|
+
)
|
|
244
|
+
elif fmt and fmt in FORMAT_PATTERNS:
|
|
245
|
+
if not FORMAT_PATTERNS[fmt].match(value):
|
|
246
|
+
errors.append(
|
|
247
|
+
f"Value '{value}' does not match format '{fmt}'"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# 自定义正则校验
|
|
251
|
+
pattern = entry.get('pattern')
|
|
252
|
+
if pattern:
|
|
253
|
+
try:
|
|
254
|
+
if not re.match(pattern, value):
|
|
255
|
+
errors.append(
|
|
256
|
+
f"Value '{value}' does not match pattern '{pattern}'"
|
|
257
|
+
)
|
|
258
|
+
except re.error:
|
|
259
|
+
errors.append(f"Invalid schema regex: '{pattern}'")
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
'valid': len(errors) == 0,
|
|
263
|
+
'errors': errors,
|
|
264
|
+
'warnings': warnings,
|
|
265
|
+
}
|
evm/_typing.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
EVM 类型定义
|
|
4
|
+
|
|
5
|
+
定义 Mixin 类期望的接口协议。
|
|
6
|
+
Mixin 继承此协议后可消除 type: ignore[attr-defined] 注释,
|
|
7
|
+
并让 mypy 正确校验跨 mixin 的属性访问。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnvironmentManagerProtocol(Protocol):
|
|
15
|
+
"""EnvironmentManager 的协议定义,供 Mixin 类使用。
|
|
16
|
+
|
|
17
|
+
所有 mixin(IOMixin、GroupMixin、HistoryMixin、SchemaMixin)
|
|
18
|
+
在运行时期望宿主类提供以下属性和方法。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
env_file: Path
|
|
22
|
+
_env_vars: dict[str, str]
|
|
23
|
+
lock_timeout: float
|
|
24
|
+
|
|
25
|
+
def _save_env_vars(self, dry_run: bool = False) -> None:
|
|
26
|
+
"""保存环境变量到存储文件"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def log_operation(
|
|
30
|
+
self,
|
|
31
|
+
operation: str,
|
|
32
|
+
key: str = '',
|
|
33
|
+
details: str = '',
|
|
34
|
+
status: str = 'success',
|
|
35
|
+
) -> None:
|
|
36
|
+
"""记录操作日志"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ['EnvironmentManagerProtocol']
|