matrx-orm 3.1.8__tar.gz → 3.1.10__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 (275) hide show
  1. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/PKG-INFO +1 -1
  2. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/__init__.py +4 -0
  3. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/async_db_manager.py +15 -1
  4. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/config.py +18 -0
  5. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/exceptions.py +7 -2
  6. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/__init__.py +8 -1
  7. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/errors.py +26 -0
  8. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/managed.py +215 -17
  9. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/session.py +22 -0
  10. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/pyproject.toml +1 -1
  11. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_managed_write_guard.py +53 -0
  12. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/test_sync_twins_guard.py +9 -0
  13. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/.gitignore +0 -0
  14. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/BASE_CLASS_METHODS.md +0 -0
  15. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/CLAUDE.md +0 -0
  16. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/MODEL_API.md +0 -0
  17. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/README.md +0 -0
  18. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/database/orm/extended/managers/ai_model_base.py +0 -0
  19. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/docs/migrations.md +0 -0
  20. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/MODULE_README.md +0 -0
  21. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/postgresql.py +0 -0
  26. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  27. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/adapters/supabase_adapter.py +0 -0
  28. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/admin/router.py +0 -0
  30. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/MODULE_README.md +0 -0
  39. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/__init__.py +0 -0
  40. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/postgres_connection.py +0 -0
  41. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/postgrest.py +0 -0
  42. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/supabase_auth.py +0 -0
  43. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/supabase_config.py +0 -0
  44. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/client/supabase_manager.py +0 -0
  45. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  46. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/MODULE_README.md +0 -0
  47. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  48. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/TASKS.md +0 -0
  49. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/__init__.py +0 -0
  50. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/base.py +0 -0
  51. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/db_function.py +0 -0
  52. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/diagnostics.py +0 -0
  53. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/expressions.py +0 -0
  54. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/extended.py +0 -0
  55. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/fields.py +0 -0
  56. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/introspection.py +0 -0
  57. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/listen.py +0 -0
  58. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/loop_filters.py +0 -0
  59. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/model_dto.py +0 -0
  60. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/model_view.py +0 -0
  61. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/paginator.py +0 -0
  62. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/pydantic_bridge.py +0 -0
  63. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/registry.py +0 -0
  64. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/relations.py +0 -0
  65. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/resilience.py +0 -0
  66. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/rls_session.py +0 -0
  67. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/signals.py +0 -0
  68. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/transaction.py +0 -0
  69. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/types.py +0 -0
  70. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/core/write_queue.py +0 -0
  71. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/entity.py +0 -0
  72. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/error_handling.py +0 -0
  73. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/extended/__init__.py +0 -0
  74. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/extended/app_error_handler.py +0 -0
  75. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/middleware/__init__.py +0 -0
  76. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/middleware/base.py +0 -0
  77. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/MODULE_README.md +0 -0
  78. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/__init__.py +0 -0
  79. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/cli.py +0 -0
  80. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/ddl.py +0 -0
  81. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/diff.py +0 -0
  82. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/executor.py +0 -0
  83. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/integration.py +0 -0
  84. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/loader.py +0 -0
  85. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/operations.py +0 -0
  86. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/state.py +0 -0
  87. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/migrations/table_filter.py +0 -0
  88. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/MODULE_README.md +0 -0
  89. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/__init__.py +0 -0
  90. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/bulk_update_values.py +0 -0
  91. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/conflict.py +0 -0
  92. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/conflict_writes.py +0 -0
  93. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/create.py +0 -0
  94. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/delete.py +0 -0
  95. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/dynamic_admin.py +0 -0
  96. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/queue_claim.py +0 -0
  97. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/read.py +0 -0
  98. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/staging_load.py +0 -0
  99. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/operations/update.py +0 -0
  100. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/py.typed +0 -0
  101. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/python_sql/MODULE_README.md +0 -0
  102. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/python_sql/__init__.py +0 -0
  103. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/python_sql/db_objects.py +0 -0
  104. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  105. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  106. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/query/MODULE_README.md +0 -0
  107. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/query/__init__.py +0 -0
  108. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/query/builder.py +0 -0
  109. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/query/executor.py +0 -0
  110. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  111. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/__init__.py +0 -0
  112. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/code_handler.py +0 -0
  113. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  114. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/columns.py +0 -0
  115. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/common.py +0 -0
  116. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/diff_preview.py +0 -0
  117. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  118. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/generator.py +0 -0
  119. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  120. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  121. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  122. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  123. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/package_wiring.py +0 -0
  124. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/relationships.py +0 -0
  125. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/runner.py +0 -0
  126. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/schema.py +0 -0
  127. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/schema_manager.py +0 -0
  128. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/tables.py +0 -0
  129. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/schema_builder/views.py +0 -0
  130. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/README.md +0 -0
  131. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/coalesce.py +0 -0
  132. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/dag.py +0 -0
  133. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/fallback.py +0 -0
  134. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/flush.py +0 -0
  135. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/lifecycle.py +0 -0
  136. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/op.py +0 -0
  137. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/reads.py +0 -0
  138. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/session/telemetry.py +0 -0
  139. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  140. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/__init__.py +0 -0
  141. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/executor.py +0 -0
  142. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/queries.py +0 -0
  143. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/registry.py +0 -0
  144. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/types.py +0 -0
  145. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/sql_executor/utils.py +0 -0
  146. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/state.py +0 -0
  147. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/utils/__init__.py +0 -0
  148. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/utils/sql_utils.py +0 -0
  149. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/matrx_orm/utils/type_converters.py +0 -0
  150. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/scripts/git-branches.sh +0 -0
  151. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/scripts/publish.sh +0 -0
  152. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/scripts/release.sh +0 -0
  153. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/MODULE_README.md +0 -0
  154. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/__init__.py +0 -0
  155. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/conftest.py +0 -0
  156. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/MODULE_README.md +0 -0
  157. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/__init__.py +0 -0
  158. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_admin_db_columns.py +0 -0
  159. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_admin_search_sentinels.py +0 -0
  160. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_advisory_lock.py +0 -0
  161. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_agent_message.py +0 -0
  162. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_api_auth.py +0 -0
  163. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_api_config.py +0 -0
  164. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_api_handlers.py +0 -0
  165. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_api_protocol.py +0 -0
  166. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_array_agg_order_by.py +0 -0
  167. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_associations.py +0 -0
  168. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_bulk_update_by_pk.py +0 -0
  169. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  170. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_cache_debug.py +0 -0
  171. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_call_function.py +0 -0
  172. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_case_when.py +0 -0
  173. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_composite_pk_filter.py +0 -0
  174. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_composite_pk_write_paths.py +0 -0
  175. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_config.py +0 -0
  176. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_conflict_target.py +0 -0
  177. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_conflict_writes.py +0 -0
  178. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_connection_codecs.py +0 -0
  179. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_connection_poison_guard.py +0 -0
  180. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_count_composite_distinct.py +0 -0
  181. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_db_function_field.py +0 -0
  182. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_ddl_generator.py +0 -0
  183. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_dirty_tracking.py +0 -0
  184. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_exceptions.py +0 -0
  185. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_execute_admin_sql.py +0 -0
  186. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_expression_primitives.py +0 -0
  187. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_fields.py +0 -0
  188. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  189. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_filter_raw.py +0 -0
  190. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_from_alias.py +0 -0
  191. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_fts.py +0 -0
  192. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_func_expression_args.py +0 -0
  193. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_having_annotate_alias.py +0 -0
  194. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  195. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_introspect_rls_policies.py +0 -0
  196. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_join_and_aggregate.py +0 -0
  197. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_listen_notify.py +0 -0
  198. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_matrx_entity.py +0 -0
  199. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_migration_diff_types.py +0 -0
  200. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_migration_loader.py +0 -0
  201. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_model_instance.py +0 -0
  202. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_model_meta.py +0 -0
  203. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_order_by_expression.py +0 -0
  204. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_patch_jsonb_path.py +0 -0
  205. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_postgrest_filters.py +0 -0
  206. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_query_builder.py +0 -0
  207. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_query_error_detail.py +0 -0
  208. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_query_executor_sql.py +0 -0
  209. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_query_timeout.py +0 -0
  210. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  211. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_queue_claim.py +0 -0
  212. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_registry.py +0 -0
  213. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_registry_multi_database.py +0 -0
  214. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_registry_multi_schema.py +0 -0
  215. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_relations.py +0 -0
  216. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_resilience.py +0 -0
  217. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_schema_exists.py +0 -0
  218. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_session_advisory_lock.py +0 -0
  219. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_state_cache.py +0 -0
  220. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_subquery_filter_raw.py +0 -0
  221. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_subquery_in_filter.py +0 -0
  222. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_supabase_auth.py +0 -0
  223. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_supabase_config.py +0 -0
  224. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_update_case_expression.py +0 -0
  225. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_update_subquery.py +0 -0
  226. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_upsert_with_conflict.py +0 -0
  227. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_values_jsonb_decode.py +0 -0
  228. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_watched_lifecycle.py +0 -0
  229. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level1/test_write_retry_semantics.py +0 -0
  230. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/MODULE_README.md +0 -0
  231. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/__init__.py +0 -0
  232. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/conftest.py +0 -0
  233. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_bulk_ops.py +0 -0
  234. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_cache_integration.py +0 -0
  235. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_crud.py +0 -0
  236. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_foreign_keys.py +0 -0
  237. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_m2m.py +0 -0
  238. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_manager.py +0 -0
  239. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_migrations_live.py +0 -0
  240. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_query_execution.py +0 -0
  241. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/level2/test_schema_diff.py +0 -0
  242. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/.env.example +0 -0
  243. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/README.md +0 -0
  244. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/__init__.py +0 -0
  245. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/generate.py +0 -0
  246. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/generated/.gitkeep +0 -0
  247. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/matrx_orm.yaml +0 -0
  248. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project/test_schema_generation.py +0 -0
  249. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/.env.example +0 -0
  250. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/README.md +0 -0
  251. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/__init__.py +0 -0
  252. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/client_example.py +0 -0
  253. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/client_example.ts +0 -0
  254. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  255. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/sample_project_desktop/server.py +0 -0
  256. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/schema/entity_tests.py +0 -0
  257. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/schema/test_base_generation.py +0 -0
  258. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  259. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/schema/test_generate_schema.py +0 -0
  260. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/schema/test_multi_schema_output.py +0 -0
  261. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/__init__.py +0 -0
  262. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/conftest.py +0 -0
  263. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_capture_poison_proof.py +0 -0
  264. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_coalesce.py +0 -0
  265. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_dag.py +0 -0
  266. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_disk_spill_fallback.py +0 -0
  267. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_flush_individual_fallback.py +0 -0
  268. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_governed_write.py +0 -0
  269. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_managed.py +0 -0
  270. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_op_enqueue_site.py +0 -0
  271. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_reads.py +0 -0
  272. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/session/test_session.py +0 -0
  273. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/test_database_name_alias.py +0 -0
  274. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/test_model_cls_refactor.py +0 -0
  275. {matrx_orm-3.1.8 → matrx_orm-3.1.10}/tests/test_sql_param_casts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.8
