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.
- {storepy-3.2.0 → storepy-3.3.0}/PKG-INFO +1 -1
- {storepy-3.2.0 → storepy-3.3.0}/pyproject.toml +1 -1
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/workflow.py +62 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/PKG-INFO +1 -1
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_workflow.py +37 -0
- {storepy-3.2.0 → storepy-3.3.0}/LICENSE +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/README.md +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/setup.cfg +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/__init__.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ask.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ask_knowledge.md +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/core.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/__init__.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/exec.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/id.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/mutation.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/query.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/crud/write.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/datasource.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/ddl.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/__init__.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/_values.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/mongo.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/mysql.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/postgres.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/executors/sqlite.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/feedback.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/__init__.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/mysql.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/postgres.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/introspect/sqlite.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/llm.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/metadef.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/permission.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/schema.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/py_store/sync.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/SOURCES.txt +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/dependency_links.txt +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/requires.txt +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/src/storepy.egg-info/top_level.txt +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_ask.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_coverage_margin.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_federation_e2e.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_host_contract.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_host_paths.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_metadef.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_migration.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_mongo_executor.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_mongo_session.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_multi_datasource.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_profile.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_py_store.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_raw_sql_and_ddl.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_rbac_host.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_real_backends_e2e.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_require_context.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_scenario_course_platform.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_scenario_manager_transaction.py +0 -0
- {storepy-3.2.0 → storepy-3.3.0}/tests/test_session.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
File without changes
|