matrx-orm 3.1.24__tar.gz → 3.1.26__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 (297) hide show
  1. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/CLAUDE.md +1 -1
  2. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/__init__.py +2 -0
  4. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/associations.py +102 -16
  5. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/async_db_manager.py +34 -0
  6. matrx_orm-3.1.26/matrx_orm/secrets_battery/FEATURE.md +112 -0
  7. matrx_orm-3.1.26/matrx_orm/secrets_battery/__init__.py +136 -0
  8. matrx_orm-3.1.26/matrx_orm/secrets_battery/crypto.py +144 -0
  9. matrx_orm-3.1.26/matrx_orm/secrets_battery/model.py +93 -0
  10. matrx_orm-3.1.26/matrx_orm/secrets_battery/org_service.py +588 -0
  11. matrx_orm-3.1.26/matrx_orm/secrets_battery/service.py +522 -0
  12. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/pyproject.toml +1 -1
  13. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_associations.py +72 -0
  14. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_secrets_battery.py +3 -4
  15. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/.gitignore +0 -0
  16. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/AGENTS.md +0 -0
  17. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/BASE_CLASS_METHODS.md +0 -0
  18. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/MODEL_API.md +0 -0
  19. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/README.md +0 -0
  20. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/database/orm/extended/managers/ai_model_base.py +0 -0
  21. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/docs/migrations.md +0 -0
  22. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/MODULE_README.md +0 -0
  23. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/MODULE_README.md +0 -0
  24. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/__init__.py +0 -0
  25. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/async_postgresql.py +0 -0
  26. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/base_adapter.py +0 -0
  27. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/postgresql.py +0 -0
  28. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  29. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/adapters/supabase_adapter.py +0 -0
  30. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/admin/__init__.py +0 -0
  31. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/admin/router.py +0 -0
  32. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/__init__.py +0 -0
  33. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/auth.py +0 -0
  34. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/config.py +0 -0
  35. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/handlers.py +0 -0
  36. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/protocol.py +0 -0
  37. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/api/server.py +0 -0
  38. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/cache_debug.py +0 -0
  39. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/MODULE_README.md +0 -0
  40. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/__init__.py +0 -0
  41. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/postgres_connection.py +0 -0
  42. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/postgrest.py +0 -0
  43. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/supabase_auth.py +0 -0
  44. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/supabase_config.py +0 -0
  45. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/client/supabase_manager.py +0 -0
  46. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  47. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/MODULE_README.md +0 -0
  48. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  49. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/TASKS.md +0 -0
  50. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/__init__.py +0 -0
  51. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/base.py +0 -0
  52. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/config.py +0 -0
  53. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/db_function.py +0 -0
  54. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/diagnostics.py +0 -0
  55. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/expressions.py +0 -0
  56. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/extended.py +0 -0
  57. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/fields.py +0 -0
  58. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/introspection.py +0 -0
  59. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/listen.py +0 -0
  60. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/loop_filters.py +0 -0
  61. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/model_dto.py +0 -0
  62. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/model_view.py +0 -0
  63. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/paginator.py +0 -0
  64. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/pool_maintenance.py +0 -0
  65. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/pydantic_bridge.py +0 -0
  66. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/registry.py +0 -0
  67. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/relations.py +0 -0
  68. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/resilience.py +0 -0
  69. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/rls_session.py +0 -0
  70. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/signals.py +0 -0
  71. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/tls.py +0 -0
  72. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/transaction.py +0 -0
  73. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/types.py +0 -0
  74. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/core/write_queue.py +0 -0
  75. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/entity.py +0 -0
  76. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/error_handling.py +0 -0
  77. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/exceptions.py +0 -0
  78. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/extended/__init__.py +0 -0
  79. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/extended/app_error_handler.py +0 -0
  80. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/identity_rule.py +0 -0
  81. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/middleware/__init__.py +0 -0
  82. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/middleware/base.py +0 -0
  83. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/MODULE_README.md +0 -0
  84. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/__init__.py +0 -0
  85. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/cli.py +0 -0
  86. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/ddl.py +0 -0
  87. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/diff.py +0 -0
  88. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/executor.py +0 -0
  89. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/integration.py +0 -0
  90. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/loader.py +0 -0
  91. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/operations.py +0 -0
  92. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/state.py +0 -0
  93. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/migrations/table_filter.py +0 -0
  94. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/MODULE_README.md +0 -0
  95. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/__init__.py +0 -0
  96. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/bulk_update_values.py +0 -0
  97. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/conflict.py +0 -0
  98. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/conflict_writes.py +0 -0
  99. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/create.py +0 -0
  100. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/delete.py +0 -0
  101. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/dynamic_admin.py +0 -0
  102. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/queue_claim.py +0 -0
  103. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/read.py +0 -0
  104. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/staging_load.py +0 -0
  105. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/operations/update.py +0 -0
  106. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/py.typed +0 -0
  107. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/python_sql/MODULE_README.md +0 -0
  108. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/python_sql/__init__.py +0 -0
  109. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/python_sql/db_objects.py +0 -0
  110. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  111. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  112. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/query/MODULE_README.md +0 -0
  113. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/query/__init__.py +0 -0
  114. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/query/builder.py +0 -0
  115. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/query/executor.py +0 -0
  116. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  117. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/__init__.py +0 -0
  118. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/code_handler.py +0 -0
  119. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  120. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/columns.py +0 -0
  121. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/common.py +0 -0
  122. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/diff_preview.py +0 -0
  123. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  124. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/generator.py +0 -0
  125. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  126. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  127. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  128. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  129. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/package_wiring.py +0 -0
  130. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/relationships.py +0 -0
  131. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/runner.py +0 -0
  132. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/schema.py +0 -0
  133. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/schema_manager.py +0 -0
  134. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/tables.py +0 -0
  135. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/schema_builder/views.py +0 -0
  136. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/README.md +0 -0
  137. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/__init__.py +0 -0
  138. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/coalesce.py +0 -0
  139. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/dag.py +0 -0
  140. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/errors.py +0 -0
  141. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/fallback.py +0 -0
  142. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/flush.py +0 -0
  143. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/lifecycle.py +0 -0
  144. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/managed.py +0 -0
  145. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/op.py +0 -0
  146. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/reads.py +0 -0
  147. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/session.py +0 -0
  148. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/session/telemetry.py +0 -0
  149. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  150. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/__init__.py +0 -0
  151. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/executor.py +0 -0
  152. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/queries.py +0 -0
  153. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/registry.py +0 -0
  154. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/types.py +0 -0
  155. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/sql_executor/utils.py +0 -0
  156. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/state.py +0 -0
  157. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/utils/__init__.py +0 -0
  158. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/utils/sql_utils.py +0 -0
  159. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/matrx_orm/utils/type_converters.py +0 -0
  160. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/scripts/git-branches.sh +0 -0
  161. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/scripts/publish.sh +0 -0
  162. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/scripts/release.sh +0 -0
  163. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/MODULE_README.md +0 -0
  164. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/__init__.py +0 -0
  165. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/conftest.py +0 -0
  166. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/MODULE_README.md +0 -0
  167. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/__init__.py +0 -0
  168. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_admin_db_columns.py +0 -0
  169. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_admin_search_negation.py +0 -0
  170. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_admin_search_sentinels.py +0 -0
  171. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_advisory_lock.py +0 -0
  172. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_agent_message.py +0 -0
  173. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_api_auth.py +0 -0
  174. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_api_config.py +0 -0
  175. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_api_handlers.py +0 -0
  176. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_api_protocol.py +0 -0
  177. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_array_agg_order_by.py +0 -0
  178. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_bulk_update_by_pk.py +0 -0
  179. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  180. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_cache_debug.py +0 -0
  181. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_call_function.py +0 -0
  182. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_case_when.py +0 -0
  183. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_composite_pk_filter.py +0 -0
  184. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_composite_pk_write_paths.py +0 -0
  185. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_config.py +0 -0
  186. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_conflict_target.py +0 -0
  187. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_conflict_writes.py +0 -0
  188. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_connection_codecs.py +0 -0
  189. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_connection_poison_guard.py +0 -0
  190. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_count_composite_distinct.py +0 -0
  191. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_db_function_field.py +0 -0
  192. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_ddl_generator.py +0 -0
  193. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_derived_table.py +0 -0
  194. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_dirty_tracking.py +0 -0
  195. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_exceptions.py +0 -0
  196. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_execute_admin_sql.py +0 -0
  197. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_expression_primitives.py +0 -0
  198. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_fields.py +0 -0
  199. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  200. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_filter_raw.py +0 -0
  201. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_from_alias.py +0 -0
  202. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_fts.py +0 -0
  203. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_func_expression_args.py +0 -0
  204. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_having_annotate_alias.py +0 -0
  205. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  206. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_introspect_rls_policies.py +0 -0
  207. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_join_and_aggregate.py +0 -0
  208. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_listen_notify.py +0 -0
  209. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_managed_write_guard.py +0 -0
  210. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_matrx_entity.py +0 -0
  211. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_migration_diff_types.py +0 -0
  212. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_migration_loader.py +0 -0
  213. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_model_instance.py +0 -0
  214. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_model_meta.py +0 -0
  215. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_order_by_expression.py +0 -0
  216. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_patch_jsonb_path.py +0 -0
  217. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_pool_maintenance.py +0 -0
  218. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_postgrest_filters.py +0 -0
  219. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_query_builder.py +0 -0
  220. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_query_error_detail.py +0 -0
  221. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_query_executor_sql.py +0 -0
  222. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_query_timeout.py +0 -0
  223. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  224. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_queue_claim.py +0 -0
  225. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_registry.py +0 -0
  226. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_registry_multi_database.py +0 -0
  227. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_registry_multi_schema.py +0 -0
  228. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_relations.py +0 -0
  229. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_resilience.py +0 -0
  230. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_schema_exists.py +0 -0
  231. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_session_advisory_lock.py +0 -0
  232. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_state_cache.py +0 -0
  233. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_subquery_filter_raw.py +0 -0
  234. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_subquery_in_filter.py +0 -0
  235. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_supabase_auth.py +0 -0
  236. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_supabase_config.py +0 -0
  237. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_tls.py +0 -0
  238. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_update_case_expression.py +0 -0
  239. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_update_subquery.py +0 -0
  240. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_upsert_with_conflict.py +0 -0
  241. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_values_jsonb_decode.py +0 -0
  242. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_watched_lifecycle.py +0 -0
  243. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level1/test_write_retry_semantics.py +0 -0
  244. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/MODULE_README.md +0 -0
  245. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/__init__.py +0 -0
  246. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/conftest.py +0 -0
  247. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_bulk_ops.py +0 -0
  248. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_cache_integration.py +0 -0
  249. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_crud.py +0 -0
  250. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_foreign_keys.py +0 -0
  251. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_m2m.py +0 -0
  252. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_manager.py +0 -0
  253. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_migrations_live.py +0 -0
  254. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_query_execution.py +0 -0
  255. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/level2/test_schema_diff.py +0 -0
  256. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/.env.example +0 -0
  257. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/README.md +0 -0
  258. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/__init__.py +0 -0
  259. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/generate.py +0 -0
  260. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/generated/.gitkeep +0 -0
  261. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/matrx_orm.yaml +0 -0
  262. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project/test_schema_generation.py +0 -0
  263. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/.env.example +0 -0
  264. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/README.md +0 -0
  265. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/__init__.py +0 -0
  266. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/client_example.py +0 -0
  267. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/client_example.ts +0 -0
  268. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  269. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/sample_project_desktop/server.py +0 -0
  270. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/entity_tests.py +0 -0
  271. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_base_generation.py +0 -0
  272. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  273. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_composite_unique_generation.py +0 -0
  274. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_dto_identity_generation.py +0 -0
  275. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_entity_capability_generation.py +0 -0
  276. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_generate_schema.py +0 -0
  277. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_multi_schema_output.py +0 -0
  278. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_sql_expression_defaults.py +0 -0
  279. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema/test_view_generation.py +0 -0
  280. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  281. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/__init__.py +0 -0
  282. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/conftest.py +0 -0
  283. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_capture_poison_proof.py +0 -0
  284. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_coalesce.py +0 -0
  285. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_dag.py +0 -0
  286. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_disk_spill_fallback.py +0 -0
  287. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_flush_individual_fallback.py +0 -0
  288. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_governed_write.py +0 -0
  289. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_managed.py +0 -0
  290. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_op_enqueue_site.py +0 -0
  291. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_reads.py +0 -0
  292. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/session/test_session.py +0 -0
  293. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_database_name_alias.py +0 -0
  294. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_model_cls_refactor.py +0 -0
  295. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  296. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_sql_param_casts.py +0 -0
  297. {matrx_orm-3.1.24 → matrx_orm-3.1.26}/tests/test_sync_twins_guard.py +0 -0
@@ -32,7 +32,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
32
32
  - **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
33
33
  - **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
34
34
  - **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the DB schema. Postgres **VIEWs** are emitted as **read-only `Model` classes** into the same schema models file (on by default; per-entry `output.include_views: false` opts a schema out, `output.exclude_views: [...]` skips individual views). View models get no manager/DTO/base-class/auto_config (views are not writable), synthesize `_primary_keys` from an `id` column (else the first column, for cache-key/`load_by_id` only), and emit plain typed fields with **no `ForeignKey`** (views carry no FK metadata — nothing cross-schema to resolve). Never hand-write a view model again.
35
- - **Secrets battery** (`matrx_orm.secrets`): the ONE canonical user-credential vault — `users.user_secrets` CRUD + the platform's single Fernet primitive, keyed by `CREDENTIALS_ENCRYPTION_KEY`. Any package: `configure_secrets(db_config_name=...)` once, then `await require_user_secret(user_id, key)`. No package ever calls another service to resolve a credential. Contract: [`matrx_orm/secrets/FEATURE.md`](matrx_orm/secrets/FEATURE.md).
35
+ - **Secrets battery** (`matrx_orm.secrets_battery`): the ONE canonical user-credential vault — `users.user_secrets` CRUD + the platform's single Fernet primitive, keyed by `CREDENTIALS_ENCRYPTION_KEY`. Any package: `configure_secrets(db_config_name=...)` once, then `await require_user_secret(user_id, key)`. No package ever calls another service to resolve a credential. Contract: [`matrx_orm/secrets_battery/FEATURE.md`](matrx_orm/secrets_battery/FEATURE.md).
36
36
  - **Polymorphic associations** (`matrx_orm.associations`): `EntityRegistry` (token→Model) + `Associations` — a registry-driven, **ownership-gated** edge engine over a host's polymorphic edge table. Every `source→target` hop is org-scoped *and* the resolved target's `org_id` is re-verified (fail-closed, screams on mismatch), so a hop can't widen access past the tenant boundary. Hardcodes no table names — the host wires its own edge + token-registry models.
37
37
 
38
38
  Public API is defined explicitly in `matrx_orm/__init__.py` (100+ exports). Treat changes there as semver-breaking.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.24
3
+ Version: 3.1.26
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
@@ -2,6 +2,7 @@ from .adapters import AdapterRegistry, AsyncPostgreSQLAdapter, BaseAdapter
2
2
  from .adapters.postgrest_client_adapter import PostgRESTClientAdapter
3
3
  from .adapters.supabase_adapter import SupabaseAdapter
4
4
  from .associations import (
5
+ ANY_ROLE,
5
6
  Associations,
6
7
  CrossOrgAssociationError,
7
8
  EntityRef,
@@ -358,6 +359,7 @@ __all__ = [
358
359
  "get_model_by_table_name",
359
360
  "validate_relation_resolution",
360
361
  # ── Polymorphic associations (registry-driven, ownership-gated) ────────────
362
+ "ANY_ROLE",
361
363
  "Associations",
362
364
  "CrossOrgAssociationError",
363
365
  "EntityRef",
@@ -36,14 +36,32 @@ tested against in-memory fakes with no database.
36
36
  from __future__ import annotations
37
37
 
38
38
  from dataclasses import dataclass
39
- from typing import TYPE_CHECKING, Any, Callable
39
+ from typing import TYPE_CHECKING, Any
40
+ from collections.abc import Callable
40
41
 
41
42
  from matrx_utils import vcprint
42
43
 
43
44
  if TYPE_CHECKING:
44
45
  from matrx_orm.core.base import Model
45
46
 
46
- __all__ = ["EntityRef", "EntityRegistry", "Associations", "UnknownEntityType", "CrossOrgAssociationError"]
47
+ __all__ = [
48
+ "EntityRef",
49
+ "EntityRegistry",
50
+ "Associations",
51
+ "UnknownEntityType",
52
+ "CrossOrgAssociationError",
53
+ "ANY_ROLE",
54
+ ]
55
+
56
+
57
+ class _AnyRole:
58
+ """Sentinel: 'match every role' (distinct from ``None`` = the NULL-role edge)."""
59
+
60
+ def __repr__(self) -> str: # pragma: no cover - repr only
61
+ return "ANY_ROLE"
62
+
63
+
64
+ ANY_ROLE = _AnyRole()
47
65
 
48
66
 
49
67
  class UnknownEntityType(ValueError):
@@ -68,7 +86,10 @@ def _registry_model_lookup(schema: str, table: str) -> type[Model] | None:
68
86
  from matrx_orm.core.registry import model_registry
69
87
 
70
88
  for model in model_registry.all_models().values():
71
- if getattr(model, "_db_schema", None) == schema and getattr(model, "_table_name", None) == table:
89
+ if (
90
+ getattr(model, "_db_schema", None) == schema
91
+ and getattr(model, "_table_name", None) == table
92
+ ):
72
93
  return model
73
94
  return None
74
95
 
@@ -164,6 +185,7 @@ class Associations:
164
185
  target_type_field: str = "target_type",
165
186
  target_id_field: str = "target_id",
166
187
  org_field: str = "org_id",
188
+ role_field: str = "role",
167
189
  ) -> None:
168
190
  self._assoc = association_model
169
191
  self._reg = registry
@@ -172,6 +194,8 @@ class Associations:
172
194
  self._tt = target_type_field
173
195
  self._ti = target_id_field
174
196
  self._org = org_field
197
+ self._role = role_field
198
+ self._role_available = True # refined by _validate_field_bindings when introspectable
175
199
  self._validate_field_bindings()
176
200
 
177
201
  def _validate_field_bindings(self) -> None:
@@ -203,27 +227,52 @@ class Associations:
203
227
  f"found on the model — {detail}. Available columns: "
204
228
  f"{sorted(fields)}. Pass the correct *_field name(s) to Associations(...)."
205
229
  )