3
+ Version: 3.1.10
4
4
  Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
5
5
  Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
6
6
  Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
@@ -270,6 +270,7 @@ def _lazy_api():
270
270
  # ordering, coalescing, pending-aware reads, managed-write enforcement,
271
271
  # transactional flush, watchdog integration. See matrx_orm/session/.
272
272
  from matrx_orm.session import (
273
+ CoordinatorWriteViolation,
273
274
  DependencyCycleError,
274
275
  FlushError,
275
276
  FlushReport,
@@ -281,6 +282,7 @@ from matrx_orm.session import (
281
282
  SessionPhase,
282
283
  WatchedLifecycleConfig,
283
284
  WatchedLifecycleRegistry,
285
+ allow_direct_managed_write,
284
286
  current_session,
285
287
  declare_managed,
286
288
  drain_spilled_ops,
@@ -296,6 +298,8 @@ from matrx_orm.session import (
296
298
  )
297
299
 
298
300
  __all__ = [
301
+ "CoordinatorWriteViolation",
302
+ "allow_direct_managed_write",
299
303
  # ── Configuration ──────────────────────────────────────────────────────────
300
304
  "DatabaseProjectConfig",
301
305
  "register_database",
@@ -36,6 +36,20 @@ from matrx_orm.exceptions import (
36
36
 
37
37
  logger = logging.getLogger("matrx_orm.async_db_manager")
38
38
 
39
+
40
+ def _resolve_ssl(value: Any) -> Any:
41
+ """Normalize DatabaseProjectConfig.ssl for asyncpg's ``ssl=`` kwarg.
42
+
43
+ ``False`` / ``"disable"`` / ``"off"`` → no TLS (Coolify / local Postgres).
44
+ Everything else is passed through (default ``"require"`` for Supabase).
45
+ """
46
+ if value is False or value is None:
47
+ return False
48
+ if isinstance(value, str) and value.strip().lower() in {"disable", "off", "false", "0"}:
49
+ return False
50
+ return value
51
+
52
+
39
53
  # asyncpg SQLSTATE class 42 "undefined …" — the SQL named something the live DB
40
54
  # does not have. All siblings of SyntaxOrAccessError; PostgresSyntaxError is
41
55
  # caught separately (it's an ORM-bug shape, not a schema-drift shape). These map
@@ -409,7 +423,7 @@ class AsyncDatabaseManager:
409
423
  max_size=config.get("pool_max", 30),
410
424
  command_timeout=config.get("command_timeout", 10),
411
425
  max_inactive_connection_lifetime=max_inactive_connection_lifetime,
412
- ssl="require",
426
+ ssl=_resolve_ssl(config.get("ssl", "require")),
413
427
  statement_cache_size=statement_cache_size,
414
428
  server_settings=server_settings,
415
429
  init=init_callback,
@@ -83,6 +83,14 @@ class DatabaseProjectConfig:
83
83
  # Supabase compute can spike first-query latency on cold connections).
84
84
  server_settings: dict[str, str] = field(default_factory=dict)
85
85
 
86
+ # TLS for the asyncpg pool. Default ``"require"`` matches Supabase /
87
+ # managed Postgres (which reject plaintext). Direct Coolify / local
88
+ # Postgres images typically have ``ssl=off`` — set ``ssl=False`` (or
89
+ # ``"disable"``) for those. Accepted values are whatever asyncpg accepts
90
+ # for its ``ssl=`` kwarg (``False``, ``True``, ``"require"``, an
91
+ # ``ssl.SSLContext``, …).
92
+ ssl: Any = "require"
93
+
86
94
  # Write queue — gates write operations behind an async queue so they wait in
87
95
  # order instead of racing for pool connections and timing out under load.
88
96
  write_queue_enabled: bool = True
@@ -242,6 +250,7 @@ class DatabaseRegistry:
242
250
  "command_timeout": config.command_timeout,
243
251
  "session_init_sql": list(config.session_init_sql),
244
252
  "server_settings": dict(config.server_settings),
253
+ "ssl": config.ssl,
245
254
  "write_queue_enabled": config.write_queue_enabled,
246
255
  "write_concurrency": config.write_concurrency,
247
256
  "write_queue_size": config.write_queue_size,
@@ -408,6 +417,7 @@ def register_database_from_env(
408
417
  command_timeout: int | None = None,
409
418
  session_init_sql: list[str] | None = None,
410
419
  server_settings: dict[str, str] | None = None,
420
+ ssl: Any = None,
411
421
  write_queue_enabled: bool | None = None,
412
422
  write_concurrency: int | None = None,
413
423
  write_queue_size: int | None = None,
@@ -562,6 +572,13 @@ def register_database_from_env(
562
572
  supabase_secret_key or os.environ.get(_supabase_secret_env, "").strip()
563
573
  )
564
574
 
575
+ _ssl_env = _overrides.get("SSL", f"{env_prefix}_SSL")
576
+ if ssl is not None:
577
+ _resolved_ssl: Any = ssl
578
+ else:
579
+ _ssl_raw = os.environ.get(_ssl_env, "").strip()
580
+ _resolved_ssl = _ssl_raw if _ssl_raw else "require"
581
+
565
582
  try:
566
583
  config = DatabaseProjectConfig(
567
584
  name=name,
@@ -582,6 +599,7 @@ def register_database_from_env(
582
599
  supabase_secret_key=_resolved_supabase_secret_key,
583
600
  session_init_sql=list(session_init_sql or []),
584
601
  server_settings=dict(server_settings or {}),
602
+ ssl=_resolved_ssl,
585
603
  **pool_kwargs,
586
604
  )
587
605
  registry.register(config)
@@ -1472,8 +1472,13 @@ class MigrationError(ORMException):
1472
1472
  return f"{_RED}{body}{_RESET}"
1473
1473
 
1474
1474
 
1475
- class ParameterError(ORMException):
1476
- """Raised when query parameters are invalid or malformed."""
1475
+ class ParameterError(ORMException, ValueError):
1476
+ """Raised when query parameters are invalid or malformed.
1477
+
1478
+ Subclasses ValueError: a malformed query parameter (e.g. a non-UUID
1479
+ string for a UUID column) IS an invalid value, so consumers that map
1480
+ ``ValueError`` to a 400/invalid-request response catch it correctly
1481
+ instead of surfacing an unmapped 500."""
1477
1482
 
1478
1483
  def __init__(
1479
1484
  self,
@@ -50,6 +50,7 @@ Usage:
50
50
  from matrx_orm.session.coalesce import coalesce_ops, summarize
51
51
  from matrx_orm.session.dag import DependencyCycleError, tier_ops, tier_summary
52
52
  from matrx_orm.session.errors import (
53
+ CoordinatorWriteViolation,
53
54
  FlushError,
54
55
  ManagedWriteViolation,
55
56
  SessionClosedError,
@@ -65,7 +66,11 @@ from matrx_orm.session.lifecycle import (
65
66
  run_sweeper,
66
67
  sweep_once,
67
68
  )
68
- from matrx_orm.session.managed import declare_managed, install_managed_writes
69
+ from matrx_orm.session.managed import (
70
+ allow_direct_managed_write,
71
+ declare_managed,
72
+ install_managed_writes,
73
+ )
69
74
  from matrx_orm.session.op import (
70
75
  OpType,
71
76
  SessionOp,
@@ -90,6 +95,7 @@ from matrx_orm.session.session import (
90
95
 
91
96
  __all__ = [
92
97
  "DependencyCycleError",
98
+ "CoordinatorWriteViolation",
93
99
  "FlushError",
94
100
  "FlushReport",
95
101
  "ManagedWriteViolation",
@@ -106,6 +112,7 @@ __all__ = [
106
112
  "coalesce_ops",
107
113
  "config_from_meta",
108
114
  "current_session",
115
+ "allow_direct_managed_write",
109
116
  "declare_managed",
110
117
  "has_pending_in_stack",
111
118
  "install_managed_writes",
@@ -48,6 +48,32 @@ class ManagedWriteViolation(SessionError):
48
48
  self.op_kind = op_kind
49
49
 
50
50
 
51
+ class CoordinatorWriteViolation(ManagedWriteViolation):
52
+ """Raised when a coordinator-owned Model is written by another path."""
53
+
54
+ def __init__(
55
+ self,
56
+ model_name: str,
57
+ op_kind: str,
58
+ *,
59
+ required_scope: str,
60
+ actual_scope: str | None,
61
+ ) -> None:
62
+ SessionError.__init__(
63
+ self,
64
+ f"{model_name} is coordinator-owned (required write scope: "
65
+ f"{required_scope!r}). Direct {op_kind} is forbidden; the active "
66
+ f"Session scope is {actual_scope!r}. Queue this write through its "
67
+ "Coordinator so parent and child rows share one transactional DAG. "
68
+ "For a genuine out-of-request maintenance operation, use "
69
+ "allow_direct_managed_write(model, reason='...') explicitly.",
70
+ )
71
+ self.model_name = model_name
72
+ self.op_kind = op_kind
73
+ self.required_scope = required_scope
74
+ self.actual_scope = actual_scope
75
+
76
+
51
77
  class SessionClosedError(SessionError):
52
78
  """Raised when an op is queued onto a Session that has already flushed
53
79
  or errored. Generally callers should use ``async with Session():`` so
@@ -1,8 +1,8 @@
1
1
  """Managed Model write interception — the runtime enforcement layer.
2
2
 
3
3
  A Model opts into Session-aware writes by declaring
4
- ``Meta.managed_writes`` as either ``"elevate"`` (default behavior when
5
- ``True``) or ``"strict"``::
4
+ ``Meta.managed_writes`` as ``"elevate"`` (default behavior when ``True``),
5
+ ``"strict"``, or ``"coordinator"``::
6
6
 
7
7
  class Conversation(Model):
8
8
  ...
@@ -10,6 +10,8 @@ A Model opts into Session-aware writes by declaring
10
10
  managed_writes = "elevate" # auto-open one-shot if no Session
11
11
  # OR
12
12
  managed_writes = "strict" # raise ManagedWriteViolation if no Session
13
+ # OR
14
+ managed_writes = "coordinator" # require a named owner Session
13
15
 
14
16
  When the Model's ``create`` / ``update`` / ``delete`` / ``bulk_*`` /
15
17
  ``upsert`` methods are called:
@@ -49,12 +51,19 @@ from __future__ import annotations
49
51
  import functools
50
52
  import logging
51
53
  import re
52
- from contextlib import asynccontextmanager
54
+ from contextlib import asynccontextmanager, contextmanager
55
+ from contextvars import ContextVar
56
+ from dataclasses import dataclass
57
+ from collections.abc import Iterator
53
58
  from typing import Any
54
59
 
55
60
  from matrx_utils import vcprint
56
61
 
57
- from matrx_orm.session.errors import MANAGED_WRITE_SKILL_HINT, ManagedWriteViolation
62
+ from matrx_orm.session.errors import (
63
+ MANAGED_WRITE_SKILL_HINT,
64
+ CoordinatorWriteViolation,
65
+ ManagedWriteViolation,
66
+ )
58
67
  from matrx_orm.session.session import Session, SessionPhase, session_stack
59
68
 
60
69
  logger = logging.getLogger("matrx_orm.session.managed")
@@ -82,6 +91,49 @@ logger = logging.getLogger("matrx_orm.session.managed")
82
91
 
83
92
  _STRICT_TABLES: set[str] = set()
84
93
 
94
+ # table token -> required Session.write_scope. This is stronger than strict:
95
+ # merely opening a Session/transaction is not authority to write the table.
96
+ _COORDINATOR_TABLES: dict[str, str] = {}
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class DirectWriteException:
101
+ model_name: str
102
+ reason: str
103
+
104
+
105
+ _direct_write_exceptions: ContextVar[tuple[DirectWriteException, ...]] = ContextVar(
106
+ "matrx_orm_direct_write_exceptions",
107
+ default=(),
108
+ )
109
+
110
+
111
+ @contextmanager
112
+ def allow_direct_managed_write(model_cls: type, *, reason: str) -> Iterator[None]:
113
+ """Explicit, scoped escape hatch for coordinator-owned maintenance writes.
114
+
115
+ ``reason`` is mandatory and appears in logs. This is intentionally not a
116
+ boolean flag: exceptional code must state why it cannot use the owner.
117
+ """
118
+ cleaned = reason.strip()
119
+ if not cleaned:
120
+ raise ValueError("allow_direct_managed_write requires a non-empty reason")
121
+ exception = DirectWriteException(model_cls.__name__, cleaned)
122
+ token = _direct_write_exceptions.set(_direct_write_exceptions.get() + (exception,))
123
+ logger.warning(
124
+ "DIRECT WRITE EXCEPTION entered for %s: %s",
125
+ model_cls.__name__,
126
+ cleaned,
127
+ )
128
+ try:
129
+ yield
130
+ finally:
131
+ _direct_write_exceptions.reset(token)
132
+
133
+
134
+ def _has_direct_exception(model_cls: type) -> bool:
135
+ return any(item.model_name == model_cls.__name__ for item in _direct_write_exceptions.get())
136
+
85
137
  # Leading INSERT INTO / UPDATE / DELETE FROM target — ORM-generated SQL only.
86
138
  _WRITE_TARGET_RE = re.compile(
87
139
  r"^\s*(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([^\s(]+)",
@@ -101,6 +153,63 @@ def _register_strict_table(model_cls: type) -> None:
101
153
  _STRICT_TABLES.add(str(name).strip('"'))
102
154
 
103
155
 
156
+ def _register_coordinator_table(model_cls: type, required_scope: str) -> None:
157
+ meta = getattr(model_cls, "_meta", None)
158
+ if meta is None:
159
+ return
160
+ for name in (
161
+ getattr(meta, "qualified_table_name", None),
162
+ getattr(meta, "table_name", None),
163
+ ):
164
+ if name:
165
+ _COORDINATOR_TABLES[str(name).strip('"')] = required_scope
166
+
167
+
168
+ def _coordinator_scope_for_model(model_cls: type) -> str | None:
169
+ meta = getattr(model_cls, "_meta", None)
170
+ if meta is None:
171
+ return None
172
+ for name in (
173
+ getattr(meta, "qualified_table_name", None),
174
+ getattr(meta, "table_name", None),
175
+ ):
176
+ if name and (scope := _COORDINATOR_TABLES.get(str(name).strip('"'))):
177
+ return scope
178
+ return None
179
+
180
+
181
+ def assert_model_write_allowed(
182
+ model_cls: type,
183
+ op_kind: str,
184
+ *,
185
+ session: Session | None = None,
186
+ ) -> None:
187
+ """Reject a coordinator-owned Model write without its owner Session."""
188
+ required_scope = _coordinator_scope_for_model(model_cls)
189
+ if required_scope is None or _has_direct_exception(model_cls):
190
+ return
191
+ active = session
192
+ if active is None:
193
+ for candidate in reversed(session_stack()):
194
+ if candidate.phase is SessionPhase.OPEN:
195
+ active = candidate
196
+ break
197
+ actual_scope = active.write_scope if active is not None else None
198
+ if actual_scope == required_scope:
199
+ return
200
+ banner = _format_coordinator_banner(
201
+ model_cls.__name__, op_kind, required_scope, actual_scope
202
+ )
203
+ vcprint(banner, color="red")
204
+ logger.error("CoordinatorWriteViolation: %s", banner)
205
+ raise CoordinatorWriteViolation(
206
+ model_cls.__name__,
207
+ op_kind,
208
+ required_scope=required_scope,
209
+ actual_scope=actual_scope,
210
+ )
211
+
212
+
104
213
  def _has_active_open_session() -> bool:
105
214
  for candidate in reversed(session_stack()):
106
215
  if candidate.phase is SessionPhase.OPEN:
@@ -116,12 +225,35 @@ def assert_managed_write_allowed(query: str) -> None:
116
225
  — when no strict table exists. Best-effort table parsing: ORM-generated DML
117
226
  only; a query we can't parse is allowed through (the wrappers + the strict
118
227
  create path remain the first line of defense)."""
119
- if not _STRICT_TABLES:
228
+ if not _STRICT_TABLES and not _COORDINATOR_TABLES:
120
229
  return
121
- # Governed contexts always allowed:
230
+ # Session.defer_* is authority-checked before an op can enter a flush.
231
+ # The flush may run in a deliberately detached context where its Session
232
+ # stack is absent, so the internal execution phase is trusted here.
122
233
  from matrx_orm.session.flush import is_flushing
123
234
 
124
- if is_flushing() or _has_active_open_session():
235
+ if is_flushing():
236
+ return
237
+ m = _WRITE_TARGET_RE.match(query)
238
+ if not m:
239
+ return
240
+ token = m.group(1).strip().strip('"')
241
+ bare_token = token.split(".")[-1].strip('"')
242
+
243
+ required_scope = _COORDINATOR_TABLES.get(token) or _COORDINATOR_TABLES.get(bare_token)
244
+ if required_scope is not None:
245
+ model_cls = _model_for_coordinator_table(token, bare_token)
246
+ if model_cls is not None:
247
+ assert_model_write_allowed(model_cls, "write")
248
+ else:
249
+ actual = _active_write_scope()
250
+ if actual != required_scope:
251
+ raise CoordinatorWriteViolation(
252
+ token, "write", required_scope=required_scope, actual_scope=actual
253
+ )
254
+ return
255
+ # Governed contexts — always allowed:
256
+ if _has_active_open_session():
125
257
  return
126
258
  try:
127
259
  from matrx_orm.core.transaction import get_active_connection
@@ -131,10 +263,6 @@ def assert_managed_write_allowed(query: str) -> None:
131
263
  except ImportError:
132
264
  pass
133
265
 
134
- m = _WRITE_TARGET_RE.match(query)
135
- if not m:
136
- return
137
- token = m.group(1).strip().strip('"')
138
266
  if token in _STRICT_TABLES or token.split(".")[-1].strip('"') in _STRICT_TABLES:
139
267
  banner = _format_chokepoint_banner(token)
140
268
  vcprint(banner, color="red")
@@ -146,6 +274,33 @@ def assert_managed_write_allowed(query: str) -> None:
146
274
  raise ManagedWriteViolation(token, "write (ungoverned)")
147
275
 
148
276
 
277
+ def _active_write_scope() -> str | None:
278
+ for candidate in reversed(session_stack()):
279
+ if candidate.phase is SessionPhase.OPEN:
280
+ return candidate.write_scope
281
+ return None
282
+
283
+
284
+ def _model_for_coordinator_table(token: str, bare_token: str) -> type | None:
285
+ from matrx_orm.core.registry import ModelRegistry
286
+
287
+ for model_cls in ModelRegistry.all_models().values():
288
+ meta = getattr(model_cls, "_meta", None)
289
+ if meta is None:
290
+ continue
291
+ names = {
292
+ str(name).strip('"')
293
+ for name in (
294
+ getattr(meta, "qualified_table_name", None),
295
+ getattr(meta, "table_name", None),
296
+ )
297
+ if name
298
+ }
299
+ if token in names or bare_token in names:
300
+ return model_cls
301
+ return None
302
+
303
+
149
304
  def is_strict_table(model_cls: type) -> bool:
150
305
  """True if ``model_cls``'s table is registered ``strict`` (execute_write-guarded).
151
306
 
@@ -235,6 +390,13 @@ def _apply_wrapping(model_cls: type, mode: str) -> None:
235
390
  # Register for the universal execute_write guard so update/delete/
236
391
  # QueryBuilder/raw-SQL paths are enforced too, not just create/bulk_*.
237
392
  _register_strict_table(model_cls)
393
+ elif mode == "coordinator":
394
+ required_scope = getattr(
395
+ getattr(model_cls, "_meta", None),
396
+ "managed_write_scope",
397
+ "coordinator",
398
+ )
399
+ _register_coordinator_table(model_cls, str(required_scope))
238
400
  for method_name in _WRITE_METHODS:
239
401
  original = getattr(model_cls, method_name, None)
240
402
  if original is None:
@@ -251,6 +413,7 @@ def declare_managed(
251
413
  model_cls: type,
252
414
  *,
253
415
  mode: str = "elevate",
416
+ write_scope: str = "coordinator",
254
417
  watched_lifecycle: dict | None = None,
255
418
  ) -> None:
256
419
  """Programmatic equivalent of declaring ``Meta.managed_writes`` /
@@ -281,15 +444,19 @@ def declare_managed(
281
444
 
282
445
  Args:
283
446
  model_cls: a matrx-orm Model class.
284
- mode: ``"elevate"`` (default — auto-open one-shot Session if
285
- none is active) or ``"strict"`` (raise
286
- ``ManagedWriteViolation`` outside any Session).
447
+ mode: ``"elevate"`` (auto-open a one-shot), ``"strict"`` (require
448
+ any governed Session), or ``"coordinator"`` (require the named
449
+ owner Session and reject ordinary Session-of-one writes).
450
+ write_scope: authority required by coordinator mode.
287
451
  watched_lifecycle: optional dict matching the
288
452
  :class:`WatchedLifecycleConfig` fields (``predicate``,
289
453
  ``max_age_secs``, ``transition_to_on_timeout``, etc.).
290
454
  """
291
- if mode not in ("strict", "elevate"):
292
- raise ValueError(f"declare_managed: mode must be 'strict' or 'elevate', got {mode!r}")
455
+ if mode not in ("strict", "elevate", "coordinator"):
456
+ raise ValueError(
457
+ "declare_managed: mode must be 'strict', 'elevate', or "
458
+ f"'coordinator', got {mode!r}"
459
+ )
293
460
  meta = getattr(model_cls, "_meta", None)
294
461
  if meta is None:
295
462
  raise ValueError(
@@ -304,6 +471,10 @@ def declare_managed(
304
471
  object.__setattr__(meta, "managed_writes", mode)
305
472
 
306
473
  _apply_wrapping(model_cls, mode)
474
+ if mode == "coordinator":
475
+ if not write_scope.strip():
476
+ raise ValueError("declare_managed coordinator mode requires write_scope")
477
+ _register_coordinator_table(model_cls, write_scope.strip())
307
478
 
308
479
  if watched_lifecycle is not None:
309
480
  try:
@@ -337,7 +508,7 @@ def _managed_mode(model_cls: type) -> str | None:
337
508
  return None
338
509
  if declared is True:
339
510
  return "elevate"
340
- if isinstance(declared, str) and declared in ("strict", "elevate"):
511
+ if isinstance(declared, str) and declared in ("strict", "elevate", "coordinator"):
341
512
  return declared
342
513
  logger.warning(
343
514
  "managed_writes on %s must be True | 'strict' | 'elevate' — got %r. Treating as 'elevate'.",
@@ -381,6 +552,8 @@ def _wrap_classmethod(model_cls: type, method_name: str, original: Any, mode: st
381
552
  session = candidate
382
553
  break
383
554
  if session is not None:
555
+ if mode == "coordinator":
556
+ assert_model_write_allowed(cls, method_name, session=session)
384
557
  # Defer to the active Session.
385
558
  return await _defer_to_session(
386
559
  session,
@@ -392,6 +565,9 @@ def _wrap_classmethod(model_cls: type, method_name: str, original: Any, mode: st
392
565
  )
393
566
 
394
567
  # No active Session.
568
+ if mode == "coordinator":
569
+ assert_model_write_allowed(cls, method_name)
570
+
395
571
  if mode == "strict":
396
572
  banner = _format_violation_banner(cls.__name__, method_name)
397
573
  vcprint(banner, color="red")
@@ -565,6 +741,28 @@ def _format_chokepoint_banner(table: str) -> str:
565
741
  )
566
742
 
567
743
 
744
+ def _format_coordinator_banner(
745
+ model_name: str,
746
+ op_kind: str,
747
+ required_scope: str,
748
+ actual_scope: str | None,
749
+ ) -> str:
750
+ return (
751
+ "\n"
752
+ "████████████████████████████████████████████████████████████████████\n"
753
+ "██ COORDINATOR-OWNED TABLE — DIRECT WRITE FORBIDDEN ██\n"
754
+ "████████████████████████████████████████████████████████████████████\n"
755
+ f" Model : {model_name}\n"
756
+ f" Operation : {op_kind}\n"
757
+ f" Required : Session(write_scope={required_scope!r})\n"
758
+ f" Actual : Session(write_scope={actual_scope!r})\n"
759
+ " Fix : queue this write through the registered Coordinator.\n"
760
+ " Exception : allow_direct_managed_write(Model, reason='...')\n"
761
+ " is reserved for genuine out-of-request maintenance.\n"
762
+ "████████████████████████████████████████████████████████████████████\n"
763
+ )
764
+
765
+
568
766
  def _format_violation_banner(model_name: str, op_kind: str) -> str:
569
767
  return (
570
768
  "\n"
@@ -232,6 +232,7 @@ class Session:
232
232
  "_max_age_secs",
233
233
  "_opened_at",
234
234
  "_stack_token",
235
+ "_write_scope",
235
236
  )
236
237
 
237
238
  def __init__(
@@ -241,6 +242,7 @@ class Session:
241
242
  autoflush_on_exception: bool = True,
242
243
  expected_writes: int | None = None,
243
244
  max_age_secs: int | None = None,
245
+ write_scope: str | None = None,
244
246
  ) -> None:
245
247
  self._id = str(uuid.uuid4())
246
248
  self._database = database
@@ -253,6 +255,7 @@ class Session:
253
255
  self._max_age_secs = max_age_secs
254
256
  self._opened_at = datetime.now(UTC)
255
257
  self._stack_token: Any = None
258
+ self._write_scope = write_scope
256
259
 
257
260
  # ---------------------- properties / introspection -------------------
258
261
 
@@ -272,6 +275,16 @@ class Session:
272
275
  def database(self) -> str | None:
273
276
  return self._database
274
277
 
278
+ @property
279
+ def write_scope(self) -> str | None:
280
+ """Named authority carried by this Unit of Work.
281
+
282
+ Models declared with ``managed_writes='coordinator'`` accept writes
283
+ only from a Session whose scope matches their registered owner.
284
+ Ordinary ``Session()`` instances deliberately carry no authority.
285
+ """
286
+ return self._write_scope
287
+
275
288
  @property
276
289
  def report(self) -> FlushReport | None:
277
290
  return self._report
@@ -322,6 +335,9 @@ class Session:
322
335
  """Queue an INSERT op. Returns the op_id. Never raises (except on
323
336
  a closed Session, which is a programmer error)."""
324
337
  self._guard_open(op_kind="insert", model_name=model_cls.__name__)
338
+ from matrx_orm.session.managed import assert_model_write_allowed
339
+
340
+ assert_model_write_allowed(model_cls, "insert", session=self)
325
341
  op = make_insert(model_cls, payload, pk_value=pk_value)
326
342
  self._ops.append(op)
327
343
  return op.op_id
@@ -334,6 +350,9 @@ class Session:
334
350
  ) -> str:
335
351
  """Queue an UPDATE op. Returns the op_id."""
336
352
  self._guard_open(op_kind="update", model_name=model_cls.__name__)
353
+ from matrx_orm.session.managed import assert_model_write_allowed
354
+
355
+ assert_model_write_allowed(model_cls, "update", session=self)
337
356
  op = make_update(model_cls, pk_value, fields)
338
357
  self._ops.append(op)
339
358
  return op.op_id
@@ -345,6 +364,9 @@ class Session:
345
364
  ) -> str:
346
365
  """Queue a DELETE op. Returns the op_id."""
347
366
  self._guard_open(op_kind="delete", model_name=model_cls.__name__)
367
+ from matrx_orm.session.managed import assert_model_write_allowed
368
+
369
+ assert_model_write_allowed(model_cls, "delete", session=self)
348
370
  op = make_delete(model_cls, pk_value)
349
371
  self._ops.append(op)
350
372
  return op.op_id
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.8"
3
+ version = "3.1.10"
4
4
  description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }