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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: store-api-py
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)
5
5
  License: MIT
6
6
  Keywords: py-store,storepy,fastapi,rest,crud,gql
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "store-api-py"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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) → PermissionError(403)
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 同语义)。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: store-api-py
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)
5
5
  License: MIT
6
6
  Keywords: py-store,storepy,fastapi,rest,crud,gql
@@ -9,4 +9,5 @@ src/store_api_py.egg-info/SOURCES.txt
9
9
  src/store_api_py.egg-info/dependency_links.txt
10
10
  src/store_api_py.egg-info/requires.txt
11
11
  src/store_api_py.egg-info/top_level.txt
12
+ tests/test_error_matrix.py
12
13
  tests/test_smoke.py
@@ -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