evolver-tools 1.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.
Files changed (69) hide show
  1. evolver_tools/__init__.py +2 -0
  2. evolver_tools/__main__.py +3 -0
  3. evolver_tools/cli.py +89 -0
  4. evolver_tools/vendor/b64/__init__.py +2 -0
  5. evolver_tools/vendor/b64/b64.py +176 -0
  6. evolver_tools/vendor/cal_tool/__init__.py +1 -0
  7. evolver_tools/vendor/cal_tool/cli.py +234 -0
  8. evolver_tools/vendor/chart_cli/__init__.py +444 -0
  9. evolver_tools/vendor/chart_cli/__main__.py +3 -0
  10. evolver_tools/vendor/colors/__init__.py +5 -0
  11. evolver_tools/vendor/colors/__main__.py +97 -0
  12. evolver_tools/vendor/csv_stats/__init__.py +5 -0
  13. evolver_tools/vendor/csv_stats/__main__.py +4 -0
  14. evolver_tools/vendor/csv_stats/analyzer.py +258 -0
  15. evolver_tools/vendor/csv_stats/cli.py +45 -0
  16. evolver_tools/vendor/dirsize/__init__.py +183 -0
  17. evolver_tools/vendor/envcheck/__init__.py +426 -0
  18. evolver_tools/vendor/ff/__init__.py +427 -0
  19. evolver_tools/vendor/ff/__main__.py +3 -0
  20. evolver_tools/vendor/find_dups/__init__.py +7 -0
  21. evolver_tools/vendor/find_dups/cli.py +392 -0
  22. evolver_tools/vendor/hashsum/__init__.py +211 -0
  23. evolver_tools/vendor/hashsum/__main__.py +5 -0
  24. evolver_tools/vendor/http_live/__init__.py +265 -0
  25. evolver_tools/vendor/http_live/__main__.py +2 -0
  26. evolver_tools/vendor/ipinfo/__init__.py +3 -0
  27. evolver_tools/vendor/ipinfo/__main__.py +30 -0
  28. evolver_tools/vendor/jq_lite/__init__.py +257 -0
  29. evolver_tools/vendor/jq_lite/__main__.py +5 -0
  30. evolver_tools/vendor/json2csv/__init__.py +3 -0
  31. evolver_tools/vendor/json2csv/__main__.py +82 -0
  32. evolver_tools/vendor/jsonql/__init__.py +326 -0
  33. evolver_tools/vendor/jsonql/__main__.py +5 -0
  34. evolver_tools/vendor/license_cli/__init__.py +1 -0
  35. evolver_tools/vendor/license_cli/__main__.py +4 -0
  36. evolver_tools/vendor/license_cli/cli.py +289 -0
  37. evolver_tools/vendor/markdown_check/__init__.py +211 -0
  38. evolver_tools/vendor/nb/__init__.py +319 -0
  39. evolver_tools/vendor/nb/__main__.py +3 -0
  40. evolver_tools/vendor/passgen/__init__.py +224 -0
  41. evolver_tools/vendor/portcheck/__init__.py +2 -0
  42. evolver_tools/vendor/portcheck/__main__.py +66 -0
  43. evolver_tools/vendor/project_doctor/__init__.py +412 -0
  44. evolver_tools/vendor/project_doctor/__main__.py +3 -0
  45. evolver_tools/vendor/ren/__init__.py +283 -0
  46. evolver_tools/vendor/ren/__main__.py +3 -0
  47. evolver_tools/vendor/siege_lite/__init__.py +250 -0
  48. evolver_tools/vendor/siege_lite/__main__.py +3 -0
  49. evolver_tools/vendor/smellfinder/__init__.py +376 -0
  50. evolver_tools/vendor/smellfinder/__main__.py +3 -0
  51. evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
  52. evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
  53. evolver_tools/vendor/sysmon/__init__.py +299 -0
  54. evolver_tools/vendor/sysmon/__main__.py +3 -0
  55. evolver_tools/vendor/timer/__init__.py +127 -0
  56. evolver_tools/vendor/treedir/__init__.py +2 -0
  57. evolver_tools/vendor/treedir/__main__.py +128 -0
  58. evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
  59. evolver_tools/vendor/urlparse_tool/cli.py +212 -0
  60. evolver_tools/vendor/web_summary/__init__.py +341 -0
  61. evolver_tools/vendor/web_summary/__main__.py +3 -0
  62. evolver_tools/vendor/wordcount/__init__.py +2 -0
  63. evolver_tools/vendor/wordcount/__main__.py +101 -0
  64. evolver_tools-1.4.0.dist-info/METADATA +107 -0
  65. evolver_tools-1.4.0.dist-info/RECORD +69 -0
  66. evolver_tools-1.4.0.dist-info/WHEEL +5 -0
  67. evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
  68. evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
  69. evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ envcheck — 环境变量验证器
