eraflow 0.1.0a1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deepaint-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ include README.md LICENSE NOTICE pyproject.toml
2
+ recursive-include src/eraflow *.py py.typed
3
+ prune docs
4
+ prune test
5
+ prune tmp
6
+ prune .venv
7
+ prune .github
8
+ global-exclude *.pyc __pycache__
eraflow-0.1.0a1/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ eraflow 采用 MIT License,完整许可见 LICENSE;用户于 2026-10-30 明确授权采用 MIT。
2
+ ERAF 公共实现提取自 CommonLib,提交 6345822245da9df10f71e4f2b3e7fcdb954eb9b3,保留源码中的原始来源声明。
3
+ Copyright (c) 2026 deepaint-ai
4
+ cryptography、gevent、Flask、Werkzeug 是独立依赖,不复制其实现;各自许可证不被本声明替换。
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: eraflow
3
+ Version: 0.1.0a1
4
+ Summary: ERAF 协议的 Python 接入 SDK,统一提供身份与凭据自动安装、轮换恢复、Edge 请求验签、实时鉴权及运行状态诊断,简化服务的 External API 接入。
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/deepaint-ai/eraflow
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ Provides-Extra: flask
16
+ Provides-Extra: dev
17
+ License-File: LICENSE
18
+ License-File: NOTICE
19
+
20
+ # eraflow
21
+
22
+ ERAF 协议的 Python 接入 SDK,统一提供身份与凭据自动安装、轮换恢复、Edge 请求验签、实时鉴权及运行状态诊断,简化服务的 External API 接入。
23
+
24
+ 当前发行候选为 0.1.0a1(Alpha 预发布),供隔离验证与下游接入评估;不代表生产接线已完成。最低 Python 3.7,完整文件安装仅面向 Windows/Linux;macOS 未支持。纯 Python wheel 标签不代表所有平台安装语义可用。
25
+
26
+ ```python
27
+ from eraflow import Provider, ProviderOperation, prepare_external_target
28
+ from eraflow.flask import install_provider
29
+ # service_root、静态身份、UAC 配置与安全回调由宿主明确提供。
30
+ ```
31
+
32
+ 核心不隐式导入 Flask,不在导入时启动任务或联网。管理员保护、真实 socket/原始请求事实、共享防重放、领域授权及写操作最终复核均须由宿主接线。
33
+
34
+ 源码来自固定 CommonLib 提交,保留材料协议和 data/.cache 布局。参见源码仓 docs/README.md 的接入、迁移、验证和发布说明。采用 MIT License,见 LICENSE;来源及依赖声明见 NOTICE。
@@ -0,0 +1,15 @@
1
+ # eraflow
2
+
3
+ ERAF 协议的 Python 接入 SDK,统一提供身份与凭据自动安装、轮换恢复、Edge 请求验签、实时鉴权及运行状态诊断,简化服务的 External API 接入。
4
+
5
+ 当前发行候选为 0.1.0a1(Alpha 预发布),供隔离验证与下游接入评估;不代表生产接线已完成。最低 Python 3.7,完整文件安装仅面向 Windows/Linux;macOS 未支持。纯 Python wheel 标签不代表所有平台安装语义可用。
6
+
7
+ ```python
8
+ from eraflow import Provider, ProviderOperation, prepare_external_target
9
+ from eraflow.flask import install_provider
10
+ # service_root、静态身份、UAC 配置与安全回调由宿主明确提供。
11
+ ```
12
+
13
+ 核心不隐式导入 Flask,不在导入时启动任务或联网。管理员保护、真实 socket/原始请求事实、共享防重放、领域授权及写操作最终复核均须由宿主接线。
14
+
15
+ 源码来自固定 CommonLib 提交,保留材料协议和 data/.cache 布局。参见源码仓 docs/README.md 的接入、迁移、验证和发布说明。采用 MIT License,见 LICENSE;来源及依赖声明见 NOTICE。
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=67.8", "wheel>=0.42"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "eraflow"
7
+ dynamic = ["version"]
8
+ description = "ERAF 协议的 Python 接入 SDK,统一提供身份与凭据自动安装、轮换恢复、Edge 请求验签、实时鉴权及运行状态诊断,简化服务的 External API 接入。"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.7"
12
+ dependencies = ["cryptography>=43.0.3,<47", "gevent>=22.10.2,<27", "requests>=2.20,<3"]
13
+ classifiers = ["License :: OSI Approved :: MIT License", "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only"]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/deepaint-ai/eraflow"
17
+
18
+ [project.optional-dependencies]
19
+ flask = ["Flask>=2.2.5,<4", "Werkzeug>=2.2.3,<4"]
20
+ dev = ["build==0.10.0; python_version < '3.8'", "build>=1,<2; python_version >= '3.8'", "mypy==1.4.1", "pytest==7.4.4; python_version < '3.8'", "pytest>=8,<10; python_version >= '3.8'", "ruff==0.8.6", "types-requests==2.31.0.6", "jsonschema>=4.17,<5", "setuptools>=67.8", "wheel>=0.42", "SQLAlchemy>=1.4,<2", "PyJWT>=2,<3", "arrow<1", "redis==4.5.5", "pywin32>=300; sys_platform == 'win32'"]
21
+
22
+ [tool.setuptools.dynamic]
23
+ version = {attr = "eraflow._version.__version__"}
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+ [tool.setuptools.package-data]
27
+ eraflow = ["py.typed"]
28
+ [tool.setuptools]
29
+ license-files = ["LICENSE", "NOTICE"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["test"]
33
+ addopts = "-ra -p no:cacheprovider --basetemp=tmp/pytest"
34
+
35
+ [tool.ruff]
36
+ target-version = "py37"
37
+ line-length = 100
38
+ cache-dir = "tmp/ruff-cache"
39
+ [tool.ruff.lint]
40
+ select = ["E4", "E7", "E9", "F", "I"]
41
+
42
+ [tool.mypy]
43
+ python_version = "3.7"
44
+ files = ["src/eraflow"]
45
+ check_untyped_defs = true
46
+ disallow_untyped_defs = true
47
+ strict_optional = false
48
+ no_implicit_optional = false
49
+ ignore_missing_imports = true
50
+ cache_dir = "tmp/mypy-cache"
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ "test/*.py" = ["E402"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ # coding=utf-8
2
+ """ERAF 协议的 Python 接入 SDK;导入不启动任务或访问网络。"""
3
+
4
+ from ._version import __version__
5
+ from .materials import (
6
+ ConsumerTarget,
7
+ CredentialBytes,
8
+ ERAFIdentityMaterialError,
9
+ NoPackageError,
10
+ TrustedArtifact,
11
+ )
12
+ from .provider import (
13
+ Admission,
14
+ HTTPRequestBytes,
15
+ Provider,
16
+ ProviderAccessError,
17
+ ProviderOperation,
18
+ UnavailableProvider,
19
+ )
20
+ from .runtime import ExternalRuntime, ExternalStartupError, MaterialView, prepare_external_target
21
+
22
+ __all__ = [
23
+ "__version__",
24
+ "ConsumerTarget",
25
+ "CredentialBytes",
26
+ "ERAFIdentityMaterialError",
27
+ "NoPackageError",
28
+ "TrustedArtifact",
29
+ "Admission",
30
+ "HTTPRequestBytes",
31
+ "Provider",
32
+ "ProviderAccessError",
33
+ "ProviderOperation",
34
+ "UnavailableProvider",
35
+ "ExternalRuntime",
36
+ "MaterialView",
37
+ "ExternalStartupError",
38
+ "prepare_external_target",
39
+ ]
@@ -0,0 +1,4 @@
1
+ # coding=utf-8
2
+ """发行版本的唯一来源。"""
3
+
4
+ __version__ = "0.1.0a1"
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+ # CommonLib
4
+
5
+ """External 公共 Flask 接线:安装入口、请求保护装饰器与 /external/status 路由。
6
+
7
+ 来源实现:v1.1.0;发行版本统一由 eraflow._version 提供。
8
+ Updated: 2026-10-23
9
+ 变更记录:
10
+ v1.1.0 | 2026-10-23 | HTTP 接纳使用服务器原始事实和原请求预算,有限缓冲正文并将同一字节交给业务;拒绝编码及流式入口。
11
+ v1.0.4 | 2026-10-20 | 公共文件简化为 provider_api/provider_lib,更新唯一引用;业务行为、协议与鉴权不变。
12
+ v1.0.3 | 2026-10-20 | 公共文件迁入 External 子目录,固定新引用和服务根定位;协议、鉴权与生命周期不变。
13
+ v1.0.2 | 2026-10-20 | 唯一安装入口承接无可信制品的公共失败 Status,保留管理员预算和保护拒绝链
14
+ v1.0.1 | 2026-10-19 | 仅 External 强制禁止缓存,统一脱敏 HTTPException,保留内部历史响应并拒绝异实例重复安装
15
+ v1.0.0 | 2026-10-19 | 首版:install_provider 向 app.extensions['commonlib.external']
16
+ 注册生命周期(serv_process 监听前 start/退出 stop);
17
+ ExternalHTTP.protect(operation_id) 静态操作保护装饰器——
18
+ 请求入口建统一单调 deadline、消费 commonlib.socket_facts、
19
+ 解析唯一 Authorization(Bearer/DPoP)与 Edge 证明头、调用
20
+ Provider.authorize 一次内省并保存请求级 Admission;
21
+ read 操作在有限响应发出前自动复核,write 操作由业务在最后
22
+ 可拒绝点显式 Admission.recheck();/external/status 复用既有
23
+ 管理员 Bearer guard(libs.token_auth_lib),只读、no-store、
24
+ 拒绝 query/body/Transfer-Encoding,HEAD 同鉴权、OPTIONS 不给
25
+ 详情、清除 CORS;错误响应稳定脱敏,不回显 Token/异常正文
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import io
31
+ import re
32
+ import time
33
+ import uuid
34
+ from functools import wraps
35
+ from typing import Any, Callable, Optional
36
+
37
+ from flask import Blueprint, Response, g, jsonify, request
38
+ from werkzeug.exceptions import HTTPException
39
+
40
+ from .provider import (
41
+ MAX_HTTP_BODY_BYTES,
42
+ STATUS_TOTAL_TIMEOUT,
43
+ Admission,
44
+ HTTPRequestBytes,
45
+ Provider,
46
+ ProviderAccessError,
47
+ UnavailableProvider,
48
+ _http_request_fields,
49
+ )
50
+
51
+ _AUTHORIZATION_PATTERN = re.compile(r"(Bearer|DPoP) ([\x21-\x2b\x2d-\x7e]+)")
52
+ _ERROR_STATUS = {"authentication": 401, "authorization": 403, "dependency": 503}
53
+ _ERROR_MESSAGE = {
54
+ "authentication": "外部请求认证未通过",
55
+ "authorization": "外部请求无操作权限",
56
+ "dependency": "外部依赖暂不可用",
57
+ }
58
+
59
+
60
+ def _request_id() -> Any:
61
+ return uuid.uuid4().hex
62
+
63
+
64
+ def _error_response(status: Any, code: Any, correlation_id: Any) -> Any:
65
+ response = jsonify({"code": code, "message": "请求未能完成", "request_id": correlation_id})
66
+ response.status_code = status
67
+ response.headers["X-Request-Id"] = correlation_id
68
+ response.headers["Cache-Control"] = "no-store"
69
+ return response
70
+
71
+
72
+ def _access_error_response(error: Any) -> Any:
73
+ status = _ERROR_STATUS.get(error.category, 503)
74
+ response = jsonify(
75
+ {
76
+ "code": "external_" + error.category,
77
+ "message": _ERROR_MESSAGE.get(error.category, "请求未能完成"),
78
+ "request_id": _request_id(),
79
+ }
80
+ )
81
+ response.status_code = status
82
+ if status == 401:
83
+ response.headers["WWW-Authenticate"] = "Bearer"
84
+ response.headers["Cache-Control"] = "no-store"
85
+ return response
86
+
87
+
88
+ def _strip_external_cors(response: Any) -> Any:
89
+ """External 响应不保留全局历史 CORS 许可。"""
90
+ if request.path == "/external" or request.path.startswith("/external/"):
91
+ for name in (
92
+ "Access-Control-Allow-Origin",
93
+ "Access-Control-Allow-Headers",
94
+ "Access-Control-Allow-Methods",
95
+ "Access-Control-Allow-Credentials",
96
+ "Access-Control-Expose-Headers",
97
+ "Access-Control-Max-Age",
98
+ ):
99
+ response.headers.pop(name, None)
100
+ response.headers["Cache-Control"] = "no-store"
101
+ return response
102
+
103
+
104
+ class ExternalHTTP(object):
105
+ """安装入口返回的请求保护面;只在受保护请求内暴露 Admission。"""
106
+
107
+ def __init__(self, app: Any, provider: Any) -> None:
108
+ self.app = app
109
+ self.provider = provider
110
+
111
+ @property
112
+ def admission(self) -> Admission:
113
+ """本请求本操作的不可变接纳上下文;未受保护请求/跨请求读取均失败。"""
114
+ admission = getattr(g, "external_admission", None)
115
+ if not isinstance(admission, Admission):
116
+ raise ProviderAccessError("dependency", "admission_missing")
117
+ return admission
118
+
119
+ def protect(self, operation_id: str) -> Callable[..., Any]:
120
+ operation = self.provider.operations.get(operation_id)
121
+ if operation is None:
122
+ raise ValueError("unknown external operation: %s" % operation_id)
123
+
124
+ def decorate(handler: Any) -> Any:
125
+ endpoint = handler.__name__
126
+
127
+ @wraps(handler)
128
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
129
+ if request.endpoint is None or request.endpoint.rsplit(".", 1)[-1] != (
130
+ operation.endpoint if operation.endpoint else endpoint
131
+ ):
132
+ return _error_response(405, "method_not_allowed", _request_id())
133
+ if request.method not in operation.methods:
134
+ return _error_response(405, "method_not_allowed", _request_id())
135
+ started = time.monotonic()
136
+ inbound = self.provider.inbound_seconds
137
+ deadline = started + inbound if inbound is not None else None
138
+ values = request.headers.getlist("Authorization")
139
+ match = _AUTHORIZATION_PATTERN.fullmatch(values[0]) if len(values) == 1 else None
140
+ if match is None or request.headers.getlist("DPoP"):
141
+ return _access_error_response(
142
+ ProviderAccessError("authentication", "malformed_token")
143
+ )
144
+ scheme, token = match.groups()
145
+ attestation_values = request.headers.getlist("X-SMAI-Transport-Attestation")
146
+ attestation = attestation_values[0] if len(attestation_values) == 1 else None
147
+ if len(attestation_values) > 1:
148
+ return _access_error_response(
149
+ ProviderAccessError("authentication", "attestation_malformed")
150
+ )
151
+ facts = request.environ.get("commonlib.socket_facts")
152
+ try:
153
+ captured = None
154
+ if type(facts) is dict and facts.get("tls") is False:
155
+ raw = request.environ.get("commonlib.http_request_bytes")
156
+ if (
157
+ type(raw) is not dict
158
+ or raw.get("input") is not request.environ["wsgi.input"]
159
+ or raw.get("method") != request.method
160
+ or raw.get("target") != facts.get("raw_target")
161
+ or type(raw.get("started")) not in (int, float)
162
+ or raw["started"] > started
163
+ or deadline is None
164
+ or "_cached_data" in request.__dict__
165
+ or getattr(raw["input"], "position", 0) != 0
166
+ ):
167
+ raise ProviderAccessError("dependency", "socket_facts")
168
+ deadline = min(deadline, raw["started"] + inbound)
169
+ headers = raw["headers"]
170
+
171
+ def values_for(name: Any) -> Any:
172
+ return [v for k, v in headers if k.lower() == name]
173
+
174
+ if (
175
+ values_for("authorization") != values
176
+ or values_for("x-smai-transport-attestation") != attestation_values
177
+ ):
178
+ raise ProviderAccessError("authentication", "attestation_malformed")
179
+ # 先检查 framing,再在原预算内读取;不会等待未声明长度的流。
180
+ lengths = values_for("content-length")
181
+ if (
182
+ len(lengths) > 1
183
+ or lengths
184
+ and not re.fullmatch("0|[1-9][0-9]*", lengths[0])
185
+ or any(
186
+ values_for(n)
187
+ for n in (
188
+ "content-encoding",
189
+ "transfer-encoding",
190
+ "trailer",
191
+ "expect",
192
+ )
193
+ )
194
+ ):
195
+ raise ProviderAccessError("authentication", "attestation_claims")
196
+ length = int(lengths[0]) if lengths else 0
197
+ if length > MAX_HTTP_BODY_BYTES:
198
+ raise ProviderAccessError("authentication", "attestation_claims")
199
+ body = self.provider._bounded(lambda: raw["input"].read(length), deadline)
200
+ if len(body) != length:
201
+ raise ProviderAccessError("authentication", "attestation_claims")
202
+ captured = HTTPRequestBytes(request.method, raw["target"], headers, body)
203
+ _http_request_fields(captured)
204
+ request._cached_data = body
205
+ request.environ["wsgi.input"] = io.BytesIO(body)
206
+ request.__dict__["stream"] = request.environ["wsgi.input"]
207
+ admission = self.provider.authorize(
208
+ facts,
209
+ operation_id,
210
+ scheme,
211
+ token,
212
+ request.method,
213
+ attestation=attestation,
214
+ deadline=deadline,
215
+ **({"http_request": captured} if captured is not None else {}),
216
+ )
217
+ g.external_admission = admission
218
+ result = handler(*args, **kwargs)
219
+ if isinstance(result, Response):
220
+ payload = result
221
+ else:
222
+ payload = jsonify(result)
223
+ # 有限读响应发出前复核同次依据;写操作由业务最后可拒绝点显式复核。
224
+ if operation.accept == "read":
225
+ admission.recheck()
226
+ return payload
227
+ except ProviderAccessError as error:
228
+ return _access_error_response(error)
229
+ except HTTPException as error:
230
+ return _error_response(
231
+ error.code or 503, "external_request_failed", _request_id()
232
+ )
233
+ except Exception:
234
+ # 不记录异常链或请求正文;诊断按 request_id 关联。
235
+ return _error_response(503, "external_dependency_unavailable", _request_id())
236
+
237
+ return wrapped
238
+
239
+ return decorate
240
+
241
+
242
+ def _status_exception_response(error: Any) -> Any:
243
+ status = error.code if isinstance(error, HTTPException) else 503
244
+ description = (
245
+ error.description
246
+ if isinstance(error, HTTPException) and type(error.description) is dict
247
+ else {}
248
+ )
249
+ code = description.get("code")
250
+ if code not in ("00010", "00011", "00021"):
251
+ code = "00011" if status in (401, 403) else "00021"
252
+ message = description.get("msg") if status in (401, 403) else None
253
+ if message not in ("授权超时", "授权无效", "权限不足"):
254
+ message = "授权无效" if status in (401, 403) else "诊断请求失败"
255
+ return jsonify({"code": code, "msg": message, "data": {}}), status
256
+
257
+
258
+ def _bounded_status(call: Any, deadline: Any) -> Any:
259
+ from gevent import Timeout
260
+
261
+ class _Expired(Exception):
262
+ pass
263
+
264
+ remaining = deadline - time.monotonic()
265
+ if remaining <= 0:
266
+ return jsonify({"code": "00021", "msg": "诊断请求失败", "data": {}}), 503
267
+ try:
268
+ with Timeout(remaining, _Expired()):
269
+ return call()
270
+ except _Expired:
271
+ return jsonify({"code": "00021", "msg": "诊断请求失败", "data": {}}), 503
272
+
273
+
274
+ def install_provider(
275
+ app: Any,
276
+ provider: Any,
277
+ service_display: Optional[str] = None,
278
+ storage_check: Optional[Callable[..., Any]] = None,
279
+ admin_guard: Optional[Callable[..., Any]] = None,
280
+ ) -> ExternalHTTP:
281
+ """唯一安装入口:注册生命周期、Status 路由与保护边界;import 时不启动。
282
+
283
+ provider 为可信制品构造的 Provider 或准备失败的 UnavailableProvider;后者
284
+ 仅保留诊断和拒绝接纳,不创建运行时。storage_check 为服务自有
285
+ 必要持久层只读检查(返回 STORAGE_NOT_APPLICABLE 表示真实无持久层),
286
+ 不从 ZIP/请求新增探测目标。admin_guard 为既有管理员 Bearer guard 装饰器
287
+ (必须由宿主显式传入;缺失拒绝安装)。安装幂等;构造失败由创建方清理。
288
+ """
289
+ if not callable(admin_guard):
290
+ raise ValueError("admin_guard must be an explicit administrator decorator")
291
+ if not isinstance(provider, (Provider, UnavailableProvider)):
292
+ raise ValueError("provider must be Provider or UnavailableProvider")
293
+ if service_display is not None and (
294
+ type(service_display) is not str
295
+ or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,119}", service_display) is None
296
+ ):
297
+ raise ValueError("invalid external service display")
298
+ http = app.extensions.get("commonlib.external_http")
299
+ if http is not None:
300
+ if http.provider is not provider:
301
+ raise ValueError("external provider already installed")
302
+ return http
303
+ http = ExternalHTTP(app, provider)
304
+ app.extensions["commonlib.external_http"] = http
305
+ # serv_process 在监听前 start、退出 finally stop;此处不重复注册 atexit。
306
+ app.extensions["commonlib.external"] = provider
307
+
308
+ status_api = Blueprint("commonlib_external_status", __name__)
309
+
310
+ @status_api.route("/external/status", methods=["GET"], provide_automatic_options=False)
311
+ def external_status() -> Any:
312
+ deadline = time.monotonic() + STATUS_TOTAL_TIMEOUT
313
+
314
+ def invoke() -> Any:
315
+ if (
316
+ request.args
317
+ or request.content_length not in (None, 0)
318
+ or request.environ.get("HTTP_TRANSFER_ENCODING")
319
+ ):
320
+ from flask import abort
321
+
322
+ abort(400, {"code": "00021"})
323
+ data = provider.collect_status(
324
+ request.environ.get("commonlib.socket_facts"),
325
+ storage_check,
326
+ deadline=deadline,
327
+ service_display=service_display,
328
+ )
329
+ response = jsonify({"code": "00000", "msg": "成功", "data": data})
330
+ response.headers["Cache-Control"] = "no-store"
331
+ return response
332
+
333
+ guard = admin_guard
334
+ try:
335
+ return _bounded_status(guard(invoke), deadline)
336
+ except HTTPException as error:
337
+ return _status_exception_response(error)
338
+ except Exception:
339
+ return _status_exception_response(Exception())
340
+
341
+ app.register_blueprint(status_api)
342
+
343
+ # 路由匹配失败、未加保护的 External 路由也不进入会回显正文的历史处理器。
344
+ handle_http_exception = app.handle_http_exception
345
+
346
+ def external_http_exception(error: Any) -> Any:
347
+ if request.path == "/external" or request.path.startswith("/external/"):
348
+ return _error_response(error.code or 503, "external_request_failed", _request_id())
349
+ return handle_http_exception(error)
350
+
351
+ app.handle_http_exception = external_http_exception
352
+
353
+ @app.after_request
354
+ def _commonlib_external_boundary(response: Any) -> Any:
355
+ return _strip_external_cors(response)
356
+
357
+ return http