foolproof-operation 1.1.1__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.
- foolproof_operation/__init__.py +18 -0
- foolproof_operation/__main__.py +31 -0
- foolproof_operation/cli.py +488 -0
- foolproof_operation/core/__init__.py +56 -0
- foolproof_operation/core/audit.py +179 -0
- foolproof_operation/core/context.py +548 -0
- foolproof_operation/core/events.py +119 -0
- foolproof_operation/core/loader.py +256 -0
- foolproof_operation/core/runtime.py +129 -0
- foolproof_operation/core/tasklog.py +157 -0
- foolproof_operation/flows.py +213 -0
- foolproof_operation/foolproof.composition.json +58 -0
- foolproof_operation/gui.py +1252 -0
- foolproof_operation/index/__init__.py +62 -0
- foolproof_operation/index/paths.py +264 -0
- foolproof_operation/index/probe.py +324 -0
- foolproof_operation/index/service.py +373 -0
- foolproof_operation/index/store.py +516 -0
- foolproof_operation/index/usn.py +685 -0
- foolproof_operation/panels.py +197 -0
- foolproof_operation/plugins/__init__.py +12 -0
- foolproof_operation/plugins/categories.py +252 -0
- foolproof_operation/plugins/installer.py +166 -0
- foolproof_operation/plugins/pip.py +276 -0
- foolproof_operation/plugins/reqtxt.py +129 -0
- foolproof_operation/plugins/scanner.py +368 -0
- foolproof_operation/plugins/search.py +497 -0
- foolproof_operation/requirements_categories.json +816 -0
- foolproof_operation/tools/everything/LICENSE +21 -0
- foolproof_operation/tools/everything/README.md +39 -0
- foolproof_operation/tools/everything/es.exe +0 -0
- foolproof_operation-1.1.1.dist-info/METADATA +113 -0
- foolproof_operation-1.1.1.dist-info/RECORD +37 -0
- foolproof_operation-1.1.1.dist-info/WHEEL +5 -0
- foolproof_operation-1.1.1.dist-info/entry_points.txt +2 -0
- foolproof_operation-1.1.1.dist-info/licenses/LICENSE +21 -0
- foolproof_operation-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""启动审计(fail-loud):把「配置/依赖/索引/日志」一次核完,给出可执行建议。
|
|
2
|
+
|
|
3
|
+
对应工作区既有的审计口径(crawl_hub 的 ``--check`` 启动审计、repocheck 的规则即数据):
|
|
4
|
+
**宁可启动时显式报错,也不要运行到一半才炸**。本模块只做**只读**检查,不改任何状态。
|
|
5
|
+
|
|
6
|
+
检查项:
|
|
7
|
+
|
|
8
|
+
| 项 | 判据 | 失败后果 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| 插件清单 | `foolproof.composition.json` 存在且能被解析 | error(无法装配) |
|
|
11
|
+
| 分类清单 | 结构校验(``plugins.categories.audit``) | error(清单即事实源) |
|
|
12
|
+
| 索引状态 | 各卷 ``ready`` / ``missing`` / ``denied`` | missing=warn(可 `envs --build`),denied=warn(降级到 es/walk) |
|
|
13
|
+
| 检索后端 | 三后端可用性 | 全不可用=error |
|
|
14
|
+
| es.exe | 文件在位 | warn(仅影响 es 后端) |
|
|
15
|
+
| 索引目录 | 可创建/可写 | warn(不影响只读检索) |
|
|
16
|
+
| 任务日志 | 可写 | warn(日志失败不影响主流程) |
|
|
17
|
+
|
|
18
|
+
用法:``python requirements.py cli doctor``(``--json`` 输出机读结果)。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from .context import Service
|
|
29
|
+
from .runtime import default_manifest_path
|
|
30
|
+
|
|
31
|
+
__all__ = ["AuditItem", "AuditService", "format_report", "LEVEL_ERROR", "LEVEL_WARN", "LEVEL_OK"]
|
|
32
|
+
|
|
33
|
+
LEVEL_ERROR = "error"
|
|
34
|
+
LEVEL_WARN = "warn"
|
|
35
|
+
LEVEL_OK = "ok"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AuditItem:
|
|
40
|
+
"""一条审计结果。"""
|
|
41
|
+
|
|
42
|
+
level: str
|
|
43
|
+
subject: str
|
|
44
|
+
detail: str
|
|
45
|
+
advice: str = ""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def ok(self) -> bool:
|
|
49
|
+
return self.level == LEVEL_OK
|
|
50
|
+
|
|
51
|
+
def as_dict(self) -> Dict[str, str]:
|
|
52
|
+
return {
|
|
53
|
+
"level": self.level,
|
|
54
|
+
"subject": self.subject,
|
|
55
|
+
"detail": self.detail,
|
|
56
|
+
"advice": self.advice,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_report(items: List[AuditItem]) -> str:
|
|
61
|
+
"""人类可读报告(``✓`` / ``!`` / ``✗``)。"""
|
|
62
|
+
marks = {LEVEL_OK: "✓", LEVEL_WARN: "!", LEVEL_ERROR: "✗"}
|
|
63
|
+
lines: List[str] = []
|
|
64
|
+
for item in items:
|
|
65
|
+
line = "[{0}] {1}:{2}".format(marks.get(item.level, "?"), item.subject, item.detail)
|
|
66
|
+
if item.advice:
|
|
67
|
+
line += " → {0}".format(item.advice)
|
|
68
|
+
lines.append(line)
|
|
69
|
+
errors = sum(1 for item in items if item.level == LEVEL_ERROR)
|
|
70
|
+
warns = sum(1 for item in items if item.level == LEVEL_WARN)
|
|
71
|
+
lines.append("审计结论:{0} 项通过 / {1} 项警告 / {2} 项错误".format(
|
|
72
|
+
sum(1 for item in items if item.ok), warns, errors))
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AuditService(Service):
|
|
77
|
+
"""启动审计服务(插件行 ``audit``)。可选依赖:``categories`` / ``fileindex`` / ``search`` / ``tasklog``。"""
|
|
78
|
+
|
|
79
|
+
name = "audit"
|
|
80
|
+
|
|
81
|
+
def __init__(self, ctx: Any, config: Any = None) -> None:
|
|
82
|
+
super().__init__(ctx, config)
|
|
83
|
+
self.manifest_path = (self.config or {}).get("manifest") or default_manifest_path()
|
|
84
|
+
|
|
85
|
+
# ── 单项检查 ─────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def _check_manifest(self) -> AuditItem:
|
|
88
|
+
path = self.manifest_path
|
|
89
|
+
if not os.path.isfile(path):
|
|
90
|
+
return AuditItem(LEVEL_ERROR, "插件清单", "不存在:{0}".format(path),
|
|
91
|
+
"恢复 foolproof.composition.json(或删掉它走内置默认清单)")
|
|
92
|
+
try:
|
|
93
|
+
with open(path, encoding="utf-8") as fh:
|
|
94
|
+
data = json.load(fh)
|
|
95
|
+
rows = data["plugins"] if isinstance(data, dict) else data
|
|
96
|
+
ids = [row.get("id") for row in rows if isinstance(row, dict)]
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
return AuditItem(LEVEL_ERROR, "插件清单", "解析失败:{0}".format(exc), "修好 JSON 再启动")
|
|
99
|
+
if not ids or any(not i for i in ids):
|
|
100
|
+
return AuditItem(LEVEL_ERROR, "插件清单", "存在缺 id 的行", "每行必须有非空 id")
|
|
101
|
+
return AuditItem(LEVEL_OK, "插件清单", "{0} 行:{1}".format(len(ids), ", ".join(ids)))
|
|
102
|
+
|
|
103
|
+
def _check_categories(self) -> AuditItem:
|
|
104
|
+
service = self.ctx.get("categories", None) if self.ctx is not None else None
|
|
105
|
+
if service is None:
|
|
106
|
+
return AuditItem(LEVEL_WARN, "分类清单", "categories 行未装配", "检查清单里是否有 categories 行")
|
|
107
|
+
problems = service.audit()
|
|
108
|
+
stats = service.stats()
|
|
109
|
+
if problems:
|
|
110
|
+
return AuditItem(LEVEL_ERROR, "分类清单", "{0} 项结构问题".format(len(problems)),
|
|
111
|
+
problems[0])
|
|
112
|
+
return AuditItem(
|
|
113
|
+
LEVEL_OK, "分类清单",
|
|
114
|
+
"{0} 分类 / {1} 库(自动发现 {2} 条)".format(
|
|
115
|
+
stats["categories"], stats["entries"], stats["auto_discovery"]),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _check_index(self) -> AuditItem:
|
|
119
|
+
service = self.ctx.get("fileindex", None) if self.ctx is not None else None
|
|
120
|
+
if service is None:
|
|
121
|
+
return AuditItem(LEVEL_WARN, "索引状态", "fileindex 行未装配", "检索将退回 es/walk")
|
|
122
|
+
try:
|
|
123
|
+
status = service.status()
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
return AuditItem(LEVEL_WARN, "索引状态", "状态读取失败:{0}".format(exc))
|
|
126
|
+
if not status:
|
|
127
|
+
return AuditItem(LEVEL_WARN, "索引状态", "未识别到任何 NTFS 卷", "非 NTFS 环境仅能走 walk")
|
|
128
|
+
ready = [v for v, s in status.items() if s.get("status") == "ready"]
|
|
129
|
+
denied = [v for v, s in status.items() if s.get("status") == "denied"]
|
|
130
|
+
missing = [v for v, s in status.items() if s.get("status") == "missing"]
|
|
131
|
+
detail = "ready={0} missing={1} denied={2}".format(
|
|
132
|
+
",".join(ready) or "-", ",".join(missing) or "-", ",".join(denied) or "-")
|
|
133
|
+
if missing and not ready:
|
|
134
|
+
return AuditItem(LEVEL_WARN, "索引状态", detail, "运行 cli envs --build 建索引")
|
|
135
|
+
if denied:
|
|
136
|
+
return AuditItem(LEVEL_WARN, "索引状态", detail, "被拒卷回退遍历;如需索引请以管理员运行")
|
|
137
|
+
return AuditItem(LEVEL_OK, "索引状态", detail)
|
|
138
|
+
|
|
139
|
+
def _check_backends(self) -> AuditItem:
|
|
140
|
+
service = self.ctx.get("search", None) if self.ctx is not None else None
|
|
141
|
+
if service is None:
|
|
142
|
+
return AuditItem(LEVEL_WARN, "检索后端", "search 行未装配", "环境发现将不可用")
|
|
143
|
+
rows = service.availability()
|
|
144
|
+
usable = [name for name, ok, _ in rows if ok]
|
|
145
|
+
detail = ";".join("{0}={1}".format(name, "可用" if ok else reason) for name, ok, reason in rows)
|
|
146
|
+
if not usable:
|
|
147
|
+
return AuditItem(LEVEL_ERROR, "检索后端", detail, "至少需要 walk 兜底可用")
|
|
148
|
+
return AuditItem(LEVEL_OK, "检索后端", detail)
|
|
149
|
+
|
|
150
|
+
def _check_tasklog(self) -> AuditItem:
|
|
151
|
+
probe = None
|
|
152
|
+
if self.ctx is not None:
|
|
153
|
+
holder = self.ctx.get("tasklog", None)
|
|
154
|
+
probe = getattr(holder, "log", holder)
|
|
155
|
+
if probe is None:
|
|
156
|
+
return AuditItem(LEVEL_WARN, "任务日志", "tasklog 行未装配", "操作流水不会被记录")
|
|
157
|
+
entry = probe.record("audit.probe")
|
|
158
|
+
if entry is None:
|
|
159
|
+
return AuditItem(LEVEL_WARN, "任务日志", "写入失败:{0}".format(probe.last_error),
|
|
160
|
+
"检查路径与权限(或设 FOOLPROOF_TASKLOG)")
|
|
161
|
+
return AuditItem(LEVEL_OK, "任务日志", probe.path)
|
|
162
|
+
|
|
163
|
+
# ── 汇总 ─────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
def run(self) -> List[AuditItem]:
|
|
166
|
+
"""执行全部检查(只读;唯一写入是任务日志的一条 `audit.probe`)。"""
|
|
167
|
+
return [
|
|
168
|
+
self._check_manifest(),
|
|
169
|
+
self._check_categories(),
|
|
170
|
+
self._check_index(),
|
|
171
|
+
self._check_backends(),
|
|
172
|
+
self._check_tasklog(),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
def failed(self, items: Optional[List[AuditItem]] = None) -> bool:
|
|
176
|
+
return any(item.level == LEVEL_ERROR for item in (items if items is not None else self.run()))
|
|
177
|
+
|
|
178
|
+
def __repr__(self) -> str:
|
|
179
|
+
return "<AuditService manifest={0}>".format(self.manifest_path)
|