230
+ # role is OPTIONAL on the edge table — engines over role-less edge
231
+ # tables keep working; passing a role against one raises at call time.
232
+ self._role_available = self._role in fields
206
233
 
207
234
  # ---- writes ----------------------------------------------------------
208
235
 
236
+ def _role_filter(self, role: Any) -> dict[str, Any]:
237
+ """Translate a role argument into an edge filter. ``ANY_ROLE`` → no
238
+ filter; ``None`` → the NULL-role edge; a string → that role. Raises
239
+ loudly when a role is used against a role-less edge table."""
240
+ if role is ANY_ROLE:
241
+ return {}
242
+ if not self._role_available:
243
+ model_name = getattr(self._assoc, "__name__", str(self._assoc))
244
+ raise ValueError(
245
+ f"role={role!r} was passed but edge model {model_name} has no "
246
+ f"{self._role!r} column — add the column or configure role_field."
247
+ )
248
+ return {self._role: role}
249
+
209
250
  async def associate(
210
251
  self,
211
252
  source: EntityRef,
212
253
  target: EntityRef,
213
254
  *,
214
255
  org_id: str,
256
+ role: str | None = None,
215
257
  metadata: dict[str, Any] | None = None,
216
258
  created_by: str | None = None,
217
259
  verify: bool = True,
218
260
  ) -> Any:
