storepy 3.3.0__tar.gz → 3.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.
Files changed (64) hide show
  1. {storepy-3.3.0 → storepy-3.4.0}/PKG-INFO +1 -1
  2. {storepy-3.3.0 → storepy-3.4.0}/pyproject.toml +1 -1
  3. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/__init__.py +32 -0
  4. storepy-3.4.0/src/py_store/bootstrap.py +19 -0
  5. storepy-3.4.0/src/py_store/cache.py +36 -0
  6. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/feedback.py +15 -1
  7. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/metadef.py +23 -0
  8. {storepy-3.3.0 → storepy-3.4.0}/src/storepy.egg-info/PKG-INFO +1 -1
  9. {storepy-3.3.0 → storepy-3.4.0}/src/storepy.egg-info/SOURCES.txt +4 -0
  10. storepy-3.4.0/tests/test_cache.py +64 -0
  11. storepy-3.4.0/tests/test_feedback_flush.py +40 -0
  12. {storepy-3.3.0 → storepy-3.4.0}/tests/test_metadef.py +31 -0
  13. {storepy-3.3.0 → storepy-3.4.0}/LICENSE +0 -0
  14. {storepy-3.3.0 → storepy-3.4.0}/README.md +0 -0
  15. {storepy-3.3.0 → storepy-3.4.0}/setup.cfg +0 -0
  16. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/ask.py +0 -0
  17. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/ask_knowledge.md +0 -0
  18. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/core.py +0 -0
  19. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/__init__.py +0 -0
  20. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/exec.py +0 -0
  21. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/id.py +0 -0
  22. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/mutation.py +0 -0
  23. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/query.py +0 -0
  24. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/crud/write.py +0 -0
  25. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/datasource.py +0 -0
  26. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/ddl.py +0 -0
  27. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/__init__.py +0 -0
  28. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/_values.py +0 -0
  29. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/mongo.py +0 -0
  30. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/mysql.py +0 -0
  31. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/postgres.py +0 -0
  32. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/executors/sqlite.py +0 -0
  33. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/introspect/__init__.py +0 -0
  34. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/introspect/mysql.py +0 -0
  35. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/introspect/postgres.py +0 -0
  36. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/introspect/sqlite.py +0 -0
  37. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/llm.py +0 -0
  38. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/permission.py +0 -0
  39. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/schema.py +0 -0
  40. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/sync.py +0 -0
  41. {storepy-3.3.0 → storepy-3.4.0}/src/py_store/workflow.py +0 -0
  42. {storepy-3.3.0 → storepy-3.4.0}/src/storepy.egg-info/dependency_links.txt +0 -0
  43. {storepy-3.3.0 → storepy-3.4.0}/src/storepy.egg-info/requires.txt +0 -0
  44. {storepy-3.3.0 → storepy-3.4.0}/src/storepy.egg-info/top_level.txt +0 -0
  45. {storepy-3.3.0 → storepy-3.4.0}/tests/test_ask.py +0 -0
  46. {storepy-3.3.0 → storepy-3.4.0}/tests/test_coverage_margin.py +0 -0
  47. {storepy-3.3.0 → storepy-3.4.0}/tests/test_federation_e2e.py +0 -0
  48. {storepy-3.3.0 → storepy-3.4.0}/tests/test_host_contract.py +0 -0
  49. {storepy-3.3.0 → storepy-3.4.0}/tests/test_host_paths.py +0 -0
  50. {storepy-3.3.0 → storepy-3.4.0}/tests/test_migration.py +0 -0
  51. {storepy-3.3.0 → storepy-3.4.0}/tests/test_mongo_executor.py +0 -0
  52. {storepy-3.3.0 → storepy-3.4.0}/tests/test_mongo_session.py +0 -0
  53. {storepy-3.3.0 → storepy-3.4.0}/tests/test_multi_datasource.py +0 -0
  54. {storepy-3.3.0 → storepy-3.4.0}/tests/test_profile.py +0 -0
  55. {storepy-3.3.0 → storepy-3.4.0}/tests/test_py_store.py +0 -0
  56. {storepy-3.3.0 → storepy-3.4.0}/tests/test_raw_sql_and_ddl.py +0 -0
  57. {storepy-3.3.0 → storepy-3.4.0}/tests/test_rbac_host.py +0 -0
  58. {storepy-3.3.0 → storepy-3.4.0}/tests/test_real_backends_e2e.py +0 -0
  59. {storepy-3.3.0 → storepy-3.4.0}/tests/test_require_context.py +0 -0
  60. {storepy-3.3.0 → storepy-3.4.0}/tests/test_scenario_course_platform.py +0 -0
  61. {storepy-3.3.0 → storepy-3.4.0}/tests/test_scenario_manager_transaction.py +0 -0
  62. {storepy-3.3.0 → storepy-3.4.0}/tests/test_session.py +0 -0
  63. {storepy-3.3.0 → storepy-3.4.0}/tests/test_sql_commit_and_registry.py +0 -0
  64. {storepy-3.3.0 → storepy-3.4.0}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: storepy
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control
5
5
  Author-email: leo <coen_ddt@qq.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "storepy"
