storepy 3.2.0__tar.gz → 3.3.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 (60) hide show
  1. {storepy-3.2.0 → storepy-3.3.0}/PKG-INFO +1 -1
  2. {storepy-3.2.0 → storepy-3.3.0}/pyproject.toml +1 -1
  3. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/workflow.py +62 -0
  4. {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/PKG-INFO +1 -1
  5. {storepy-3.2.0 → storepy-3.3.0}/tests/test_workflow.py +37 -0
  6. {storepy-3.2.0 → storepy-3.3.0}/LICENSE +0 -0
  7. {storepy-3.2.0 → storepy-3.3.0}/README.md +0 -0
  8. {storepy-3.2.0 → storepy-3.3.0}/setup.cfg +0 -0
  9. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/__init__.py +0 -0
  10. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ask.py +0 -0
  11. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ask_knowledge.md +0 -0
  12. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/core.py +0 -0
  13. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/__init__.py +0 -0
  14. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/exec.py +0 -0
  15. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/id.py +0 -0
  16. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/mutation.py +0 -0
  17. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/query.py +0 -0
  18. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/write.py +0 -0
  19. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/datasource.py +0 -0
  20. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ddl.py +0 -0
  21. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/__init__.py +0 -0
  22. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/_values.py +0 -0
  23. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/mongo.py +0 -0
  24. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/mysql.py +0 -0
  25. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/postgres.py +0 -0
  26. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/sqlite.py +0 -0
  27. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/feedback.py +0 -0
  28. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/__init__.py +0 -0
  29. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/mysql.py +0 -0
  30. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/postgres.py +0 -0
  31. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/sqlite.py +0 -0
  32. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/llm.py +0 -0
  33. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/metadef.py +0 -0
  34. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/permission.py +0 -0
  35. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/schema.py +0 -0
  36. {storepy-3.2.0 → storepy-3.3.0}/src/py_store/sync.py +0 -0
  37. {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/SOURCES.txt +0 -0
  38. {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/dependency_links.txt +0 -0
  39. {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/requires.txt +0 -0
  40. {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/top_level.txt +0 -0
  41. {storepy-3.2.0 → storepy-3.3.0}/tests/test_ask.py +0 -0
  42. {storepy-3.2.0 → storepy-3.3.0}/tests/test_coverage_margin.py +0 -0
  43. {storepy-3.2.0 → storepy-3.3.0}/tests/test_federation_e2e.py +0 -0
  44. {storepy-3.2.0 → storepy-3.3.0}/tests/test_host_contract.py +0 -0
  45. {storepy-3.2.0 → storepy-3.3.0}/tests/test_host_paths.py +0 -0
  46. {storepy-3.2.0 → storepy-3.3.0}/tests/test_metadef.py +0 -0
  47. {storepy-3.2.0 → storepy-3.3.0}/tests/test_migration.py +0 -0
  48. {storepy-3.2.0 → storepy-3.3.0}/tests/test_mongo_executor.py +0 -0
  49. {storepy-3.2.0 → storepy-3.3.0}/tests/test_mongo_session.py +0 -0
  50. {storepy-3.2.0 → storepy-3.3.0}/tests/test_multi_datasource.py +0 -0
  51. {storepy-3.2.0 → storepy-3.3.0}/tests/test_profile.py +0 -0
  52. {storepy-3.2.0 → storepy-3.3.0}/tests/test_py_store.py +0 -0
  53. {storepy-3.2.0 → storepy-3.3.0}/tests/test_raw_sql_and_ddl.py +0 -0
  54. {storepy-3.2.0 → storepy-3.3.0}/tests/test_rbac_host.py +0 -0
  55. {storepy-3.2.0 → storepy-3.3.0}/tests/test_real_backends_e2e.py +0 -0
  56. {storepy-3.2.0 → storepy-3.3.0}/tests/test_require_context.py +0 -0
  57. {storepy-3.2.0 → storepy-3.3.0}/tests/test_scenario_course_platform.py +0 -0
  58. {storepy-3.2.0 → storepy-3.3.0}/tests/test_scenario_manager_transaction.py +0 -0
  59. {storepy-3.2.0 → storepy-3.3.0}/tests/test_session.py +0 -0
  60. {storepy-3.2.0 → storepy-3.3.0}/tests/test_sql_commit_and_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: storepy
3
- Version: 3.2.0
3
+ Version: 3.3.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.2.0"
7
+ version = "3.3.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"
@@ -16,6 +16,7 @@ import json
16
16
  import re
17
17
  import time
18
18
  from contextlib import contextmanager
19
+ from typing import List
19
20
 
20
21
  from . import permission as _permission
21
22
  from .crud.exec import _call, _sources_of, run_atomic
@@ -268,6 +269,63 @@ def validate_defn(defn):
268
269
  return errors
269
270
 
270
271
 
272
+ def _collect_param_keys(ast_value, out=None):
273
+ """递归收集 core 返回 ast 中所有 ``params`` 对象的字符串值(@key 名)。"""
274
+ if out is None:
275
+ out = set()
276
+ if isinstance(ast_value, dict):
277
+ params = ast_value.get('params')
278
+ if isinstance(params, dict):
279
+ # core `ast.params` 的值为 `@key` 形态(含前导 @,core parse.rs::parse_params
280
+ # 保留 ref 原值);归一化去掉前导 @,使错误文案为 `@c0` 而非 `@@c0`。
281
+ for v in params.values():
282
+ if isinstance(v, str) and v:
283
+ out.add(v[1:] if v.startswith('@') else v)
284
+ for v in ast_value.values():
285
+ _collect_param_keys(v, out)
286
+ elif isinstance(ast_value, _LIST_TYPES):
287
+ for v in ast_value:
288
+ _collect_param_keys(v, out)
289
+ return out
290
+
291
+
292
+ def validate_planable(defn):
293
+ """注册期「只校验不绑参」可规划性校验 → 错误列表(空 = 通过)。纯内存、无 IO、无参数值绑定。
294
+
295
+ ① 结构性可规划:对每个 query 步骤的 gql 调 core 门面
296
+ ``_core.build_pipeline(gql, {}, None)``(params 传 {} = 不绑参;ctx 传 None);
297
+ 不可规划 → core 抛异常,此处汇聚为注册期错误。
298
+ ② 参数键完整:返回体 ``ast`` 暴露 ``params``(槽位→@key,含嵌套关系);
299
+ gql 引用的每个 ``@key`` 须在 ``step['params']`` 顶层存在。
300
+
301
+ 与 ``validate_defn``(纯函数)分职:本函数需要 core 注册表,故独立、不入 validate_defn。
302
+ """
303
+ errors: List[str] = []
304
+ if not isinstance(defn, dict):
305
+ return errors
306
+ steps = defn.get('steps')
307
+ if not isinstance(steps, _LIST_TYPES):
308
+ return errors
309
+ for i, step in enumerate(steps):
310
+ if not isinstance(step, dict) or step.get('op') != 'query':
311
+ continue
312
+ gql = step.get('gql')
313
+ if not isinstance(gql, str):
314
+ continue
315
+ where = f'steps[{i}]'
316
+ try:
317
+ built = _core.build_pipeline(gql, {}, None)
318
+ except Exception as e: # 透传 core 文案,与 node 逐字节对齐
319
+ errors.append(f'{where}: gql 不可规划: {e}')
320
+ continue
321
+ params = step.get('params')
322
+ provided = set(params) if isinstance(params, dict) else set()
323
+ for key in sorted(_collect_param_keys(built.get('ast') if isinstance(built, dict) else None)):
324
+ if key not in provided:
325
+ errors.append(f'{where}: gql 引用了未提供的参数 @{key}(params 须提供该键)')
326
+ return errors
327
+
328
+
271
329
  def _split_when(when):
272
330
  """when → (op, param_key);无歧义文法:含 exists 键即 op=exists(is 作参数键),
273
331
  其余算子的右值键统一 than(is 键已被 exists 参数占用;is/eq 算子同用 than)"""
@@ -317,6 +375,10 @@ def register(defn, ctx=None):
317
375
  errors = validate_defn(defn)
318
376
  if errors:
319
377
  raise WorkflowError('WORKFLOW_UNSUPPORTED: ' + ';'.join(errors))
378
+ # B1:注册期「只校验不绑参」可规划性(结构 + 参数键完整);独立于纯函数 validate_defn
379
+ plan_errors = validate_planable(defn)
380
+ if plan_errors:
381
+ raise WorkflowError('WORKFLOW_UNSUPPORTED: ' + ';'.join(plan_errors))
320
382
  name = defn['name']
321
383
  if name in _workflows:
322
384
  if _workflows[name] == defn:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: storepy
3
- Version: 3.2.0
3
+ Version: 3.3.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
@@ -12,9 +12,23 @@ import pytest
12
12
 
13
13
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'src'))
14
14
 
15
+ from py_store import schema as sc
15
16
  from py_store import workflow as wf
16
17
  from py_store.workflow import WorkflowError
17
18
 
19
+
20
+ # B1 前置:GOOD 的 gql/mutation 引用 Inventory,注册期可规划性校验要求其已注册。
21
+ # 本仓 conftest 的模块级夹具会在每模块清空注册表(clear_schemas),故按约定入口
22
+ # `_register_test_schemas` 提供(只定义不执行,由夹具在清场后重放)。
23
+ def _register_test_schemas():
24
+ sc.register({
25
+ 'name': 'Inventory', 'collection': 'inventory', 'idPrefix': 'inv',
26
+ 'fields': {
27
+ '_id': {'type': 'string'}, 'stock': {'type': 'int'},
28
+ 'productId': {'type': 'string'}, 'warehouse': {'type': 'string'},
29
+ },
30
+ })
31
+
18
32
  GOOD = {
19
33
  'name': 'placeOrder',
20
34
  'read': ['admin', 'ops'],
@@ -322,3 +336,26 @@ def test_parity_run_doc_shape():
322
336
  assert sorted(run_doc['steps'][1]) == ['as', 'op', 'state']
323
337
  finally:
324
338
  wf._workflows.pop('shapeWf', None)
339
+
340
+
341
+ # ─── B1:注册期 GQL 可规划性校验(结构 + 参数键完整) ─────────
342
+
343
+ def test_b1_planable_unplanned_rejected():
344
+ bad = {'name': 'noModelWf', 'steps': [
345
+ {'op': 'query', 'as': 'a', 'gql': 'NoSuchModel{_id}', 'params': {}}]}
346
+ assert len(wf.validate_planable(bad)) == 1
347
+ with pytest.raises(wf.WorkflowError, match=r'WORKFLOW_UNSUPPORTED: steps\[0\]: gql 不可规划'):
348
+ wf.register(bad)
349
+
350
+
351
+ def test_b1_planable_missing_param_key():
352
+ bad = {'name': 'missParamWf', 'steps': [
353
+ {'op': 'query', 'as': 'a', 'gql': 'Inventory($condition:@c0){_id}', 'params': {}}]}
354
+ assert wf.validate_planable(bad) == [
355
+ 'steps[0]: gql 引用了未提供的参数 @c0(params 须提供该键)']
356
+
357
+
358
+ def test_b1_planable_ok():
359
+ assert wf.validate_planable({'name': 'okWf', 'steps': [
360
+ {'op': 'query', 'as': 'a', 'gql': 'Inventory($condition:@c0){_id, stock}',
361
+ 'params': {'c0': {'stock': 1}}}]}) == []
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
File without changes