219
261
  """Idempotently create the ``source -> target`` edge in ``org_id``.
220
262
 
263
+ **``role`` is part of edge identity** (mirroring the DB's unique index
264
+ ``(source, target, role) NULLS NOT DISTINCT``): the same endpoints with
265
+ different roles are DISTINCT edges; ``role=None`` names the NULL-role
266
+ edge. **A repeat call with ``metadata`` UPDATES the existing edge's
267
+ metadata** (full replace — the edge's metadata is the caller's document,
268
+ e.g. keyword-portfolio settings on a keyword→project link); with
269
+ ``metadata=None`` the existing edge is returned untouched.
270
+
221
271
  Validates both tokens against the registry; with ``verify=True`` (the
222
272
  default) it also loads each endpoint and confirms it exists and is in
223
273
  ``org_id`` — raising :class:`CrossOrgAssociationError` otherwise, so a
224
274
  leaky or dangling edge can never be *created* (the write-side mate of the
225
- read-side gate). Then ``get_or_create`` on the unique tuple so a repeat
226
- call is a no-op. ``org_id`` is required — the edge is the tenancy
275
+ read-side gate). ``org_id`` is required — the edge is the tenancy
227
276
  boundary. Pass ``verify=False`` only for trusted/bulk callers that already
228
277
  guarantee both endpoints. Returns the edge row.
229
278
  """
@@ -246,11 +295,21 @@ class Associations:
246
295
  self._ti: target.id,
247
296
  self._org: org_id,
248
297
  }
249
- edge, _created = await self._assoc.get_or_create(defaults=defaults, **lookup)
298
+ if self._role_available:
299
+ lookup[self._role] = role
300
+ elif role is not None:
301
+ self._role_filter(role) # raises with the loud config message
302
+ edge, created = await self._assoc.get_or_create(defaults=defaults, **lookup)
303
+ if not created and metadata is not None and getattr(edge, "metadata", None) != metadata:
304
+ edge = await edge.update(metadata=metadata)
250
305
  return edge
251
306
 
252
- async def dissociate(self, source: EntityRef, target: EntityRef, *, org_id: str) -> int:
253
- """Delete the ``source -> target`` edge(s) in ``org_id``. Returns the count removed."""
307
+ async def dissociate(
308
+ self, source: EntityRef, target: EntityRef, *, org_id: str, role: Any = ANY_ROLE
309
+ ) -> int:
310
+ """Delete the ``source -> target`` edge(s) in ``org_id``. Returns the count
311
+ removed. Default removes every role's edge between the endpoints;
312
+ ``role=None`` targets only the NULL-role edge, a string only that role."""
254
313
  if not org_id:
255
314
  raise ValueError("dissociate requires a non-empty org_id")
256
315
  edges = await self._assoc.filter_items(
@@ -260,6 +319,7 @@ class Associations:
260
319
  self._tt: target.type,
261
320
  self._ti: target.id,
262
321
  self._org: org_id,
322
+ **self._role_filter(role),
263
323
  }
264
324
  )
265
325
  for edge in edges:
@@ -269,25 +329,45 @@ class Associations:
269
329
  # ---- reads -----------------------------------------------------------
270
330
 
271
331
  async def edges_from(
272
- self, source: EntityRef, *, org_id: str, target_type: str | None = None
332
+ self,
333
+ source: EntityRef,
334
+ *,
335
+ org_id: str,
336
+ target_type: str | None = None,
337
+ role: Any = ANY_ROLE,
273
338
  ) -> list[Any]:
274
- """Raw edge rows where ``source`` is the source, in ``org_id`` (optionally one target type)."""
339
+ """Raw edge rows where ``source`` is the source, in ``org_id`` (optionally
340
+ one target type and/or one role — ``role=None`` = the NULL-role edges)."""
275
341
  filters: dict[str, Any] = {self._st: source.type, self._si: source.id, self._org: org_id}
276
342
  if target_type is not None:
277
343
  filters[self._tt] = target_type
344
+ filters.update(self._role_filter(role))
278
345
  return await self._assoc.filter_items(**filters)
279
346
 
280
347
  async def edges_to(
281
- self, target: EntityRef, *, org_id: str, source_type: str | None = None
348
+ self,
349
+ target: EntityRef,
350
+ *,
351
+ org_id: str,
352
+ source_type: str | None = None,
353
+ role: Any = ANY_ROLE,
282
354
  ) -> list[Any]:
283
- """Raw edge rows where ``target`` is the target, in ``org_id`` (optionally one source type)."""
355
+ """Raw edge rows where ``target`` is the target, in ``org_id`` (optionally
356
+ one source type and/or one role — ``role=None`` = the NULL-role edges)."""
284
357
  filters: dict[str, Any] = {self._tt: target.type, self._ti: target.id, self._org: org_id}
285
358
  if source_type is not None:
286
359
  filters[self._st] = source_type
360
+ filters.update(self._role_filter(role))
287
361
  return await self._assoc.filter_items(**filters)
288
362
 
289
363
  async def targets_from(
290
- self, source: EntityRef, target_type: str, *, org_id: str, load: bool = True
364
+ self,
365
+ source: EntityRef,
366
+ target_type: str,
367
+ *,
368
+ org_id: str,
369
+ load: bool = True,
370
+ role: Any = ANY_ROLE,
291
371
  ) -> list[Any]:
292
372
  """Resolve the ``target_type`` entities ``source`` points to — ownership-gated.
293
373
 
@@ -297,17 +377,23 @@ class Associations:
297
377
  With ``load=False`` only the :class:`EntityRef` pointers are returned (still
298
378
  org-scoped by the edge query, but NOT row-verified).
299
379
  """
300
- edges = await self.edges_from(source, org_id=org_id, target_type=target_type)
380
+ edges = await self.edges_from(source, org_id=org_id, target_type=target_type, role=role)
301
381
  ids = [getattr(e, self._ti) for e in edges]
302
382
  if not load:
303
383
  return [EntityRef(target_type, i) for i in ids]
304
384
  return await self._load_gated(target_type, ids, org_id)
305
385
 
306
386
  async def sources_to(
307
- self, target: EntityRef, source_type: str, *, org_id: str, load: bool = True
387
+ self,
388
+ target: EntityRef,
389
+ source_type: str,
390
+ *,
391
+ org_id: str,
392
+ load: bool = True,
393
+ role: Any = ANY_ROLE,
308
394
  ) -> list[Any]:
309
395
  """Reverse of :meth:`targets_from` — the ``source_type`` entities pointing at ``target``."""
310
- edges = await self.edges_to(target, org_id=org_id, source_type=source_type)
396
+ edges = await self.edges_to(target, org_id=org_id, source_type=source_type, role=role)
311
397
  ids = [getattr(e, self._si) for e in edges]
312
398
  if not load:
313
399
  return [EntityRef(source_type, i) for i in ids]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from dataclasses import dataclass
4
5
  import json
5
6
  import logging
6
7
  import os
@@ -285,6 +286,15 @@ _init_connection = _init_connection_default
285
286
  _init_vector_codec = _init_connection_default
286
287
 
287
288
 
289
+ @dataclass(frozen=True, slots=True)
290
+ class ConnectionIdentity:
291
+ """The role/database a pool is actually connected as."""
292
+
293
+ current_user: str
294
+ session_user: str
295
+ database: str
296
+
297
+
288
298
  class AsyncDatabaseManager:
289
299
  _instance = None
290
300
  _pools = {}
@@ -496,6 +506,30 @@ class AsyncDatabaseManager:
496
506
  ) from e
497
507
  return cls._pools[config_name]
498
508
 
509
+ @classmethod
510
+ async def connection_identity(cls, config_name: str) -> ConnectionIdentity:
511
+ """Who is this pool actually connected AS.
512
+
513
+ Exists so a service can PROVE it is inside its granted database
514
+ boundary (`current_user` = its scoped role) without hand-writing SQL —
515
+ the question "which role am I?" is database introspection, which the
516
+ ORM owns. Postgres exposes `current_user` / `session_user` /
517
+ `current_database` as reserved keywords rather than callable
518
+ functions, so `call_function` cannot reach them; this is the sanctioned
519
+ accessor.
520
+ """
521
+ pool = await cls.get_pool(config_name)
522
+ async with pool.acquire() as conn:
523
+ row = await conn.fetchrow(
524
+ "SELECT current_user AS current_user, session_user AS session_user, "
525
+ "current_database() AS database"
526
+ )
527
+ return ConnectionIdentity(
528
+ current_user=row["current_user"],
529
+ session_user=row["session_user"],
530
+ database=row["database"],
531
+ )
532
+
499
533
  @classmethod
500
534
  def _start_keepalive(cls, config_name: str, pool_min: int) -> None:
501
535
  """Start (or restart) the per-config pool keepalive task.
@@ -0,0 +1,112 @@
1
+ # matrx-orm secrets battery — the ONE credential vault
2
+
3
+ **One table, one key, everywhere.** Every platform credential — personal AND
4
+ organizational — lives in `users.user_secrets` (Fernet-encrypted). Every package
5
+ and deployment that needs a credential holds the same `CREDENTIALS_ENCRYPTION_KEY`
6
+ and reads the vault directly through this module. **No package calls another
7
+ package or a service to resolve a credential** — there is no privileged
8
+ decryptor (decided 2026-07-21; doctrine: `docs/packages/PACKAGE_DOCTRINE.md` §5).
9
+
10
+ There is no second vault. The `private_vault.organization_secrets` system
11
+ (Supabase Vault, decryption inside Postgres) was **annihilated 2026-07-21** —
12
+ schema, tables, SECURITY DEFINER functions, and the duplicated `vault.secrets`
13
+ material all dropped. A new `Fernet(...)` or a second secrets table anywhere is
14
+ a defect.
15
+
16
+ ## Consumer contract
17
+
18
+ ```python
19
+ from matrx_orm.secrets_battery import require_secret
20
+ api_key = await require_secret(user_id, organization_id, "DATAFORSEO_API_KEY")
21
+ ```
22
+
23
+ **`require_secret` is the one you want.** It implements the hierarchy:
24
+ **personal → organization → raise**, naming both scopes it searched. Most people
25
+ never hold their own API keys — their organization does.
26
+
27
+ `require_user_secret(user_id, key)` is the personal-only variant. Both raise
28
+ `SecretNotFoundError` / `SecretDecryptError` and never return None;
29
+ `get_user_secret` / `get_org_secret` are the degradable twins.
30
+
31
+ ## Scope — one table, two shapes
32
+
33
+ A row is PERSONAL (`user_id` set, `organization_id` NULL) or ORG-SCOPED
34
+ (`organization_id` set, `user_id` NULL). A DB CHECK enforces exactly one, and
35
+ partial unique indexes make `(scope, key)` unique among live rows.
36
+
37
+ | Surface | Personal | Org |
38
+ |---|---|---|
39
+ | read one | `get_user_secret` | `get_org_secret` |
40
+ | read many | — | `resolve_org_secrets` |
41
+ | hierarchy (one key) | `require_secret` | ← same call |
42
+ | hierarchy (many keys) | `resolve_effective_secrets` | ← same call |
43
+ | CRUD | `create/update/delete_user_secret` | `create/update/delete_org_secret` |
44
+ | listing | `list_user_secrets` | `list_org_secrets` |
45
+ | grants | — | `set_org_secret_grants` |
46
+
47
+ **Precedence is the same in both directions:** a PERSONAL value wins over an org
48
+ value on key collision. `require_secret` checks personal first;
49
+ `resolve_effective_secrets` loads org values then overlays personal on top. They
50
+ are two expressions of one rule — keep them that way.
51
+
52
+ ## Grants and access modes
53
+
54
+ An org secret is `access_mode='all_members'` (default) or `'restricted'`.
55
+ A restricted secret is readable only by a user holding a `can_use` row in
56
+ `users.user_secret_grants` — **or by an org admin**, via the `is_org_admin`
57
+ flag the host computes and passes down.
58
+
59
+ ## The authorization boundary — read this before adding a check here
60
+
61
+ **matrx-orm has no tenancy model and must never grow one.** This module does NOT
62
+ decide who is a member or an admin of an organization. The HOST asserts that and
63
+ passes the outcome in as `is_org_admin`. What the battery DOES own is the
64
+ per-secret grant check, because `user_secret_grants` is its own table.
65
+
66
+ aidream's authorization layer is
67
+ [`aidream/services/user_secrets/organizations.py`](../../../../aidream/services/user_secrets/organizations.py):
68
+ membership → `iam.has_org_access_for`, adminhood → `public.is_org_admin_for`,
69
+ asserted before the battery is touched.
70
+
71
+ ## Audit
72
+
73
+ `users.user_secret_audit` records **actions and metadata, NEVER values**
74
+ (`record_secret_audit`). Audit is a trail, not a transaction: a failed audit
75
+ write is logged loudly but never fails the operation the user asked for —
76
+ losing the trail must not also lose the work.
77
+
78
+ ## Wiring
79
+
80
+ - **Hosted:** `configure_secrets(db_config_name="<registered config>")` at
81
+ startup (aidream: `package_integration.py`) — aliases `matrx_secrets` onto the
82
+ host pool. One physical pool.
83
+ - **Standalone package/service:** register your DB with matrx-orm, call the same
84
+ `configure_secrets`. Key comes from `CREDENTIALS_ENCRYPTION_KEY` env, or pass
85
+ `encryption_key=` (host injection).
86
+ - Key resolution (byte-identical to the historical aidream implementation — all
87
+ existing ciphertexts keep decrypting): explicit key → `CREDENTIALS_ENCRYPTION_KEY`
88
+ (44-char urlsafe-base64 used directly, else SHA-256-derived) → `SUPABASE_SECRET_KEY`
89
+ derivation with a loud warning → loud RuntimeError.
90
+
91
+ ## Invariants
92
+
93
+ - **This is the only Fernet instance in the universe.** The dict-payload helpers
94
+ (`encrypt_payload`/`decrypt_payload`) serve `tool.mcp_user_conn` blobs — same
95
+ key, same instance.
96
+ - Every read is scoped by a **required** `user_id` or `organization_id`. No None
97
+ = "show everything".
98
+ - Soft delete by default (`deleted_at`); the partial unique indexes back the
99
+ upserts.
100
+ - `value_hint` is the only value-derived field that may reach a listing/UI.
101
+ Listing and management surfaces are value-free by construction.
102
+ - `value_version` is bumped by the `users.user_secrets_value_version` DB trigger
103
+ when `value_encrypted` changes — never by hand.
104
+ - Rotating an org secret's value CLEARS its personal-secret lineage
105
+ (`source_user_secret_id`) — the org value is no longer what the contributor holds.
106
+ - One unreadable row (key drift) is skipped with a loud log during bulk
107
+ resolution — a broken row must not deny every other key.
108
+
109
+ Drift guard: `scripts/validate_secrets_decrypt.py` (loud, non-blocking, in
110
+ `release.sh`) asserts every active row still decrypts with the configured key.
111
+
112
+ Verified against live code + the live `users.user_secrets` schema 2026-07-21.
@@ -0,0 +1,136 @@
1
+ """matrx-orm secrets battery — the ONE canonical user-credential vault.
2
+
3
+ One shared database table (``users.user_secrets``), one encryption key
4
+ (``CREDENTIALS_ENCRYPTION_KEY``), held by every package/deployment that needs
5
+ credentials. No package ever calls another package (or a service) to resolve
6
+ a credential — it reads and decrypts directly through this module.
7
+
8
+ Consumer contract (the whole thing):
9
+
10
+ from matrx_orm.secrets_battery import require_user_secret
11
+ api_key = await require_user_secret(user_id, "DATAFORSEO_API_KEY")
12
+
13
+ Wiring:
14
+
15
+ * **Hosted (inside aidream or any app with a registered pool):**
16
+ ``configure_secrets(db_config_name="<registered config>")`` — registers a
17
+ name alias; one physical pool, no second connection.
18
+ * **Standalone package:** register your database with matrx-orm as usual,
19
+ then ``configure_secrets(db_config_name=...)``. The key comes from the
20
+ ``CREDENTIALS_ENCRYPTION_KEY`` env var, or pass ``encryption_key=`` here.
21
+
22
+ Contract details: ``FEATURE.md`` beside this module.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from .crypto import (
28
+ configure_encryption,
29
+ decrypt_payload,
30
+ decrypt_value,
31
+ encrypt_payload,
32
+ encrypt_value,
33
+ get_fernet,
34
+ mask_value,
35
+ reset_encryption_for_tests,
36
+ )
37
+ from .model import PACKAGE_DB_NAME, UserSecret, UserSecretAudit, UserSecretGrant
38
+ from .org_service import (
39
+ ACCESS_MODES,
40
+ OrgSecretRow,
41
+ create_org_secret,
42
+ delete_org_secret,
43
+ get_org_secret_row,
44
+ list_org_secrets,
45
+ record_secret_audit,
46
+ resolve_effective_secrets,
47
+ resolve_org_secrets,
48
+ set_org_secret_grants,
49
+ sync_org_secret_from_user,
50
+ update_org_secret,
51
+ )
52
+ from .service import (
53
+ KNOWN_CATEGORIES,
54
+ VALID_KEY_RE,
55
+ SecretDecryptError,
56
+ SecretNotFoundError,
57
+ SecretRow,
58
+ bulk_upsert_from_env_text,
59
+ create_user_secret,
60
+ delete_user_secret,
61
+ get_user_secret,
62
+ list_user_secrets,
63
+ parse_env_text,
64
+ get_org_secret,
65
+ require_secret,
66
+ require_user_secret,
67
+ update_user_secret,
68
+ )
69
+
70
+
71
+ def configure_secrets(
72
+ *,
73
+ db_config_name: str | None = None,
74
+ encryption_key: str | None = None,
75
+ ) -> None:
76
+ """Bind the battery. Idempotent; call once at startup.
77
+
78
+ ``db_config_name`` — an already-registered matrx-orm config to alias
79
+ ``matrx_secrets`` onto. ``encryption_key`` — explicit key injection for
80
+ hosts that prefer passing it over the env var."""
81
+ if encryption_key is not None:
82
+ configure_encryption(encryption_key)
83
+ if db_config_name is not None and db_config_name != PACKAGE_DB_NAME:
84
+ from matrx_orm import is_database_registered, register_database_alias
85
+
86
+ if not is_database_registered(db_config_name):
87
+ raise RuntimeError(
88
+ f"configure_secrets: db config {db_config_name!r} is not registered — "
89
+ "register the database with matrx-orm first"
90
+ )
91
+ if not is_database_registered(PACKAGE_DB_NAME):
92
+ register_database_alias(PACKAGE_DB_NAME, db_config_name)
93
+
94
+
95
+ __all__ = [
96
+ "configure_secrets",
97
+ "configure_encryption",
98
+ "reset_encryption_for_tests",
99
+ "get_fernet",
100
+ "encrypt_value",
101
+ "decrypt_value",
102
+ "encrypt_payload",
103
+ "decrypt_payload",
104
+ "mask_value",
105
+ "UserSecret",
106
+ "UserSecretGrant",
107
+ "UserSecretAudit",
108
+ "PACKAGE_DB_NAME",
109
+ "ACCESS_MODES",
110
+ "OrgSecretRow",
111
+ "create_org_secret",
112
+ "delete_org_secret",
113
+ "get_org_secret_row",
114
+ "list_org_secrets",
115
+ "record_secret_audit",
116
+ "resolve_effective_secrets",
117
+ "resolve_org_secrets",
118
+ "set_org_secret_grants",
119
+ "sync_org_secret_from_user",
120
+ "update_org_secret",
121
+ "SecretRow",
122
+ "SecretNotFoundError",
123
+ "SecretDecryptError",
124
+ "KNOWN_CATEGORIES",
125
+ "VALID_KEY_RE",
126
+ "list_user_secrets",
127
+ "get_user_secret",
128
+ "require_secret",
129
+ "get_org_secret",
130
+ "require_user_secret",
131
+ "create_user_secret",
132
+ "update_user_secret",
133
+ "delete_user_secret",
134
+ "parse_env_text",
135
+ "bulk_upsert_from_env_text",
136
+ ]