7
- version = "3.3.0"
7
+ version = "3.4.0"
8
8
  description = "Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -26,6 +26,7 @@ from pymongo.errors import PyMongoError
26
26
 
27
27
  from . import (
28
28
  ask,
29
+ cache,
29
30
  crud,
30
31
  datasource,
31
32
  ddl,
@@ -250,6 +251,19 @@ class Store:
250
251
  """注入进程级 ns 标签(tenant/env),供落库事件附加(进程级隔离下天然单 ns)"""
251
252
  feedback.set_meta({'tenant': tenant, 'env': env})
252
253
 
254
+ async def flushFeedback(self):
255
+ """等待全部在途 __feedback 落库完成(graceful shutdown 前调用;对齐 node store.flushFeedback)"""
256
+ await feedback.flush()
257
+
258
+ # ── 缓存状态注记(B6;见 cache.py)──
259
+ def setCacheStatus(self, fn):
260
+ """注册缓存状态 provider:`x-cache` 注记位唯一取值来源;未注册恒 BYPASS"""
261
+ return cache.set_cache_status(fn)
262
+
263
+ def cacheStatus(self, ctx: dict | None = None) -> str:
264
+ """当前响应的缓存状态注记(恒为 HIT|MISS|BYPASS 之一)"""
265
+ return cache.cache_status(permission.get_context() if ctx is None else ctx)
266
+
253
267
  # ── 工作流编排(首批:线性 + when 守卫 + fail-fast;见 workflow.py 与设计文档)──
254
268
  def registerWorkflow(self, defn: dict, ctx: dict | None = None) -> dict:
255
269
  """注册工作流定义(可选 ``ctx`` 过定义层门禁;默认 Open。白名单外显式 Err 含 WORKFLOW_UNSUPPORTED)"""
@@ -307,6 +321,10 @@ class Store:
307
321
  ensure_builtins = ensureBuiltins
308
322
  enable_feedback_table = enableFeedbackTable
309
323
  set_feedback_meta = setFeedbackMeta
324
+ flush_feedback = flushFeedback
325
+ # 缓存状态注记(蛇形别名与上方驼峰同实现)
326
+ set_cache_status = setCacheStatus
327
+ cache_status = cacheStatus
310
328
 
311
329
  # ── 其余 API 显式绑定(staticmethod:避免实例化后 self 注入)──
312
330
  # Schema 管理
@@ -450,3 +468,17 @@ async def init(connections):
450
468
  await _create_indexes_if_needed()
451
469
 
452
470
  return store
471
+
472
+
473
+ def __getattr__(name: str):
474
+ """惰性暴露框架统一入口 ``create_app``。
475
+
476
+ 放在模块级 ``__getattr__``(PEP 562)而非顶部 ``from .bootstrap import create_app``,
477
+ 以规避包内循环导入;因此无模块级导入,兼容 ruff E402 / I001。
478
+ """
479
+ if name == "create_app":
480
+ from .bootstrap import create_app
481
+
482
+ return create_app
483
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
484
+
@@ -0,0 +1,19 @@
1
+ """框架统一入口(Python):init → 逐条 register;协议面由接入方另行挂载。
2
+
3
+ 注意:``init`` / ``store`` 用函数内惰性导入——``__init__.py`` 末尾导入本模块,
4
+ 若此处顶层 ``from . import init, store`` 会在包尚未完成初始化时取到未定义名,
5
+ 故延迟到调用期解析。
6
+ """
7
+
8
+ from typing import Any, Iterable
9
+
10
+
11
+ async def create_app(*, datasource: Any, schemas: Iterable[dict] = (), ctx: Any = None):
12
+ from . import init, store # 延迟导入,规避包内循环导入
13
+
14
+ if datasource is None:
15
+ raise ValueError("ERR_BOOTSTRAP:缺 datasource")
16
+ await init(datasource)
17
+ for defn in schemas:
18
+ store.register(defn, ctx)
19
+ return {"store": store}
@@ -0,0 +1,36 @@
1
+ """缓存状态注记(B6)—— 协议层 `x-cache` 响应头的唯一取值来源。
2
+
3
+ 本轮只落**注记位**:不实现任何实际缓存。无 provider(默认)恒返回 'BYPASS'
4
+ (响应未经过缓存)——注记必须为真,禁把无缓存谎报为 HIT/MISS。
5
+ provider 由接入方在实现缓存时经 set_cache_status 注入,签名 (ctx) -> 'HIT'|'MISS'|'BYPASS'。
6
+ """
7
+
8
+ from .feedback import emit
9
+
10
+ _VALUES = ('HIT', 'MISS', 'BYPASS')
11
+ _provider = None
12
+
13
+
14
+ def set_cache_status(fn):
15
+ """注册缓存状态 provider;传 None / 非可调用恢复默认(恒 BYPASS)"""
16
+ global _provider
17
+ _provider = fn if callable(fn) else None
18
+
19
+
20
+ def cache_status(ctx=None):
21
+ """取当前响应的缓存状态注记(恒为 HIT|MISS|BYPASS 之一;非法/异常 → BYPASS + 反馈留痕)"""
22
+ if _provider is None:
23
+ return 'BYPASS'
24
+ try:
25
+ v = _provider(ctx)
26
+ except Exception as e: # 异常回落 BYPASS 且必须留痕(禁静默)
27
+ emit({'type': 'cache_status_failed', 'code': 'cacheStatusFailed', 'layer': 'host',
28
+ 'message': f'缓存状态 provider 抛错: {e}',
29
+ 'hint': '检查 set_cache_status 注入的 provider;本轮注记位不实现缓存,异常即回落 BYPASS'})
30
+ return 'BYPASS'
31
+ if v not in _VALUES:
32
+ emit({'type': 'cache_status_invalid', 'code': 'cacheStatusInvalid', 'layer': 'host',
33
+ 'message': f'缓存状态取值非法: {v!r}(值域 HIT|MISS|BYPASS)',
34
+ 'hint': 'provider 返回值必须为 HIT/MISS/BYPASS 之一;非法值回落 BYPASS'})
35
+ return 'BYPASS'
36
+ return v
@@ -25,6 +25,9 @@ _meta = {'tenant': '', 'env': ''}
25
25
  # 落库失败累计计数(进程级;>0 表示有事件未入表——可观测,不静默)
26
26
  _fail_count = 0
27
27
 
28
+ # 在途落库任务(进程级;graceful shutdown 前经 flush() 收口,消除 fire-and-forget 丢事件窗口 D8)
29
+ _pending: set = set()
30
+
28
31
 
29
32
  def get_sink():
30
33
  """当前 sink(None = 默认 stderr 行为);供接管方保存原值、退出时恢复(token-set/reset 同构)"""
@@ -82,7 +85,9 @@ def enable_feedback_table(store):
82
85
  except RuntimeError:
83
86
  loop = None
84
87
  if loop is not None:
85
- loop.create_task(_write_feedback(store, row))
88
+ task = loop.create_task(_write_feedback(store, row))
89
+ _pending.add(task)
90
+ task.add_done_callback(_pending.discard) # 完成即出集合
86
91
  else:
87
92
  _fail('无运行中事件循环,__feedback 事件未落库')
88
93
 
@@ -90,6 +95,15 @@ def enable_feedback_table(store):
90
95
  return lambda: set_sink(prev)
91
96
 
92
97
 
98
+ async def flush():
99
+ """等待全部在途 `__feedback` 落库任务完成(graceful shutdown 前调用)。
100
+
101
+ 落库失败已由 ``_write_feedback`` 计为 ``fail_count``(不抛),故此处 ``gather`` 不冒泡。
102
+ """
103
+ while _pending:
104
+ await asyncio.gather(*list(_pending), return_exceptions=True)
105
+
106
+
93
107
  def emit(event):
94
108
  """产出一条反馈事件:有 sink 回调之;否则打印 stderr(允许拦截,禁止静默)"""
95
109
  if _sink is not None:
@@ -110,6 +110,28 @@ class MetaDefError(ValueError):
110
110
 
111
111
  # ─── 纯逻辑(双端逐字节等价;parity 锚) ───────────────────────
112
112
 
113
+ def _assert_serializable(value, path=''):
114
+ """深度检查 defn 是否含函数值 —— 持久化定义 = 纯 JSON 契约(N3)。
115
+
116
+ 必须先于 ``_to_core_defn`` 调用:后者会把 ``fn/asyncFn`` 归一为 ``True`` 占位、把其余函数剔除,
117
+ 使「含回调定义」静默降级入库,回滚/restore 即丢失回调(缺陷台账 D17)。
118
+ 本函数在归一化**之前**显式拒绝(fail-fast),文案对齐 node ``_assertSerializable``(逐字节一致)。
119
+ """
120
+ if callable(value):
121
+ raise MetaDefError(
122
+ f'metadef: 定义含函数回调({path}),不可持久化;'
123
+ '回调类定义禁止经控制面发布(定义内改用 fnRef 字符串,实现由宿主 register 时注入)'
124
+ )
125
+ if value is None or not isinstance(value, (dict, list, tuple)):
126
+ return
127
+ if isinstance(value, dict):
128
+ for k, v in value.items():
129
+ _assert_serializable(v, f'{path}.{k}' if path else str(k))
130
+ else:
131
+ for i, v in enumerate(value):
132
+ _assert_serializable(v, f'{path}.{i}' if path else str(i))
133
+
134
+
113
135
  def stable_stringify(value):
114
136
  """稳定序列化(键序无关):与 node ``_stableStringify`` 逐字节等价"""
115
137
  return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
@@ -204,6 +226,7 @@ async def persist_def(store, defn, opts):
204
226
  raise MetaDefError('metadef: defn.name 必填')
205
227
  opts = opts or {}
206
228
  _, table = _kind_of(opts)
229
+ _assert_serializable(defn, '') # ← 新增:先于 IO 与归一化
207
230
  name = defn['name']
208
231
  rows = await list_defs(store, {'tenant': opts.get('tenant'), 'env': opts.get('env'),
209
232
  'name': name, 'kind': opts.get('kind')})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: storepy
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control
5
5
  Author-email: leo <coen_ddt@qq.com>
6
6
  License-Expression: MIT
@@ -4,6 +4,8 @@ pyproject.toml
4
4
  src/py_store/__init__.py
5
5
  src/py_store/ask.py
6
6
  src/py_store/ask_knowledge.md
7
+ src/py_store/bootstrap.py
8
+ src/py_store/cache.py
7
9
  src/py_store/core.py
8
10
  src/py_store/datasource.py
9
11
  src/py_store/ddl.py
@@ -36,8 +38,10 @@ src/storepy.egg-info/dependency_links.txt
36
38
  src/storepy.egg-info/requires.txt
37
39
  src/storepy.egg-info/top_level.txt
38
40
  tests/test_ask.py
41
+ tests/test_cache.py
39
42
  tests/test_coverage_margin.py
40
43
  tests/test_federation_e2e.py
44
+ tests/test_feedback_flush.py
41
45
  tests/test_host_contract.py
42
46
  tests/test_host_paths.py
43
47
  tests/test_metadef.py
@@ -0,0 +1,64 @@
1
+ """缓存状态注记(B6)单测:注记必须为真——无 provider 恒 BYPASS;
2
+ 非法值/抛错回落 BYPASS 且走反馈通道留痕(不打断响应,禁静默)。
3
+
4
+ 运行:$env:LOCAL_CORE='1'; $env:PYTHONPATH='py-store/src'; python -m pytest py-store/tests/test_cache.py -q
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'src'))
11
+
12
+ from py_store import cache, feedback
13
+
14
+
15
+ def _collect(body):
16
+ """收集期间 emit 的事件(用后恢复原 sink,并复位 provider 防跨用例污染)"""
17
+ prev = feedback.get_sink()
18
+ events = []
19
+ feedback.set_sink(events.append)
20
+ try:
21
+ body()
22
+ finally:
23
+ feedback.set_sink(prev)
24
+ cache.set_cache_status(None)
25
+ return events
26
+
27
+
28
+ def test_no_provider_bypass():
29
+ cache.set_cache_status(None)
30
+ assert cache.cache_status() == 'BYPASS'
31
+
32
+
33
+ def test_invalid_value_bypass_with_feedback():
34
+ def _body():
35
+ cache.set_cache_status(lambda ctx: 'NOPE')
36
+ assert cache.cache_status() == 'BYPASS'
37
+
38
+ events = _collect(_body)
39
+ assert len(events) == 1
40
+ assert events[0]['code'] == 'cacheStatusInvalid'
41
+ assert events[0]['layer'] == 'host'
42
+
43
+
44
+ def test_provider_raises_bypass_with_feedback():
45
+ def _body():
46
+ def _boom(ctx):
47
+ raise RuntimeError('boom')
48
+
49
+ cache.set_cache_status(_boom)
50
+ assert cache.cache_status() == 'BYPASS'
51
+
52
+ events = _collect(_body)
53
+ assert len(events) == 1
54
+ assert events[0]['code'] == 'cacheStatusFailed'
55
+ assert 'boom' in events[0]['message']
56
+
57
+
58
+ def test_valid_value_passthrough():
59
+ def _body():
60
+ cache.set_cache_status(lambda ctx: 'HIT')
61
+ assert cache.cache_status() == 'HIT'
62
+
63
+ events = _collect(_body)
64
+ assert events == []
@@ -0,0 +1,40 @@
1
+ """A3 残留(D8):__feedback 落库 flush 与失败不静默 —— mock store,不连库。"""
2
+ import asyncio
3
+
4
+ from py_store import feedback
5
+
6
+
7
+ class _MockStore:
8
+ def __init__(self):
9
+ self.rows = []
10
+
11
+ async def insert(self, name, row):
12
+ self.rows.append((name, row))
13
+ return row
14
+
15
+
16
+ class _BoomStore:
17
+ async def insert(self, name, row):
18
+ raise RuntimeError('boom')
19
+
20
+
21
+ def test_flush_persists_inflight():
22
+ async def run():
23
+ s = _MockStore()
24
+ dispose = feedback.enable_feedback_table(s)
25
+ feedback.emit({'type': 't', 'code': 'c', 'layer': 'host', 'message': 'm', 'hint': 'h'})
26
+ await feedback.flush()
27
+ assert len(s.rows) == 1 and s.rows[0][0] == '__feedback' and s.rows[0][1]['code'] == 'c'
28
+ dispose()
29
+ asyncio.run(run())
30
+
31
+
32
+ def test_flush_failure_not_silent():
33
+ async def run():
34
+ before = feedback.fail_count()
35
+ dispose = feedback.enable_feedback_table(_BoomStore())
36
+ feedback.emit({'type': 't', 'code': 'c', 'layer': 'host'})
37
+ await feedback.flush()
38
+ assert feedback.fail_count() == before + 1
39
+ dispose()
40
+ asyncio.run(run())
@@ -232,6 +232,37 @@ def test_d21_rollback_visible_via_restore_defs():
232
232
  _run_in_sqlite(_run)
233
233
 
234
234
 
235
+ # ─── N3:回调类定义不可持久化(持久化定义 = 纯 JSON)─────────────
236
+
237
+ def test_n3_callback_defn_rejected_no_row():
238
+ """N3:含函数回调的 defn 经 persist_def 显式拒绝,库内零新增行"""
239
+
240
+ async def _run(store):
241
+ o = {'tenant': 't12', 'env': 'dev'}
242
+ defn = {'name': 'CbItem', 'fields': {'_id': {'type': 'string'}},
243
+ 'computes': {'total': {'fn': lambda item: 1}}}
244
+ with pytest.raises(MetaDefError, match='不可持久化'):
245
+ await md.persist_def(store, defn, o)
246
+ rows = await md.list_defs(store, {**o, 'name': 'CbItem'})
247
+ assert len(rows) == 0 # 拒绝先于 IO:库内零新增行
248
+
249
+ _run_in_sqlite(_run)
250
+
251
+
252
+ def test_n3_pure_json_defn_persists_same_shape():
253
+ """N3:纯 JSON defn(fnRef 字符串)正常落库且与入参同形"""
254
+
255
+ async def _run(store):
256
+ o = {'tenant': 't12', 'env': 'dev'}
257
+ defn = {'name': 'CbPure', 'fields': {'_id': {'type': 'string'}},
258
+ 'computes': {'total': {'fnRef': 'sum', 'type': 'int'}}}
259
+ row = await md.persist_def(store, defn, o)
260
+ assert row['version'] == 1
261
+ assert md.same_defn(row['defn'], defn)
262
+
263
+ _run_in_sqlite(_run)
264
+
265
+
235
266
  # ─── workflow 定义持久化(kind=workflow,落 __workflowDef;审计 §8 N1)───────
236
267
 
237
268
  def test_wf_persist_idempotent_versioning_and_list():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes