storepy 2.5.0__tar.gz → 2.7.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 (59) hide show
  1. {storepy-2.5.0 → storepy-2.7.0}/PKG-INFO +83 -1
  2. {storepy-2.5.0 → storepy-2.7.0}/README.md +82 -0
  3. {storepy-2.5.0 → storepy-2.7.0}/pyproject.toml +5 -1
  4. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/__init__.py +54 -16
  5. storepy-2.7.0/src/py_store/ask.py +322 -0
  6. storepy-2.7.0/src/py_store/ask_knowledge.md +250 -0
  7. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/write.py +62 -5
  8. storepy-2.7.0/src/py_store/ddl.py +390 -0
  9. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/__init__.py +13 -1
  10. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/mongo.py +14 -2
  11. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/mysql.py +6 -1
  12. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/postgres.py +5 -3
  13. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/sqlite.py +5 -1
  14. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/feedback.py +5 -0
  15. storepy-2.7.0/src/py_store/llm.py +141 -0
  16. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/permission.py +48 -0
  17. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/schema.py +36 -1
  18. storepy-2.7.0/src/py_store/workflow.py +690 -0
  19. {storepy-2.5.0 → storepy-2.7.0}/src/storepy.egg-info/PKG-INFO +83 -1
  20. {storepy-2.5.0 → storepy-2.7.0}/src/storepy.egg-info/SOURCES.txt +10 -1
  21. storepy-2.7.0/tests/test_ask.py +454 -0
  22. {storepy-2.5.0 → storepy-2.7.0}/tests/test_host_paths.py +48 -45
  23. storepy-2.7.0/tests/test_migration.py +130 -0
  24. {storepy-2.5.0 → storepy-2.7.0}/tests/test_profile.py +7 -4
  25. {storepy-2.5.0 → storepy-2.7.0}/tests/test_py_store.py +24 -24
  26. {storepy-2.5.0 → storepy-2.7.0}/tests/test_raw_sql_and_ddl.py +2 -1
  27. storepy-2.7.0/tests/test_rbac_host.py +105 -0
  28. {storepy-2.5.0 → storepy-2.7.0}/tests/test_require_context.py +7 -4
  29. {storepy-2.5.0 → storepy-2.7.0}/tests/test_scenario_course_platform.py +7 -6
  30. storepy-2.7.0/tests/test_scenario_manager_transaction.py +151 -0
  31. storepy-2.7.0/tests/test_workflow.py +299 -0
  32. storepy-2.5.0/src/py_store/ddl.py +0 -154
  33. {storepy-2.5.0 → storepy-2.7.0}/LICENSE +0 -0
  34. {storepy-2.5.0 → storepy-2.7.0}/setup.cfg +0 -0
  35. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/core.py +0 -0
  36. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/__init__.py +0 -0
  37. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/exec.py +0 -0
  38. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/id.py +0 -0
  39. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/mutation.py +0 -0
  40. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/crud/query.py +0 -0
  41. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/datasource.py +0 -0
  42. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/executors/_values.py +0 -0
  43. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/introspect/__init__.py +0 -0
  44. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/introspect/mysql.py +0 -0
  45. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/introspect/postgres.py +0 -0
  46. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/introspect/sqlite.py +0 -0
  47. {storepy-2.5.0 → storepy-2.7.0}/src/py_store/sync.py +0 -0
  48. {storepy-2.5.0 → storepy-2.7.0}/src/storepy.egg-info/dependency_links.txt +0 -0
  49. {storepy-2.5.0 → storepy-2.7.0}/src/storepy.egg-info/requires.txt +0 -0
  50. {storepy-2.5.0 → storepy-2.7.0}/src/storepy.egg-info/top_level.txt +0 -0
  51. {storepy-2.5.0 → storepy-2.7.0}/tests/test_coverage_margin.py +0 -0
  52. {storepy-2.5.0 → storepy-2.7.0}/tests/test_federation_e2e.py +0 -0
  53. {storepy-2.5.0 → storepy-2.7.0}/tests/test_host_contract.py +0 -0
  54. {storepy-2.5.0 → storepy-2.7.0}/tests/test_mongo_executor.py +0 -0
  55. {storepy-2.5.0 → storepy-2.7.0}/tests/test_mongo_session.py +0 -0
  56. {storepy-2.5.0 → storepy-2.7.0}/tests/test_multi_datasource.py +0 -0
  57. {storepy-2.5.0 → storepy-2.7.0}/tests/test_real_backends_e2e.py +0 -0
  58. {storepy-2.5.0 → storepy-2.7.0}/tests/test_session.py +0 -0
  59. {storepy-2.5.0 → storepy-2.7.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: 2.5.0
3
+ Version: 2.7.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
@@ -85,6 +85,7 @@ from py_store import init, store
85
85
  - [Feedback events](#feedback-events)
86
86
  - [Schema reference](#schema-reference)
87
87
  - [Transactions](#transaction-boundary)
88
+ - [Transactional capabilities](#transactional-capabilities)
88
89
  - [FAQ](#faq)
89
90
  - [Related projects](#related-projects)
90
91
 
@@ -265,6 +266,9 @@ GQL tree queries compile to a single native query per backend — never hand-wri
265
266
  - **Permission context** — `ContextVar`-based roles (`super_admin`/`admin`/`guest`/`creator`...), schema/field-level read/write whitelists, automatic owner-condition injection.
266
267
  - **Multi-datasource & multi-tenant** — locate a schema by `(source, namespace, collection)`; re-target per request with a route override.
267
268
  - **Async-first, Rust core** — built on PyMongo's `AsyncMongoClient` and a shared Rust core with SQL dialects.
269
+ - **Relation predicates in mutations** — filter `update` / `remove` by related-table fields, pushed down to all four backends (previously a silent no-op on MongoDB).
270
+ - **Autoincrement primary keys** — declare `_id` as `{"type": "int", "strategy": "autoincrement"}` for database-assigned integer IDs, with explicit errors where autoincrement is impossible.
271
+ - **Index DDL** — `schema.indexes` compiles to real `CREATE [UNIQUE] INDEX` statements (per backend, byte-identical); the generator still only emits text.
268
272
 
269
273
  ## GQL syntax
270
274
 
@@ -548,6 +552,84 @@ Boundary rules worth knowing up front (all **fail explicitly**, never silently d
548
552
  - Read consistency: only multiple reads inside an explicit session share one transaction connection; reads outside a session do not open an extra transaction.
549
553
  - **Cross-source writes (no session)**: a single write call touching ≥2 datasources **cannot be atomic**; it runs sequentially and emits one `non_atomic_write` feedback event (`code: nonAtomic`, with the source list) — degradation is allowed, silence is not. Converge writes onto a single source, or wrap them in `store.session()` (which fails closed on cross-source writes).
550
554
 
555
+ ## Transactional capabilities
556
+
557
+ Capabilities aimed at transactional workloads (orders, inventory — write contention plus
558
+ complex reads). Full details, semantics and the explicit-error list:
559
+ **[doc/transaction-capabilities.md](doc/transaction-capabilities.md)** ·
560
+ [中文](doc/transaction-capabilities.zh-CN.md).
561
+
562
+ - **Relation predicates in mutations** — `update_many('Inventory', {'product': {'category': 'meat'}}, {'$inc': {'stock': 10}})`: condition keys matching a declared relation become a semi/anti-join, normalized into a preCommand (aggregate fetching `_id`s) plus `_id $in`.
563
+ - **`$group by` one-relation paths** — `by: ['product.category']` compiles to `$lookup`+`$unwind` (Mongo) / `LEFT JOIN` (SQL); `many` paths fail explicitly (fan-out breaks count semantics).
564
+ - **Autoincrement PKs** — `_id: {'type': 'int', 'strategy': 'autoincrement'}`; PG/SQLite read back via `INSERT…RETURNING`, MySQL via last-insert-id; MongoDB and `insert_many` fail explicitly with `AUTOINCREMENT_NOT_SUPPORTED` (no silent ObjectId substitution).
565
+ - **Index DDL** — `schema.indexes` (MongoDB shape) → `CREATE [UNIQUE] INDEX idx_<table>_<cols>` in `ddl.generate`, byte-identical across MySQL/PostgreSQL/SQLite.
566
+ - **Declarative migration** — `ddl.diff_defs(old, new)` + `ddl.generate_migration(backend, old, new)`: whitelist-only (add table/column/index, type widening), per-dialect SQL, pure functions; destructive changes fail with `MIGRATION_UNSUPPORTED`.
567
+
568
+ ## Workflow orchestration (first batch)
569
+
570
+ Express "orchestration of multi-step data operations" as data: a workflow definition (defn) is pure
571
+ JSON isomorphic to a schema defn, and each run is persisted to the built-in schema `__workflowRun`
572
+ (queryable with plain GQL — zero new observability endpoints). Execution generalizes the existing
573
+ mutation step-sequence mechanism: linear steps + per-step `when` guards + fail-fast. The engine
574
+ lives in the host layer (`py_store/workflow.py`), core unchanged; aligned with
575
+ `nodejs-store/src/workflow.js` (byte-identical outputs guarded by parity anchor tests).
576
+
577
+ ```python
578
+ from py_store import workflow
579
+
580
+ workflow.register({
581
+ 'name': 'placeOrder',
582
+ 'run': ['admin', 'ops'], # three-tier whitelists read/write/run (run falls back to write)
583
+ 'steps': [
584
+ {'op': 'query', 'as': 'inv',
585
+ 'gql': 'Inventory($condition:@c0){_id, stock}',
586
+ 'params': {'c0': {'productId': '{{input.productId}}', 'warehouse': '{{input.warehouse}}'}}},
587
+ {'op': 'fail', 'when': {'exists': '{{inv._id}}', 'is': None}, 'message': '库存记录不存在'},
588
+ {'op': 'fail', 'when': {'lt': '{{inv.stock}}', 'than': '{{input.qty}}'}, 'message': '库存不足'},
589
+ {'op': 'mutation', 'model': 'Inventory',
590
+ 'data': {'_id': '{{inv._id}}', 'stock': '{{dec:{{inv.stock}},{{input.qty}}}}'}},
591
+ ],
592
+ })
593
+
594
+ run = await workflow.run('placeOrder', {'productId': 'p1', 'warehouse': 'w1', 'qty': 30})
595
+ # run['status'] ∈ succeeded | failed | rejected | drySucceeded | dryFailed
596
+ # Uniform contract: business failures never raise; the error lives in run['error']
597
+ # (set only on failure; always null on success — never `||`-masked downstream)
598
+ ```
599
+
600
+ - **Step whitelist** (three kinds; anything else fails registration with `WORKFLOW_UNSUPPORTED`):
601
+ `query` (result must be unique — >1 row is an explicit error), `mutation` (store.mutation /
602
+ upsert), `fail` (explicit business assertion). Optional `when` guards (exists / is / eq / ne /
603
+ lt / lte / gt / gte) record `skipped` explicitly — never silently skipped.
604
+ - **Placeholders**: `{{input.<path>}}`, `{{<as>.<path>}}` (forward references only),
605
+ `{{dec:<a>,<b>}}`; full-string replacement keeps the value type. No placeholders inside gql
606
+ (bind via params — injection safety); no array-index path segments.
607
+ - **Permissions**: three-tier role whitelists embedded in the defn (same RBAC semantics: admin /
608
+ super_admin bypass, guest denied, internal bypass); runs inherit the caller's Context and every
609
+ step goes through core permission checks — no superuser. `require_context(true)` rejects
610
+ context-less runs (fail-secure wins over dry-run); rejected runs are persisted for audit.
611
+ - **Atomicity**: a single-source run is atomic across steps (outer `run_atomic` wraps the whole
612
+ loop, inner mutations nest into it); multi-source / prescan-failed runs execute sequentially and
613
+ emit feedback events (`workflow_non_atomic` / `workflow_prescan_failed`) — never silent. The run
614
+ record (running → terminal) is committed outside the business transaction so failed runs stay
615
+ queryable after rollback.
616
+ - **dry-run**: `workflow.run(name, input, dry_run=True)` — query steps execute for real (read-only
617
+ safe); mutation / fail are recorded as `wouldRun` (`drySucceeded | dryFailed`).
618
+ - **Run persistence**: `__workflowRun` is bootstrapped on import (idempotent); SQL backends need a
619
+ one-time `ddl.generate(backend, ['__workflowRun'])` (Mongo creates the collection on first write).
620
+ Its `write` whitelist is explicitly empty (GQL tampering with run audit is rejected by R2).
621
+
622
+ ### Explicitly not in the first batch (detected → error; boundaries shipped with the same weight as features)
623
+
624
+ | Not supported | Why | Escape hatch |
625
+ |---|---|---|
626
+ | Loops / parallel / sub-workflows / human approval | DAG & wait semantics explode; linear + `when` covers the first batch | orchestrate in host code via the store API |
627
+ | Auto compensation (Saga) / auto retry | Inverse-operation burden; steps have no automatic idempotency | inspect run records and handle explicitly |
628
+ | Per-step host callbacks | Arbitrary code breaks whitelist governance | schema computes (read) / host code (write) |
629
+ | Workflow defn persistence / hot reload | Depends on schema versioning (next on the roadmap) | defn stays code-side JSON + register, like schemas today |
630
+ | Timers / event triggers | Scheduling is a resident-IO concern, orthogonal to pure orchestration | call `run` from the application layer |
631
+ | Placeholders inside gql / array-index paths | Injection surface / per-row iteration semantics | params binding / host-code orchestration |
632
+
551
633
  ## FAQ
552
634
 
553
635
  **How do I use one schema for both MongoDB and PostgreSQL in Python?**
@@ -45,6 +45,7 @@ from py_store import init, store
45
45
  - [Feedback events](#feedback-events)
46
46
  - [Schema reference](#schema-reference)
47
47
  - [Transactions](#transaction-boundary)
48
+ - [Transactional capabilities](#transactional-capabilities)
48
49
  - [FAQ](#faq)
49
50
  - [Related projects](#related-projects)
50
51
 
@@ -225,6 +226,9 @@ GQL tree queries compile to a single native query per backend — never hand-wri
225
226
  - **Permission context** — `ContextVar`-based roles (`super_admin`/`admin`/`guest`/`creator`...), schema/field-level read/write whitelists, automatic owner-condition injection.
226
227
  - **Multi-datasource & multi-tenant** — locate a schema by `(source, namespace, collection)`; re-target per request with a route override.
227
228
  - **Async-first, Rust core** — built on PyMongo's `AsyncMongoClient` and a shared Rust core with SQL dialects.
229
+ - **Relation predicates in mutations** — filter `update` / `remove` by related-table fields, pushed down to all four backends (previously a silent no-op on MongoDB).
230
+ - **Autoincrement primary keys** — declare `_id` as `{"type": "int", "strategy": "autoincrement"}` for database-assigned integer IDs, with explicit errors where autoincrement is impossible.
231
+ - **Index DDL** — `schema.indexes` compiles to real `CREATE [UNIQUE] INDEX` statements (per backend, byte-identical); the generator still only emits text.
228
232
 
229
233
  ## GQL syntax
230
234
 
@@ -508,6 +512,84 @@ Boundary rules worth knowing up front (all **fail explicitly**, never silently d
508
512
  - Read consistency: only multiple reads inside an explicit session share one transaction connection; reads outside a session do not open an extra transaction.
509
513
  - **Cross-source writes (no session)**: a single write call touching ≥2 datasources **cannot be atomic**; it runs sequentially and emits one `non_atomic_write` feedback event (`code: nonAtomic`, with the source list) — degradation is allowed, silence is not. Converge writes onto a single source, or wrap them in `store.session()` (which fails closed on cross-source writes).
510
514
 
515
+ ## Transactional capabilities
516
+
517
+ Capabilities aimed at transactional workloads (orders, inventory — write contention plus
518
+ complex reads). Full details, semantics and the explicit-error list:
519
+ **[doc/transaction-capabilities.md](doc/transaction-capabilities.md)** ·
520
+ [中文](doc/transaction-capabilities.zh-CN.md).
521
+
522
+ - **Relation predicates in mutations** — `update_many('Inventory', {'product': {'category': 'meat'}}, {'$inc': {'stock': 10}})`: condition keys matching a declared relation become a semi/anti-join, normalized into a preCommand (aggregate fetching `_id`s) plus `_id $in`.
523
+ - **`$group by` one-relation paths** — `by: ['product.category']` compiles to `$lookup`+`$unwind` (Mongo) / `LEFT JOIN` (SQL); `many` paths fail explicitly (fan-out breaks count semantics).
524
+ - **Autoincrement PKs** — `_id: {'type': 'int', 'strategy': 'autoincrement'}`; PG/SQLite read back via `INSERT…RETURNING`, MySQL via last-insert-id; MongoDB and `insert_many` fail explicitly with `AUTOINCREMENT_NOT_SUPPORTED` (no silent ObjectId substitution).
525
+ - **Index DDL** — `schema.indexes` (MongoDB shape) → `CREATE [UNIQUE] INDEX idx_<table>_<cols>` in `ddl.generate`, byte-identical across MySQL/PostgreSQL/SQLite.
526
+ - **Declarative migration** — `ddl.diff_defs(old, new)` + `ddl.generate_migration(backend, old, new)`: whitelist-only (add table/column/index, type widening), per-dialect SQL, pure functions; destructive changes fail with `MIGRATION_UNSUPPORTED`.
527
+
528
+ ## Workflow orchestration (first batch)
529
+
530
+ Express "orchestration of multi-step data operations" as data: a workflow definition (defn) is pure
531
+ JSON isomorphic to a schema defn, and each run is persisted to the built-in schema `__workflowRun`
532
+ (queryable with plain GQL — zero new observability endpoints). Execution generalizes the existing
533
+ mutation step-sequence mechanism: linear steps + per-step `when` guards + fail-fast. The engine
534
+ lives in the host layer (`py_store/workflow.py`), core unchanged; aligned with
535
+ `nodejs-store/src/workflow.js` (byte-identical outputs guarded by parity anchor tests).
536
+
537
+ ```python
538
+ from py_store import workflow
539
+
540
+ workflow.register({
541
+ 'name': 'placeOrder',
542
+ 'run': ['admin', 'ops'], # three-tier whitelists read/write/run (run falls back to write)
543
+ 'steps': [
544
+ {'op': 'query', 'as': 'inv',
545
+ 'gql': 'Inventory($condition:@c0){_id, stock}',
546
+ 'params': {'c0': {'productId': '{{input.productId}}', 'warehouse': '{{input.warehouse}}'}}},
547
+ {'op': 'fail', 'when': {'exists': '{{inv._id}}', 'is': None}, 'message': '库存记录不存在'},
548
+ {'op': 'fail', 'when': {'lt': '{{inv.stock}}', 'than': '{{input.qty}}'}, 'message': '库存不足'},
549
+ {'op': 'mutation', 'model': 'Inventory',
550
+ 'data': {'_id': '{{inv._id}}', 'stock': '{{dec:{{inv.stock}},{{input.qty}}}}'}},
551
+ ],
552
+ })
553
+
554
+ run = await workflow.run('placeOrder', {'productId': 'p1', 'warehouse': 'w1', 'qty': 30})
555
+ # run['status'] ∈ succeeded | failed | rejected | drySucceeded | dryFailed
556
+ # Uniform contract: business failures never raise; the error lives in run['error']
557
+ # (set only on failure; always null on success — never `||`-masked downstream)
558
+ ```
559
+
560
+ - **Step whitelist** (three kinds; anything else fails registration with `WORKFLOW_UNSUPPORTED`):
561
+ `query` (result must be unique — >1 row is an explicit error), `mutation` (store.mutation /
562
+ upsert), `fail` (explicit business assertion). Optional `when` guards (exists / is / eq / ne /
563
+ lt / lte / gt / gte) record `skipped` explicitly — never silently skipped.
564
+ - **Placeholders**: `{{input.<path>}}`, `{{<as>.<path>}}` (forward references only),
565
+ `{{dec:<a>,<b>}}`; full-string replacement keeps the value type. No placeholders inside gql
566
+ (bind via params — injection safety); no array-index path segments.
567
+ - **Permissions**: three-tier role whitelists embedded in the defn (same RBAC semantics: admin /
568
+ super_admin bypass, guest denied, internal bypass); runs inherit the caller's Context and every
569
+ step goes through core permission checks — no superuser. `require_context(true)` rejects
570
+ context-less runs (fail-secure wins over dry-run); rejected runs are persisted for audit.
571
+ - **Atomicity**: a single-source run is atomic across steps (outer `run_atomic` wraps the whole
572
+ loop, inner mutations nest into it); multi-source / prescan-failed runs execute sequentially and
573
+ emit feedback events (`workflow_non_atomic` / `workflow_prescan_failed`) — never silent. The run
574
+ record (running → terminal) is committed outside the business transaction so failed runs stay
575
+ queryable after rollback.
576
+ - **dry-run**: `workflow.run(name, input, dry_run=True)` — query steps execute for real (read-only
577
+ safe); mutation / fail are recorded as `wouldRun` (`drySucceeded | dryFailed`).
578
+ - **Run persistence**: `__workflowRun` is bootstrapped on import (idempotent); SQL backends need a
579
+ one-time `ddl.generate(backend, ['__workflowRun'])` (Mongo creates the collection on first write).
580
+ Its `write` whitelist is explicitly empty (GQL tampering with run audit is rejected by R2).
581
+
582
+ ### Explicitly not in the first batch (detected → error; boundaries shipped with the same weight as features)
583
+
584
+ | Not supported | Why | Escape hatch |
585
+ |---|---|---|
586
+ | Loops / parallel / sub-workflows / human approval | DAG & wait semantics explode; linear + `when` covers the first batch | orchestrate in host code via the store API |
587
+ | Auto compensation (Saga) / auto retry | Inverse-operation burden; steps have no automatic idempotency | inspect run records and handle explicitly |
588
+ | Per-step host callbacks | Arbitrary code breaks whitelist governance | schema computes (read) / host code (write) |
589
+ | Workflow defn persistence / hot reload | Depends on schema versioning (next on the roadmap) | defn stays code-side JSON + register, like schemas today |
590
+ | Timers / event triggers | Scheduling is a resident-IO concern, orthogonal to pure orchestration | call `run` from the application layer |
591
+ | Placeholders inside gql / array-index paths | Injection surface / per-row iteration semantics | params binding / host-code orchestration |
592
+
511
593
  ## FAQ
512
594
 
513
595
  **How do I use one schema for both MongoDB and PostgreSQL in Python?**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "storepy"
7
- version = "2.5.0"
7
+ version = "2.7.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"
@@ -56,6 +56,10 @@ Issues = "https://github.com/coenddt/py-store/issues"
56
56
  [tool.setuptools.packages.find]
57
57
  where = ["src"]
58
58
 
59
+ # ask() 的 system prompt 知识源(text-to-query 通用契约裁剪版)随包分发
60
+ [tool.setuptools.package-data]
61
+ py_store = ["ask_knowledge.md"]
62
+
59
63
  [tool.pytest.ini_options]
60
64
  testpaths = ["tests"]
61
65
  pythonpath = ["src"]
@@ -20,18 +20,19 @@ py-store — 轻量多后端数据层(Python 版,Rust 单核心架构;支
20
20
  """
21
21
 
22
22
  from collections.abc import Mapping
23
- from contextlib import contextmanager
24
23
  from typing import Any
25
24
 
26
25
  from pymongo.errors import PyMongoError
27
26
 
28
27
  from . import (
28
+ ask,
29
29
  crud,
30
30
  datasource,
31
31
  ddl,
32
32
  feedback,
33
33
  permission,
34
34
  schema,
35
+ workflow,
35
36
  )
36
37
  from . import (
37
38
  executors as executors,
@@ -39,10 +40,16 @@ from . import (
39
40
  from . import (
40
41
  introspect as introspect,
41
42
  )
43
+ from .ask import AskExhausted as AskExhausted
44
+ from .ask import AskResult as AskResult
42
45
  from .datasource import NativeCommandError as NativeCommandError
43
46
  from .datasource import NonAtomicWriteError as NonAtomicWriteError
44
47
  from .datasource import RawSqlError as RawSqlError
45
48
  from .datasource import Session as Session
49
+ from .llm import get_llm as get_llm
50
+ from .llm import make_openai_compat as make_openai_compat
51
+ from .llm import register_llm as register_llm
52
+ from .schema import text2query
46
53
  from .sync import sync_schema
47
54
 
48
55
 
@@ -52,21 +59,6 @@ def _build_pipeline(gql, params=None):
52
59
  gql, params if params is not None else {}, permission.get_context())
53
60
 
54
61
 
55
- @contextmanager
56
- def text2query():
57
- """以 text2query 档执行(功能收缩 + 硬限制),退出恢复原档位。
58
-
59
- AI 问数链路入口;与 ``permission.scoped_roles`` 同构(token-set/reset,嵌套安全)。
60
- 进入档位即等效强制携带用户上下文(core `ensure_profile_ctx`,见执行文档 §4.2)。
61
- """
62
- prev = schema.get_profile()
63
- schema.set_profile('text2query')
64
- try:
65
- yield
66
- finally:
67
- schema.set_profile(prev)
68
-
69
-
70
62
  class Store:
71
63
  """以属性方式访问各 API(**全显式类方法,无动态查找**)
72
64
 
@@ -203,6 +195,35 @@ class Store:
203
195
  """从已注册 schema def 生成指定后端 DDL 文本(纯函数,不连库、不回写;铁律 6)"""
204
196
  return ddl.generate(backend, names)
205
197
 
198
+ # ── 工作流编排(首批:线性 + when 守卫 + fail-fast;见 workflow.py 与设计文档)──
199
+ def registerWorkflow(self, defn: dict) -> dict:
200
+ """注册工作流定义(注册即静态校验,白名单外显式 Err 含 WORKFLOW_UNSUPPORTED)"""
201
+ return workflow.register(defn)
202
+
203
+ def workflows(self, ctx: dict | None = None) -> list[str]:
204
+ """全部可见工作流名(read 白名单过滤)"""
205
+ return workflow.list(ctx)
206
+
207
+ def getWorkflow(self, name: str, ctx: dict | None = None) -> dict:
208
+ """按名取工作流定义(read 白名单过滤;不可见与不存在同形——防枚举)"""
209
+ return workflow.get(name, ctx)
210
+
211
+ async def runWorkflow(self, name: str, input: dict | None = None, *,
212
+ dry_run: bool = False,
213
+ route_override: dict | None = None) -> dict[str, Any]:
214
+ """触发工作流 → 完整 run 文档(终态 failed/rejected 不抛错,以 run.status + error 表达)"""
215
+ return await workflow.run(name, input, dry_run=dry_run, route_override=route_override)
216
+
217
+ # ── AI 问数(L1,只读):自然语言 → LLM 翻译 → text2query 沙箱执行 → 结构化回喂 ──
218
+ # 护栏(档位/ctx/route_override)全部服务端硬编码于 ask.py,零暴露进 LLM 消息面(D5)
219
+ # (先于 ask 赋值取 describe_for_ai:赋值后类体命名空间的 ask 不再是模块)
220
+ describeForAi = staticmethod(ask.describe_for_ai)
221
+ describe_for_ai = describeForAi
222
+ ask = staticmethod(ask.ask)
223
+ # 问数结果/耗尽错误(实例可被 store.AskExhausted 捕获;对齐 PermissionError 先例)
224
+ AskResult = AskResult
225
+ AskExhausted = AskExhausted
226
+
206
227
  # ── 驼峰别名(与上方同名蛇形方法为**同一实现**,仅命名差异)──
207
228
  queryOne = query_one
208
229
  queryWithCount = query_with_count
@@ -214,6 +235,10 @@ class Store:
214
235
  executeRaw = execute_raw
215
236
  executeNative = execute_native
216
237
  generateDdl = generate_ddl
238
+ # 工作流编排(蛇形别名与上方驼峰同实现)
239
+ register_workflow = registerWorkflow
240
+ run_workflow = runWorkflow
241
+ get_workflow = getWorkflow
217
242
 
218
243
  # ── 其余 API 显式绑定(staticmethod:避免实例化后 self 注入)──
219
244
  # Schema 管理
@@ -243,6 +268,19 @@ class Store:
243
268
  set_require_context = staticmethod(schema.set_require_context)
244
269
  requireContext = staticmethod(schema.require_context)
245
270
  require_context = staticmethod(schema.require_context)
271
+ # RBAC 动态策略(判决唯一在 core;本层仅透传配置与查询面)
272
+ setRbac = staticmethod(permission.set_rbac)
273
+ set_rbac = staticmethod(permission.set_rbac)
274
+ rbacEnabled = staticmethod(permission.rbac_enabled)
275
+ rbac_enabled = staticmethod(permission.rbac_enabled)
276
+ rbacCan = staticmethod(permission.rbac_can)
277
+ rbac_can = staticmethod(permission.rbac_can)
278
+ rbacReadableFields = staticmethod(permission.rbac_readable_fields)
279
+ rbac_readable_fields = staticmethod(permission.rbac_readable_fields)
280
+ rbacWritableFields = staticmethod(permission.rbac_writable_fields)
281
+ rbac_writable_fields = staticmethod(permission.rbac_writable_fields)
282
+ rbacRowCondition = staticmethod(permission.rbac_row_condition)
283
+ rbac_row_condition = staticmethod(permission.rbac_row_condition)
246
284
  # 查询档位(判决唯一在 core):standard 默认放开 / text2query 功能收缩
247
285
  # 进入档即等效强制 ctx;未知档由 core 抛 ValueError 上抛(禁静默回落默认档)
248
286
  setProfile = staticmethod(schema.set_profile)