4
+
5
+ 验证环境变量存在、格式、类型,检查默认值,显示缺失项。
6
+
7
+ Features:
8
+ - 检查必需变量是否存在
9
+ - 验证变量格式(email, url, port, path, regex模式)
10
+ - 转换为指定类型(int, float, bool, list)
11
+ - 检查默认值是否被覆盖
12
+ - 显示所有缺失的必需变量
13
+ - 0依赖,纯Python标准库
14
+
15
+ Usage:
16
+ envcheck # 扫描所有env文件并检查
17
+ envcheck path/to/.env # 检查指定env文件
18
+ envcheck --require DB_HOST DB_PORT # 指定必需变量
19
+ envcheck --template .env.example # 对比模板
20
+ envcheck --json # JSON输出(用于CI)
21
+ """
22
+
23
+ import os
24
+ import re
25
+ import sys
26
+ import json
27
+ import argparse
28
+
29
+ # ─── 格式验证器 ─────────────────────────────────────────────
30
+
31
+ VALIDATORS = {
32
+ "email": re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$"),
33
+ "url": re.compile(r"^https?://[^\s]+$"),
34
+ "port": re.compile(r"^\d+$"),
35
+ "integer": re.compile(r"^-?\d+$"),
36
+ "float": re.compile(r"^-?\d+(\.\d+)?$"),
37
+ "boolean": re.compile(r"^(true|false|yes|no|1|0)$", re.I),
38
+ "path": re.compile(r"^[/~.]"),
39
+ "hex_color": re.compile(r"^#[0-9a-fA-F]{3,8}$"),
40
+ "uuid": re.compile(
41
+ r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
42
+ ),
43
+ "semver": re.compile(r"^\d+\.\d+\.\d+"),
44
+ "datetime_iso": re.compile(
45
+ r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}"
46
+ ),
47
+ }
48
+
49
+ FORMAT_HELP = {
50
+ "email": "格式应为 user@domain.com",
51
+ "url": "格式应为 https://...",
52
+ "port": "应为数字 (1-65535)",
53
+ "integer": "应为整数",
54
+ "float": "应为数字(可含小数)",
55
+ "boolean": "应为 true/false/yes/no/1/0",
56
+ "path": "应为路径 (以 / ~ 或 . 开头)",
57
+ "hex_color": "应为 #RGB/#RRGGBB/#RRGGBBAA",
58
+ "uuid": "应为 UUID 格式",
59
+ "semver": "应为语义版本号 (x.y.z)",
60
+ "datetime_iso": "应为 ISO 日期时间",
61
+ }
62
+
63
+
64
+ def validate_format(value, fmt):
65
+ """验证值格式"""
66
+ pattern = VALIDATORS.get(fmt)
67
+ if not pattern:
68
+ return True, "未知格式检查"
69
+ if pattern.match(str(value).strip()):
70
+ return True, None
71
+ return False, FORMAT_HELP.get(fmt, f"格式不匹配: {fmt}")
72
+
73
+
74
+ def cast_type(value, target_type):
75
+ """类型转换"""
76
+ try:
77
+ if target_type == "int":
78
+ return int(value), True, None
79
+ elif target_type == "float":
80
+ return float(value), True, None
81
+ elif target_type == "bool":
82
+ return value.lower() in ("true", "yes", "1"), True, None
83
+ elif target_type == "list":
84
+ import shlex
85
+ parts = shlex.split(value)
86
+ return parts, True, None
87
+ else:
88
+ return value, True, None
89
+ except (ValueError, TypeError) as e:
90
+ return value, False, str(e)
91
+
92
+
93
+ # ─── .env 文件解析 ────────────────────────────────────────
94
+
95
+ def find_env_files(start_dir=None):
96
+ """递归查找 .env 文件"""
97
+ if start_dir is None:
98
+ start_dir = os.getcwd()
99
+ results = []
100
+ for root, dirs, files in os.walk(start_dir):
101
+ # 跳过隐藏目录和 node_modules, .git, __pycache__
102
+ dirs[:] = [d for d in dirs
103
+ if not d.startswith('.') and
104
+ d not in ('node_modules', '__pycache__', 'venv', '.venv')]
105
+ for f in files:
106
+ if f == '.env' or f.endswith('.env') or f == '.env.example':
107
+ results.append(os.path.join(root, f))
108
+ return results
109
+
110
+
111
+ def parse_env_file(filepath):
112
+ """解析 .env 文件"""
113
+ variables = {}
114
+ errors = []
115
+ try:
116
+ with open(filepath, "r") as f:
117
+ for i, line in enumerate(f, 1):
118
+ line = line.strip()
119
+ if not line or line.startswith("#"):
120
+ continue
121
+ if "=" not in line:
122
+ errors.append((i, line[:60], "格式错误: 缺少 ="))
123
+ continue
124
+ key, _, val = line.partition("=")
125
+ key = key.strip()
126
+ val = val.strip()
127
+ # 去掉引号
128
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in ('"', "'"):
129
+ val = val[1:-1]
130
+ variables[key] = val
131
+ return variables, errors
132
+ except FileNotFoundError:
133
+ return None, [(0, "", "文件未找到")]
134
+
135
+
136
+ # ─── 报告输出 ──────────────────────────────────────────────
137
+
138
+ def color(text, code):
139
+ if sys.stdout.isatty():
140
+ return f"\033[{code}m{text}\033[0m"
141
+ return text
142
+
143
+
144
+ def green(text):
145
+ return color(text, "92")
146
+
147
+
148
+ def red(text):
149
+ return color(text, "91")
150
+
151
+
152
+ def yellow(text):
153
+ return color(text, "93")
154
+
155
+
156
+ def blue(text):
157
+ return color(text, "94")
158
+
159
+
160
+ def print_report(result, json_output=False):
161
+ """打印检查报告"""
162
+ if json_output:
163
+ print(json.dumps(result, indent=2, default=str))
164
+ return
165
+
166
+ total = result.get("total", 0)
167
+ present = result.get("present", 0)
168
+ missing = result.get("missing", 0)
169
+ format_issues = result.get("format_issues", 0)
170
+ type_issues = result.get("type_issues", 0)
171
+
172
+ print()
173
+ print(f" {blue('envcheck')} — 环境变量检查报告")
174
+ print(f" {'='*40}")
175
+ print(f" 来源: {result.get('source', '未知')}")
176
+ print()
177
+
178
+ if result.get("errors"):
179
+ for err in result["errors"]:
180
+ print(f" {red('⚠')} 解析错误 (行 {err[0]}): {err[1]}")
181
+ print()
182
+
183
+ if missing > 0:
184
+ print(f" {red('✗')} 缺失变量 ({missing}):")
185
+ for v in result.get("missing_vars", []):
186
+ print(f" • {v}")
187
+ print()
188
+
189
+ if format_issues > 0:
190
+ print(f" {yellow('⚠')} 格式问题 ({format_issues}):")
191
+ for v in result.get("format_issues_list", []):
192
+ print(f" • {v['var']} = {v['value'][:40]} — {v['hint']}")
193
+ print()
194
+
195
+ if type_issues > 0:
196
+ print(f" {yellow('⚠')} 类型转换问题 ({type_issues}):")
197
+ for v in result.get("type_issues_list", []):
198
+ print(f" • {v['var']} = {v['value'][:40]} — {v['hint']}")
199
+ print()
200
+
201
+ print(f" {green('✓')} 已定义: {present}/{total}")
202
+
203
+ if result.get("has_defaults"):
204
+ overridden = result.get("overridden_defaults", 0)
205
+ print(f" {blue('ℹ')} 覆盖默认值: {overridden}")
206
+
207
+ # 总体评分
208
+ score = result.get("score", 100)
209
+ if score >= 90:
210
+ c = green
211
+ elif score >= 70:
212
+ c = yellow
213
+ else:
214
+ c = red
215
+ print(f" {c('★')} 健康度: {c(str(score))}/100")
216
+ print()
217
+
218
+
219
+ def check(args):
220
+ """主检查逻辑"""
221
+ # 确定检查范围
222
+ if args.file:
223
+ files = [args.file]
224
+ else:
225
+ files = find_env_files()
226
+
227
+ if not files:
228
+ result = {
229
+ "status": "error",
230
+ "message": "未找到 .env 文件",
231
+ "total": 0, "present": 0, "missing": 0,
232
+ "format_issues": 0, "type_issues": 0,
233
+ "score": 0,
234
+ "source": "无文件",
235
+ }
236
+ print_report(result, args.json)
237
+ return 1
238
+
239
+ # 处理每个文件
240
+ all_vars = {}
241
+ all_errors = []
242
+ source_file = files[0]
243
+
244
+ for f in files:
245
+ vars_dict, errs = parse_env_file(f)
246
+ if vars_dict is not None:
247
+ all_vars.update(vars_dict)
248
+ all_errors.extend([(f, e[0], e[1]) for e in errs])
249
+ if source_file == files[0] and vars_dict is not None:
250
+ source_file = f
251
+
252
+ # 必需变量检查
253
+ required_vars = list(args.require) if args.require else []
254
+ if args.template:
255
+ tmpl_vars, _ = parse_env_file(args.template)
256
+ if tmpl_vars is not None:
257
+ required_vars = list(tmpl_vars.keys())
258
+
259
+ # 如果没有指定必需变量,把所有变量视为可选
260
+ if not required_vars:
261
+ required_vars = list(all_vars.keys())
262
+
263
+ missing_vars = [v for v in required_vars if v not in all_vars]
264
+
265
+ # 格式验证
266
+ format_issues_list = []
267
+ type_issues_list = []
268
+ format_count = 0
269
+ type_count = 0
270
+
271
+ for var_name in all_vars:
272
+ val = all_vars[var_name]
273
+
274
+ # 按变量名推断格式
275
+ hints = _infer_format_checks(var_name, val)
276
+ for fmt in hints:
277
+ ok, hint = validate_format(val, fmt)
278
+ if not ok:
279
+ format_issues_list.append({
280
+ "var": var_name, "value": val,
281
+ "check": fmt, "hint": hint
282
+ })
283
+ format_count += 1
284
+
285
+ # 类型转换检查
286
+ type_hints = _infer_type_checks(var_name, val)
287
+ for t in type_hints:
288
+ _, ok, hint = cast_type(val, t)
289
+ if not ok:
290
+ type_issues_list.append({
291
+ "var": var_name, "value": val,
292
+ "target": t, "hint": hint
293
+ })
294
+ type_count += 1
295
+
296
+ # 默认值覆盖
297
+ overridden = 0
298
+ has_defaults = False
299
+ if args.template:
300
+ has_defaults = True
301
+ tmpl_vars, _ = parse_env_file(args.template)
302
+ if tmpl_vars:
303
+ for k in tmpl_vars:
304
+ if k in all_vars and all_vars[k] != tmpl_vars[k]:
305
+ overridden += 1
306
+
307
+ # 评分
308
+ checks = len(required_vars) + format_count + type_count
309
+ passed = len(required_vars) - len(missing_vars) + (format_count + type_count) * 0
310
+ score = max(0, int((passed / max(checks, 1)) * 100 - len(missing_vars) * 10))
311
+
312
+ result = {
313
+ "status": "warning" if (missing_vars or format_issues_list)
314
+ else "ok",
315
+ "source": str(source_file),
316
+ "total": len(required_vars) if required_vars else len(all_vars),
317
+ "present": len(all_vars),
318
+ "missing": len(missing_vars),
319
+ "missing_vars": missing_vars,
320
+ "format_issues": format_count,
321
+ "format_issues_list": format_issues_list,
322
+ "type_issues": type_count,
323
+ "type_issues_list": type_issues_list,
324
+ "has_defaults": has_defaults,
325
+ "overridden_defaults": overridden,
326
+ "score": score,
327
+ "errors": all_errors,
328
+ }
329
+
330
+ print_report(result, args.json)
331
+ return 0 if result["status"] == "ok" else 1
332
+
333
+
334
+ def _infer_format_checks(var_name, value):
335
+ """根据变量名推断应该检查什么格式"""
336
+ name_upper = var_name.upper()
337
+ checks = []
338
+
339
+ # 端口
340
+ if ("PORT" in name_upper or "PORT_" in name_upper or
341
+ name_upper.endswith("_PORT")):
342
+ checks.append("port")
343
+
344
+ # URL
345
+ if ("URL" in name_upper or "URI" in name_upper or
346
+ "ENDPOINT" in name_upper or "HOST" in name_upper):
347
+ checks.append("url")
348
+
349
+ # Email
350
+ if "EMAIL" in name_upper or "MAIL" in name_upper:
351
+ checks.append("email")
352
+
353
+ # 路径
354
+ if ("PATH" in name_upper or "DIR" in name_upper or
355
+ "FILE" in name_upper or "HOME" in name_upper):
356
+ checks.append("path")
357
+
358
+ # 颜色
359
+ if "COLOR" in name_upper or "COLOUR" in name_upper:
360
+ checks.append("hex_color")
361
+
362
+ # UUID
363
+ if "UUID" in name_upper or "GUID" in name_upper:
364
+ checks.append("uuid")
365
+
366
+ # 版本号
367
+ if "VERSION" in name_upper:
368
+ checks.append("semver")
369
+
370
+ return checks
371
+
372
+
373
+ def _infer_type_checks(var_name, value):
374
+ """根据变量名推断类型转换"""
375
+ name_upper = var_name.upper()
376
+ checks = []
377
+
378
+ if ("COUNT" in name_upper or "LIMIT" in name_upper or
379
+ "SIZE" in name_upper or "MAX" in name_upper or
380
+ "MIN" in name_upper or "TTL" in name_upper or
381
+ "TIMEOUT" in name_upper or "PORT" in name_upper):
382
+ checks.append("int")
383
+
384
+ if ("ENABLED" in name_upper or "DEBUG" in name_upper or
385
+ "DISABLE" in name_upper or "DRY_RUN" in name_upper or
386
+ "VERBOSE" in name_upper):
387
+ checks.append("bool")
388
+
389
+ if ("LIST" in name_upper or "ARGS" in name_upper or
390
+ "OPTIONS" in name_upper or "ITEMS" in name_upper):
391
+ checks.append("list")
392
+
393
+ return checks
394
+
395
+
396
+ def main():
397
+ parser = argparse.ArgumentParser(
398
+ description="环境变量验证器 — 检查 .env 文件中的变量",
399
+ formatter_class=argparse.RawDescriptionHelpFormatter,
400
+ epilog="""
401
+ 示例:
402
+ envcheck 扫描当前目录的 .env
403
+ envcheck .env.production 指定文件
404
+ envcheck --require DB_HOST DB_PORT 指定必需变量
405
+ envcheck --template .env.example 对比模板
406
+ envcheck --json JSON 输出
407
+ """,
408
+ )
409
+ parser.add_argument("file", nargs="?", help="指定 .env 文件路径")
410
+ parser.add_argument("--require", "-r", nargs="+", help="必需的变量名列表")
411
+ parser.add_argument("--template", "-t", help="参考模板文件")
412
+ parser.add_argument("--json", "-j", action="store_true", help="JSON 格式输出")
413
+ args = parser.parse_args()
414
+
415
+ try:
416
+ sys.exit(check(args))
417
+ except KeyboardInterrupt:
418
+ print()
419
+ sys.exit(130)
420
+ except Exception as e:
421
+ print(f"错误: {e}", file=sys.stderr)
422
+ sys.exit(1)
423
+
424
+
425
+ if __name__ == "__main__":
426
+ main()