store-api-py 0.3.1__tar.gz → 0.4.0__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.
- {store_api_py-0.3.1 → store_api_py-0.4.0}/PKG-INFO +1 -1
- {store_api_py-0.3.1 → store_api_py-0.4.0}/pyproject.toml +1 -1
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py/app.py +11 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py/errors.py +6 -1
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py.egg-info/PKG-INFO +1 -1
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py.egg-info/SOURCES.txt +1 -0
- store_api_py-0.4.0/tests/test_error_matrix.py +52 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/tests/test_smoke.py +16 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/README.md +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/setup.cfg +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py/__init__.py +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py/params.py +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py.egg-info/dependency_links.txt +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py.egg-info/requires.txt +0 -0
- {store_api_py-0.3.1 → store_api_py-0.4.0}/src/store_api_py.egg-info/top_level.txt +0 -0
|
@@ -73,6 +73,17 @@ def create_app(
|
|
|
73
73
|
if permission_error is None:
|
|
74
74
|
permission_error = getattr(store, "PermissionError", None)
|
|
75
75
|
|
|
76
|
+
# fail-secure 装配守卫:宿主已开启上下文强制却未配 context_provider 时,装配期即
|
|
77
|
+
# 拒绝(fail-fast)。否则服务能启动、每个请求却在运行期以 ERR_NO_CONTEXT 失败——
|
|
78
|
+
# 配置错误被推迟成运行时事故(no-error-masking:不允许静默带错运行)。
|
|
79
|
+
require_context = getattr(store, "require_context", None)
|
|
80
|
+
if callable(require_context) and require_context() and context_provider is None:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"ERR_SECURE_CONFIG: 宿主已开启上下文强制(fail-secure)但未配置 context_provider;"
|
|
83
|
+
"请注入从请求解析身份的 context_provider(spec/04-context.md),"
|
|
84
|
+
"无需鉴权的内部服务请显式 store.set_require_context(False) 后再装配"
|
|
85
|
+
)
|
|
86
|
+
|
|
76
87
|
def _respond(mapped: StoreApiError) -> JSONResponse:
|
|
77
88
|
return JSONResponse(status_code=mapped.status_code, content=error_payload(mapped.code, mapped.message))
|
|
78
89
|
|
|
@@ -61,7 +61,7 @@ def context_error(message: str | None) -> StoreApiError:
|
|
|
61
61
|
|
|
62
62
|
def map_error(err: BaseException, permission_error: type[BaseException] | None) -> StoreApiError:
|
|
63
63
|
"""判定顺序(spec/03-errors.md):
|
|
64
|
-
未注入 resolver / 适配层守卫(400/413/501,由 StoreApiError 自带 status) →
|
|
64
|
+
未注入 resolver / 适配层守卫(400/413/501,由 StoreApiError 自带 status) → 权限类(403)
|
|
65
65
|
→ GQL 解析失败(400) → 其余 500 透传。
|
|
66
66
|
"""
|
|
67
67
|
if isinstance(err, StoreApiError):
|
|
@@ -71,6 +71,11 @@ def map_error(err: BaseException, permission_error: type[BaseException] | None)
|
|
|
71
71
|
if permission_error is not None and isinstance(err, permission_error):
|
|
72
72
|
# 按类型判定,禁按 message 匹配
|
|
73
73
|
return StoreApiError(403, store_code(err), str(err) or None)
|
|
74
|
+
if getattr(err, "code", None) == "no_context":
|
|
75
|
+
# spec/03 判定顺序第 3 层:权限类同档 —— py-store NoContextError(machine code
|
|
76
|
+
# `no_context`,requireContext 开启且 ctx 缺失);按 code 判定(宿主已剥前缀),
|
|
77
|
+
# 禁按 message 匹配 → 403。
|
|
78
|
+
return StoreApiError(403, store_code(err), str(err) or None)
|
|
74
79
|
# spec/03 判定顺序第 3 层:GQL 解析失败(core 稳定前缀 ERR_GQL_PARSE:,与
|
|
75
80
|
# ERR_PERM_PREFIX 同构的类型级契约——前缀判定非文案脆弱匹配)→ 400 GQL_PARSE,
|
|
76
81
|
# message 剥前缀取原文(与 go/rust/node 同语义)。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""映射矩阵:每档错误语义 → REST 状态码(A3「可程序化区分」验收)。
|
|
2
|
+
|
|
3
|
+
直接调纯函数 map_error(无需真实 store/库),用例与 node/test/error-matrix.test.js 同构。
|
|
4
|
+
规范依据:spec/03-errors.md 判定顺序 + core machine code(`no_context` / `permission_denied`)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
11
|
+
|
|
12
|
+
from store_api_py.errors import map_error # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PermissionError(Exception):
|
|
16
|
+
"""store 权限错误类(按类型判定,禁按 message 匹配)。"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _err(message: str, code: str) -> Exception:
|
|
20
|
+
e = Exception(message)
|
|
21
|
+
e.code = code
|
|
22
|
+
return e
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_matrix_permission_to_403():
|
|
26
|
+
err = PermissionError("无访问权限")
|
|
27
|
+
err.code = "permission_denied"
|
|
28
|
+
r = map_error(err, PermissionError)
|
|
29
|
+
assert r.status_code == 403
|
|
30
|
+
assert r.code == "permission_denied"
|
|
31
|
+
assert r.message == "无访问权限"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_matrix_no_context_to_403():
|
|
35
|
+
r = map_error(_err("上下文缺失", "no_context"), PermissionError)
|
|
36
|
+
assert r.status_code == 403
|
|
37
|
+
assert r.code == "no_context"
|
|
38
|
+
assert r.message == "上下文缺失"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_matrix_gql_parse_to_400():
|
|
42
|
+
r = map_error(Exception("ERR_GQL_PARSE:意外的 token"), PermissionError)
|
|
43
|
+
assert r.status_code == 400
|
|
44
|
+
assert r.code == "GQL_PARSE"
|
|
45
|
+
assert r.message == "意外的 token"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_matrix_other_to_500():
|
|
49
|
+
r = map_error(_err("连接超时", "CONN_TIMEOUT"), PermissionError)
|
|
50
|
+
assert r.status_code == 500
|
|
51
|
+
assert r.code == "CONN_TIMEOUT"
|
|
52
|
+
assert r.message == "连接超时"
|
|
@@ -86,6 +86,22 @@ def build_app(store, **extra):
|
|
|
86
86
|
return create_app(store, prefix="/api", **extra)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def test_secure_config_guard():
|
|
90
|
+
"""fail-secure 装配守卫:require_context=True 且无 context_provider ⇒ 装配期 ERR_SECURE_CONFIG"""
|
|
91
|
+
store = MockStore()
|
|
92
|
+
store.require_context = lambda: True
|
|
93
|
+
# 无 context_provider:装配期拒绝(fail-fast,错误不推迟到运行期)
|
|
94
|
+
with pytest.raises(RuntimeError, match="ERR_SECURE_CONFIG"):
|
|
95
|
+
build_app(store)
|
|
96
|
+
# 补配 context_provider:正常装配
|
|
97
|
+
app = build_app(store, context_provider=lambda request: {"uid": "u1"})
|
|
98
|
+
assert app is not None
|
|
99
|
+
# require_context=False(默认姿态)时无守卫,照常装配
|
|
100
|
+
opened = MockStore()
|
|
101
|
+
opened.require_context = lambda: False
|
|
102
|
+
assert build_app(opened) is not None
|
|
103
|
+
|
|
104
|
+
|
|
89
105
|
def test_filter_archived():
|
|
90
106
|
assert filter_archived(["user", "userDeleted", "post", "logDeleted"]) == ["user", "post", "logDeleted"]
|
|
91
107
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|