matrx-orm 3.1.183__tar.gz → 3.1.185__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 (415) hide show
  1. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/PKG-INFO +2 -2
  2. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/backup_restore.py +143 -1
  3. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/items.py +141 -40
  4. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/vault_history.py +25 -2
  5. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/pyproject.toml +2 -2
  6. matrx_orm-3.1.185/tests/test_backup_history_restore.py +144 -0
  7. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_backup_restore_live.py +28 -0
  8. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_secrets_battery.py +129 -0
  9. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/.gitignore +0 -0
  10. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/AGENTS.md +0 -0
  11. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/BASE_CLASS_METHODS.md +0 -0
  12. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/CHANGELOG.md +0 -0
  13. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/CLAUDE.md +0 -0
  14. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/MODEL_API.md +0 -0
  15. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/README.md +0 -0
  16. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/database/orm/extended/managers/ai_model_base.py +0 -0
  17. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/docs/migrations.md +0 -0
  18. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/FEATURE.md +0 -0
  19. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/MODULE_README.md +0 -0
  20. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/__init__.py +0 -0
  21. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  26. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/adapters/supabase_adapter.py +0 -0
  27. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/admin/FEATURE.md +0 -0
  28. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/admin/router.py +0 -0
  30. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/catalog.py +0 -0
  39. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/catalog_sql.py +0 -0
  40. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/MODULE_README.md +0 -0
  41. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/__init__.py +0 -0
  42. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/postgres_connection.py +0 -0
  43. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/postgrest.py +0 -0
  44. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/supabase_auth.py +0 -0
  45. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/supabase_config.py +0 -0
  46. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/client/supabase_manager.py +0 -0
  47. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  48. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/FEATURE.md +0 -0
  49. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/MODULE_README.md +0 -0
  50. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  51. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/TASKS.md +0 -0
  52. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/__init__.py +0 -0
  53. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/admin_lane.py +0 -0
  54. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/async_db_manager.py +0 -0
  55. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/base.py +0 -0
  56. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/config.py +0 -0
  57. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/connections.py +0 -0
  58. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/db_function.py +0 -0
  59. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/diagnostics.py +0 -0
  60. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/expressions.py +0 -0
  61. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/extended.py +0 -0
  62. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/fields.py +0 -0
  63. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/introspection.py +0 -0
  64. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/listen.py +0 -0
  65. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/loop_filters.py +0 -0
  66. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/model_dto.py +0 -0
  67. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/model_view.py +0 -0
  68. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/one_database.py +0 -0
  69. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/paginator.py +0 -0
  70. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/pool_maintenance.py +0 -0
  71. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/pool_watch.py +0 -0
  72. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/pooler_role.py +0 -0
  73. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/pydantic_bridge.py +0 -0
  74. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/registry.py +0 -0
  75. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/relations.py +0 -0
  76. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/resilience.py +0 -0
  77. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/rls_session.py +0 -0
  78. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/session_context.py +0 -0
  79. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/signals.py +0 -0
  80. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/tls.py +0 -0
  81. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/transaction.py +0 -0
  82. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/types.py +0 -0
  83. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/write_queue.py +0 -0
  84. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/core/write_retry.py +0 -0
  85. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/entity.py +0 -0
  86. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/error_handling.py +0 -0
  87. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/exceptions.py +0 -0
  88. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/extended/__init__.py +0 -0
  89. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/extended/app_error_handler.py +0 -0
  90. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/identity_rule.py +0 -0
  91. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/local_sqlite.py +0 -0
  92. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/middleware/__init__.py +0 -0
  93. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/middleware/base.py +0 -0
  94. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/MODULE_README.md +0 -0
  95. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/__init__.py +0 -0
  96. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/cli.py +0 -0
  97. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/ddl.py +0 -0
  98. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/diff.py +0 -0
  99. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/executor.py +0 -0
  100. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/integration.py +0 -0
  101. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/loader.py +0 -0
  102. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/operations.py +0 -0
  103. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/state.py +0 -0
  104. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/migrations/table_filter.py +0 -0
  105. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/no_org_assignment.py +0 -0
  106. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/FEATURE.md +0 -0
  107. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/MODULE_README.md +0 -0
  108. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/__init__.py +0 -0
  109. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/bulk_update_values.py +0 -0
  110. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/catalog_select.py +0 -0
  111. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/conflict.py +0 -0
  112. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/conflict_writes.py +0 -0
  113. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/create.py +0 -0
  114. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/db_functions.py +0 -0
  115. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/delete.py +0 -0
  116. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/dynamic_admin.py +0 -0
  117. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/dynamic_crud.py +0 -0
  118. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/graph_walk.py +0 -0
  119. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/latest_rows.py +0 -0
  120. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/queue_claim.py +0 -0
  121. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/read.py +0 -0
  122. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/relational_backfill.py +0 -0
  123. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/staging_load.py +0 -0
  124. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/operations/update.py +0 -0
  125. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/platform_access.py +0 -0
  126. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/platform_db.py +0 -0
  127. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/py.typed +0 -0
  128. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/pytest_live_db_guard.py +0 -0
  129. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/pytest_lock_guard.py +0 -0
  130. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/pytest_plugin.py +0 -0
  131. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/python_sql/MODULE_README.md +0 -0
  132. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/python_sql/__init__.py +0 -0
  133. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/python_sql/db_objects.py +0 -0
  134. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  135. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  136. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/query/FEATURE.md +0 -0
  137. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/query/MODULE_README.md +0 -0
  138. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/query/__init__.py +0 -0
  139. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/query/builder.py +0 -0
  140. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/query/executor.py +0 -0
  141. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/resources/__init__.py +0 -0
  142. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
  143. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
  144. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/FEATURE.md +0 -0
  145. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  146. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/__init__.py +0 -0
  147. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/code_handler.py +0 -0
  148. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  149. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/columns.py +0 -0
  150. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/common.py +0 -0
  151. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/diff_preview.py +0 -0
  152. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  153. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/generator.py +0 -0
  154. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  155. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  156. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  157. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  158. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/package_wiring.py +0 -0
  159. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/relationships.py +0 -0
  160. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/runner.py +0 -0
  161. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/schema.py +0 -0
  162. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/schema_manager.py +0 -0
  163. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/tables.py +0 -0
  164. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/schema_builder/views.py +0 -0
  165. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  166. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/__init__.py +0 -0
  167. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/attachments.py +0 -0
  168. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/crypto.py +0 -0
  169. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/host_authorization.py +0 -0
  170. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/model.py +0 -0
  171. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
  172. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/native_enable.py +0 -0
  173. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/native_lifecycle.py +0 -0
  174. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/native_passkeys.py +0 -0
  175. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/native_source.py +0 -0
  176. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/org_service.py +0 -0
  177. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/secrets_battery/service.py +0 -0
  178. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/README.md +0 -0
  179. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/__init__.py +0 -0
  180. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/coalesce.py +0 -0
  181. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/dag.py +0 -0
  182. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/errors.py +0 -0
  183. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/fallback.py +0 -0
  184. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/flush.py +0 -0
  185. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/lifecycle.py +0 -0
  186. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/managed.py +0 -0
  187. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/op.py +0 -0
  188. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/reads.py +0 -0
  189. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/session.py +0 -0
  190. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/session/telemetry.py +0 -0
  191. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  192. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/__init__.py +0 -0
  193. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/executor.py +0 -0
  194. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/queries.py +0 -0
  195. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/registry.py +0 -0
  196. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/types.py +0 -0
  197. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/sql_executor/utils.py +0 -0
  198. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/state.py +0 -0
  199. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/utils/__init__.py +0 -0
  200. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/utils/sql_utils.py +0 -0
  201. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/utils/type_converters.py +0 -0
  202. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/matrx_orm/write_actor.py +0 -0
  203. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/scripts/git-branches.sh +0 -0
  204. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/scripts/publish.sh +0 -0
  205. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/scripts/release.sh +0 -0
  206. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/MODULE_README.md +0 -0
  207. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/__init__.py +0 -0
  208. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/conftest.py +0 -0
  209. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/MODULE_README.md +0 -0
  210. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/__init__.py +0 -0
  211. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_adapter_registry.py +0 -0
  212. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_admin_db_columns.py +0 -0
  213. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_admin_response_rendering.py +0 -0
  214. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_admin_search_negation.py +0 -0
  215. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_admin_search_sentinels.py +0 -0
  216. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_advisory_lock.py +0 -0
  217. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_agent_message.py +0 -0
  218. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_api_auth.py +0 -0
  219. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_api_config.py +0 -0
  220. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_api_handlers.py +0 -0
  221. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_api_protocol.py +0 -0
  222. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_array_agg_order_by.py +0 -0
  223. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_association_trigger_actor_naming.py +0 -0
  224. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_associations.py +0 -0
  225. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_associations_wiring.py +0 -0
  226. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_bulk_hydration_offload.py +0 -0
  227. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_bulk_update_by_pk.py +0 -0
  228. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  229. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_cache_debug.py +0 -0
  230. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_call_function.py +0 -0
  231. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_case_when.py +0 -0
  232. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_catalog.py +0 -0
  233. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_committed_row_guard.py +0 -0
  234. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_composite_pk_filter.py +0 -0
  235. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_composite_pk_write_paths.py +0 -0
  236. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_config.py +0 -0
  237. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_conflict_target.py +0 -0
  238. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_conflict_writes.py +0 -0
  239. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_connection_codecs.py +0 -0
  240. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_connection_poison_guard.py +0 -0
  241. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_connection_primitives.py +0 -0
  242. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_count_composite_distinct.py +0 -0
  243. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_count_limit_param_leak.py +0 -0
  244. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_db_function_field.py +0 -0
  245. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_ddl_generator.py +0 -0
  246. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_deadlock_retry.py +0 -0
  247. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_derived_table.py +0 -0
  248. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_dirty_tracking.py +0 -0
  249. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_dynamic_crud.py +0 -0
  250. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_exceptions.py +0 -0
  251. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  252. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_expression_primitives.py +0 -0
  253. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_fields.py +0 -0
  254. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  255. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_fk_cycle_resolution.py +0 -0
  256. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_from_alias.py +0 -0
  257. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_fts.py +0 -0
  258. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_func_expression_args.py +0 -0
  259. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_graph_walk.py +0 -0
  260. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_having_annotate_alias.py +0 -0
  261. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  262. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_introspect_rls_policies.py +0 -0
  263. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_join_and_aggregate.py +0 -0
  264. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_latest_rows.py +0 -0
  265. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_listen_notify.py +0 -0
  266. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_local_sqlite.py +0 -0
  267. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_lock_contention_retry.py +0 -0
  268. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_managed_write_guard.py +0 -0
  269. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_matrx_entity.py +0 -0
  270. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_migration_diff_types.py +0 -0
  271. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_migration_loader.py +0 -0
  272. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_model_instance.py +0 -0
  273. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_model_meta.py +0 -0
  274. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_optimistic_concurrency.py +0 -0
  275. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_order_by_expression.py +0 -0
  276. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_org_assignment_inventory.py +0 -0
  277. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_orm_banners_are_client_safe.py +0 -0
  278. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_parameter_name_hydration.py +0 -0
  279. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_patch_jsonb_path.py +0 -0
  280. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_percentile_cont.py +0 -0
  281. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_persisted_count.py +0 -0
  282. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_pool_create_never_wedges.py +0 -0
  283. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_pool_holder_census.py +0 -0
  284. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_pool_loop_recreation.py +0 -0
  285. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_pool_maintenance.py +0 -0
  286. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_postgrest_filters.py +0 -0
  287. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_pydantic_bridge.py +0 -0
  288. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_query_builder.py +0 -0
  289. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_query_error_detail.py +0 -0
  290. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_query_executor_sql.py +0 -0
  291. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_query_timeout.py +0 -0
  292. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  293. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_queue_claim.py +0 -0
  294. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_read_connection_retry.py +0 -0
  295. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_registry.py +0 -0
  296. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_registry_multi_database.py +0 -0
  297. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_registry_multi_schema.py +0 -0
  298. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_relational_backfill.py +0 -0
  299. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_relations.py +0 -0
  300. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_resilience.py +0 -0
  301. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_rls_session_scope.py +0 -0
  302. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_schema_exists.py +0 -0
  303. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_session_advisory_lock.py +0 -0
  304. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_standalone_write_session_context.py +0 -0
  305. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_state_cache.py +0 -0
  306. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_subquery_filter_raw.py +0 -0
  307. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_subquery_in_filter.py +0 -0
  308. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_subquery_nested_expressions.py +0 -0
  309. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_supabase_auth.py +0 -0
  310. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_supabase_config.py +0 -0
  311. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_tls.py +0 -0
  312. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_transaction_database_identity.py +0 -0
  313. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_typed_join_projection.py +0 -0
  314. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_typed_predicates.py +0 -0
  315. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_update_case_expression.py +0 -0
  316. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_update_subquery.py +0 -0
  317. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_update_with_rebase.py +0 -0
  318. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_upsert_default_leak.py +0 -0
  319. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_upsert_with_conflict.py +0 -0
  320. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_values_jsonb_decode.py +0 -0
  321. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_watched_lifecycle.py +0 -0
  322. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_write_queue_deadline.py +0 -0
  323. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level1/test_write_retry_semantics.py +0 -0
  324. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/MODULE_README.md +0 -0
  325. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/__init__.py +0 -0
  326. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/conftest.py +0 -0
  327. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_bulk_ops.py +0 -0
  328. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_cache_integration.py +0 -0
  329. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_crud.py +0 -0
  330. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_foreign_keys.py +0 -0
  331. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_m2m.py +0 -0
  332. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_manager.py +0 -0
  333. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_migrations_live.py +0 -0
  334. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_query_execution.py +0 -0
  335. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/test_schema_diff.py +0 -0
  336. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
  337. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/live_pooler_role.py +0 -0
  338. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/native_source_cases.py +0 -0
  339. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/native_source_oracle.rs +0 -0
  340. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/.env.example +0 -0
  341. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/README.md +0 -0
  342. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/__init__.py +0 -0
  343. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/generate.py +0 -0
  344. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/generated/.gitkeep +0 -0
  345. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/matrx_orm.yaml +0 -0
  346. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project/test_schema_generation.py +0 -0
  347. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/.env.example +0 -0
  348. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/README.md +0 -0
  349. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/__init__.py +0 -0
  350. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/client_example.py +0 -0
  351. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/client_example.ts +0 -0
  352. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  353. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/sample_project_desktop/server.py +0 -0
  354. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/entity_tests.py +0 -0
  355. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_base_generation.py +0 -0
  356. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  357. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_composite_unique_generation.py +0 -0
  358. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_dto_identity_generation.py +0 -0
  359. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_entity_capability_generation.py +0 -0
  360. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_enum_ordering.py +0 -0
  361. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_generate_schema.py +0 -0
  362. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_junction_analysis.py +0 -0
  363. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_multi_schema_output.py +0 -0
  364. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_schema_analysis.py +0 -0
  365. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_sql_expression_defaults.py +0 -0
  366. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_table_relationships_query.py +0 -0
  367. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema/test_view_generation.py +0 -0
  368. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema_builder/test_array_default_parsing.py +0 -0
  369. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  370. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  371. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/__init__.py +0 -0
  372. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/conftest.py +0 -0
  373. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_a_failed_capture_still_names_the_failure.py +0 -0
  374. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_cache_no_downgrade.py +0 -0
  375. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_capture_poison_proof.py +0 -0
  376. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_coalesce.py +0 -0
  377. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_dag.py +0 -0
  378. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_disk_spill_fallback.py +0 -0
  379. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_flush_individual_fallback.py +0 -0
  380. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_governed_write.py +0 -0
  381. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_live_db_guard_plugin.py +0 -0
  382. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_managed.py +0 -0
  383. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_op_enqueue_site.py +0 -0
  384. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_reads.py +0 -0
  385. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_record_error_bounded_insert.py +0 -0
  386. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_record_error_storm.py +0 -0
  387. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_session.py +0 -0
  388. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/session/test_spill_guard_plugin.py +0 -0
  389. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_array_arg_element_type.py +0 -0
  390. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_attachment_mutation_serialization.py +0 -0
  391. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_backup_alias_conflict.py +0 -0
  392. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_composite_fk_is_not_a_column_fk.py +0 -0
  393. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_database_name_alias.py +0 -0
  394. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_db_functions.py +0 -0
  395. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_dsn_password_masking.py +0 -0
  396. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_dynamic_select_unregistered.py +0 -0
  397. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_immutable_insert_or_exact_verify.py +0 -0
  398. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_keyed_digest.py +0 -0
  399. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_model_cls_refactor.py +0 -0
  400. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_native_enable.py +0 -0
  401. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_native_lifecycle.py +0 -0
  402. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_native_lifecycle_fences.py +0 -0
  403. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_native_passkey_receipts.py +0 -0
  404. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_native_source.py +0 -0
  405. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  406. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_one_database.py +0 -0
  407. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_order_by_desc_nulls_last.py +0 -0
  408. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_pooler_role_guard.py +0 -0
  409. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_secret_audit_names_its_organization.py +0 -0
  410. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_secrets_minting_keys.py +0 -0
  411. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_shared_lock_guard.py +0 -0
  412. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_sql_param_casts.py +0 -0
  413. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_sync_twins_guard.py +0 -0
  414. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_user_visible_lookup.py +0 -0
  415. {matrx_orm-3.1.183 → matrx_orm-3.1.185}/tests/test_vault_aggregate_recovery_stateful.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.183
3
+ Version: 3.1.185
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
@@ -26,7 +26,7 @@ Requires-Dist: asyncpg>=0.31.0
26
26
  Requires-Dist: cbor2<7,>=6.1.3
27
27
  Requires-Dist: cryptography>=43.0
28
28
  Requires-Dist: gitpython>=3.1.40
29
- Requires-Dist: matrx-utils>=2.0.54
29
+ Requires-Dist: matrx-utils>=2.0.55
30
30
  Requires-Dist: psycopg-pool>=3.2.5
31
31
  Requires-Dist: psycopg[binary]>=3.2.5
32
32
  Requires-Dist: python-dotenv>=1.0
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import AsyncIterator
6
6
  from contextlib import asynccontextmanager
7
7
  from dataclasses import dataclass
8
+ from datetime import UTC, datetime
8
9
  from typing import Literal
9
10
  from uuid import UUID
10
11
 
@@ -34,6 +35,11 @@ from .mutation_receipts import (
34
35
  reserve_backup_restore_receipt,
35
36
  )
36
37
  from .org_service import record_secret_audit
38
+ from .vault_history import (
39
+ locked_history_revision,
40
+ password_field_update_snapshot,
41
+ write_password_field_update_snapshot,
42
+ )
37
43
 
38
44
 
39
45
  @dataclass(frozen=True, repr=False)
@@ -58,6 +64,15 @@ class BackupRestoreAliasConflictError(Exception):
58
64
  """A restored alias is reserved; the entire record was rolled back."""
59
65
 
60
66
 
67
+ @dataclass(frozen=True, repr=False)
68
+ class BackupHistoryRevisionSpec:
69
+ """One portable password state; source ciphertext never enters this boundary."""
70
+
71
+ revision: int
72
+ value: str
73
+ source_recorded_at: datetime | None = None
74
+
75
+
61
76
  @asynccontextmanager
62
77
  async def _restore_transaction() -> AsyncIterator[None]:
63
78
  try:
@@ -91,6 +106,7 @@ async def create_backup_record_quarantined(
91
106
  non_secret_fields: list[dict[str, str]],
92
107
  fields: list[FieldSpec],
93
108
  attachments: list[BackupAttachmentSpec],
109
+ history: list[BackupHistoryRevisionSpec] | None = None,
94
110
  ) -> BackupRestoreResult:
95
111
  """Restore one host-authorized personal aggregate and receipt in one transaction."""
96
112
  # The host supplies verified actor/context and computes the binding fingerprint.
@@ -114,6 +130,7 @@ async def create_backup_record_quarantined(
114
130
  # Validation is shared, but restoration must not silently normalize source text.
115
131
  metadata["login_urls"] = login_urls
116
132
  seen: set[str] = set()
133
+ password_fields: list[FieldSpec] = []
117
134
  for field in fields:
118
135
  _validate_field_key(field.field_key)
119
136
  _refuse_reserved_generic_field(field.field_key)
@@ -123,6 +140,8 @@ async def create_backup_record_quarantined(
123
140
  raise ValueError("backup field is not portable")
124
141
  if field.inject_into_sandbox:
125
142
  raise ValueError("backup cannot enable sandbox injection")
143
+ if field.field_key == "password":
144
+ password_fields.append(field)
126
145
  seen.add(field.field_key)
127
146
  for attachment in attachments:
128
147
  if attachment.source_handling not in {"visible", "revealable"}:
@@ -133,6 +152,7 @@ async def create_backup_record_quarantined(
133
152
  _validate_text(attachment.media_type, name="attachment media type", maximum=255)
134
153
  if attachment.description is not None and len(attachment.description) > 2000:
135
154
  raise ValueError("attachment description is too long")
155
+ history = _validate_history(history, password_fields)
136
156
  except (TypeError, ValueError, AttributeError):
137
157
  raise ValueError("Invalid backup restore record.") from None
138
158
 
@@ -165,8 +185,9 @@ async def create_backup_record_quarantined(
165
185
  created_by=actor_id,
166
186
  updated_by=actor_id,
167
187
  )
188
+ password_row: UserSecret | None = None
168
189
  for field in fields:
169
- await UserSecret.create(
190
+ created = await UserSecret.create(
170
191
  user_id=actor_id,
171
192
  organization_id=None,
172
193
  credential_item_id=item.id,
@@ -184,6 +205,22 @@ async def create_backup_record_quarantined(
184
205
  created_by=actor_id,
185
206
  updated_by=actor_id,
186
207
  )
208
+ if field.field_key == "password":
209
+ password_row = created
210
+ if history:
211
+ # The private writer deliberately requires an active field and an
212
+ # active snapshot manifest. This activation is transaction-local:
213
+ # the item remains disabled, no grant is created, and the field is
214
+ # made inactive again before the receipt can commit.
215
+ assert password_row is not None
216
+ await _write_quarantined_history(
217
+ item_id=str(item.id),
218
+ field_id=str(password_row.id),
219
+ actor_id=actor_id,
220
+ audit_organization_id=audit_organization_id,
221
+ handling=password_fields[0].handling,
222
+ history=history,
223
+ )
187
224
  for attachment in attachments:
188
225
  await CredentialAttachment.create(
189
226
  credential_item_id=item.id,
@@ -215,6 +252,111 @@ async def create_backup_record_quarantined(
215
252
  return BackupRestoreResult(str(item.id), False)
216
253
 
217
254
 
255
+ def _validate_history(
256
+ history: list[BackupHistoryRevisionSpec] | None,
257
+ password_fields: list[FieldSpec],
258
+ ) -> tuple[BackupHistoryRevisionSpec, ...]:
259
+ """Refuse an incomplete portable chain before any restore row is inserted."""
260
+ if history is None:
261
+ return ()
262
+ if not isinstance(history, list):
263
+ raise ValueError("backup history is invalid")
264
+ if not history:
265
+ return ()
266
+ if len(password_fields) != 1 or password_fields[0].handling not in {"visible", "revealable"}:
267
+ raise ValueError("backup history requires one ordinary password field")
268
+ checked: list[BackupHistoryRevisionSpec] = []
269
+ for expected, revision in enumerate(history, start=1):
270
+ if (
271
+ not isinstance(revision, BackupHistoryRevisionSpec)
272
+ or type(revision.revision) is not int
273
+ or revision.revision != expected
274
+ or not isinstance(revision.value, str)
275
+ or (
276
+ revision.source_recorded_at is not None
277
+ and (
278
+ not isinstance(revision.source_recorded_at, datetime)
279
+ or revision.source_recorded_at.tzinfo is None
280
+ or revision.source_recorded_at.utcoffset() is None
281
+ )
282
+ )
283
+ ):
284
+ raise ValueError("backup history is invalid")
285
+ checked.append(revision)
286
+ if checked[-1].value != password_fields[0].value:
287
+ raise ValueError("backup history does not match the current password")
288
+ return tuple(checked)
289
+
290
+
291
+ async def _write_quarantined_history(
292
+ *,
293
+ item_id: str,
294
+ field_id: str,
295
+ actor_id: str,
296
+ audit_organization_id: str,
297
+ handling: str,
298
+ history: tuple[BackupHistoryRevisionSpec, ...],
299
+ ) -> None:
300
+ """Write fresh destination snapshots while preserving final quarantine."""
301
+ if await locked_history_revision(item_id) != 0:
302
+ raise RuntimeError("new backup item has an unexpected history revision")
303
+ first = history[0]
304
+ current = encrypt_value(first.value)
305
+ changed = await UserSecret.update_where(
306
+ {"id": field_id, "deleted_at__isnull": True},
307
+ value_encrypted=current,
308
+ value_hint=mask_value(first.value),
309
+ is_active=True,
310
+ updated_by=actor_id,
311
+ updated_at=datetime.now(UTC),
312
+ )
313
+ if changed.rows_affected != 1:
314
+ raise RuntimeError("backup password field disappeared during restore")
315
+ expected_revision = 0
316
+ started = datetime.now(UTC)
317
+ for revision in history:
318
+ after = current if revision.revision == 1 else encrypt_value(revision.value)
319
+ if revision.revision > 1:
320
+ changed = await UserSecret.update_where(
321
+ {"id": field_id, "deleted_at__isnull": True},
322
+ value_encrypted=after,
323
+ value_hint=mask_value(revision.value),
324
+ updated_by=actor_id,
325
+ updated_at=datetime.now(UTC),
326
+ )
327
+ if changed.rows_affected != 1:
328
+ raise RuntimeError("backup password field disappeared during restore")
329
+ snapshot = password_field_update_snapshot(
330
+ item_id=item_id,
331
+ item_user_id=actor_id,
332
+ audit_organization_id=audit_organization_id,
333
+ field_id=field_id,
334
+ field_key="password",
335
+ handling=handling,
336
+ execution_purpose="general",
337
+ before_encrypted=current,
338
+ after_encrypted=after,
339
+ capture_started_at=started,
340
+ source_recorded_at=revision.source_recorded_at,
341
+ )
342
+ expected_revision = await write_password_field_update_snapshot(
343
+ item_id=item_id,
344
+ actor_id=actor_id,
345
+ audit_organization_id=audit_organization_id,
346
+ expected_revision=expected_revision,
347
+ snapshot=snapshot,
348
+ )
349
+ current = after
350
+ changed = await UserSecret.update_where(
351
+ {"id": field_id, "deleted_at__isnull": True},
352
+ is_active=False,
353
+ updated_by=actor_id,
354
+ updated_at=datetime.now(UTC),
355
+ )
356
+ if changed.rows_affected != 1:
357
+ raise RuntimeError("backup password field disappeared during restore")
358
+
359
+
218
360
  async def _validate_replay(item_id: str, actor_id: str) -> None:
219
361
  rows = (
220
362
  await CredentialItem.filter(id=item_id, deleted_at__isnull=True)
@@ -1290,6 +1290,109 @@ async def add_credential_field(
1290
1290
  return _field_row(rows[0])
1291
1291
 
1292
1292
 
1293
+ def _captures_personal_password_history(
1294
+ item_raw: dict[str, Any] | None,
1295
+ field_raw: dict[str, Any],
1296
+ ) -> bool:
1297
+ """Return whether this locked row belongs in the private password history.
1298
+
1299
+ The module-local gate remains deliberately false in production. Keeping
1300
+ eligibility here gives every existing-field ciphertext writer the same
1301
+ future activation boundary without adding a caller-controlled switch.
1302
+ """
1303
+ return (
1304
+ _PASSWORD_HISTORY_CAPTURE_ENABLED
1305
+ and item_raw is not None
1306
+ and item_raw.get("user_id") is not None
1307
+ and item_raw.get("organization_id") is None
1308
+ and field_raw.get("field_key") == "password"
1309
+ and field_raw.get("handling") in {"visible", "revealable"}
1310
+ and field_raw.get("execution_purpose") == GENERAL_EXECUTION_PURPOSE
1311
+ and field_raw.get("is_active") is True
1312
+ )
1313
+
1314
+
1315
+ async def _prepare_personal_password_history_capture(
1316
+ *,
1317
+ item_raw: dict[str, Any] | None,
1318
+ field_raw: dict[str, Any],
1319
+ actor_id: str | None,
1320
+ audit_organization_id: str | None,
1321
+ ) -> tuple[int, datetime] | None:
1322
+ """Write the single allowed bootstrap before an eligible ciphertext change.
1323
+
1324
+ The caller already holds both the item and field locks. Returning the
1325
+ locked next revision makes the later delta conditional on the same chain.
1326
+ """
1327
+ if not _captures_personal_password_history(item_raw, field_raw):
1328
+ return None
1329
+ if not actor_id or not audit_organization_id:
1330
+ raise VaultHistorySnapshotError(
1331
+ "personal password history requires actor and admitted audit organization"
1332
+ )
1333
+ assert item_raw is not None
1334
+ item_id = str(item_raw["id"])
1335
+ capture_started_at = datetime.now(UTC)
1336
+ revision = await locked_history_revision(item_id)
1337
+ if revision == 0:
1338
+ encrypted = field_raw["value_encrypted"]
1339
+ bootstrap = password_field_update_snapshot(
1340
+ item_id=item_id,
1341
+ item_user_id=str(item_raw["user_id"]),
1342
+ audit_organization_id=audit_organization_id,
1343
+ field_id=str(field_raw["id"]),
1344
+ field_key=str(field_raw["field_key"]),
1345
+ handling=str(field_raw["handling"]),
1346
+ execution_purpose=str(field_raw["execution_purpose"]),
1347
+ before_encrypted=encrypted,
1348
+ after_encrypted=encrypted,
1349
+ capture_started_at=capture_started_at,
1350
+ )
1351
+ revision = await write_password_field_update_snapshot(
1352
+ item_id=item_id,
1353
+ actor_id=actor_id,
1354
+ audit_organization_id=audit_organization_id,
1355
+ expected_revision=revision,
1356
+ snapshot=bootstrap,
1357
+ )
1358
+ return revision, capture_started_at
1359
+
1360
+
1361
+ async def _append_personal_password_history_capture(
1362
+ *,
1363
+ capture: tuple[int, datetime] | None,
1364
+ item_raw: dict[str, Any] | None,
1365
+ field_raw: dict[str, Any],
1366
+ after_encrypted: Any,
1367
+ actor_id: str | None,
1368
+ audit_organization_id: str | None,
1369
+ ) -> None:
1370
+ """Append the locked before-to-after delta after its ciphertext update."""
1371
+ if capture is None:
1372
+ return
1373
+ assert item_raw is not None and actor_id and audit_organization_id
1374
+ revision, capture_started_at = capture
1375
+ snapshot = password_field_update_snapshot(
1376
+ item_id=str(item_raw["id"]),
1377
+ item_user_id=str(item_raw["user_id"]),
1378
+ audit_organization_id=audit_organization_id,
1379
+ field_id=str(field_raw["id"]),
1380
+ field_key=str(field_raw["field_key"]),
1381
+ handling=str(field_raw["handling"]),
1382
+ execution_purpose=str(field_raw["execution_purpose"]),
1383
+ before_encrypted=field_raw["value_encrypted"],
1384
+ after_encrypted=after_encrypted,
1385
+ capture_started_at=capture_started_at,
1386
+ )
1387
+ await write_password_field_update_snapshot(
1388
+ item_id=str(item_raw["id"]),
1389
+ actor_id=actor_id,
1390
+ audit_organization_id=audit_organization_id,
1391
+ expected_revision=revision,
1392
+ snapshot=snapshot,
1393
+ )
1394
+
1395
+
1293
1396
  async def update_credential_field_value(
1294
1397
  field_id: str,
1295
1398
  value: str,
@@ -1323,30 +1426,12 @@ async def update_credential_field_value(
1323
1426
  f"field {raw.get('field_key') or raw.get('key')!r} is not editable — "
1324
1427
  "it is managed by its owning integration"
1325
1428
  )
1326
- capture = (
1327
- _PASSWORD_HISTORY_CAPTURE_ENABLED and not system_rotation
1328
- and item_raw is not None and item_raw.get("user_id") is not None
1329
- and raw.get("field_key") == "password" and raw.get("handling") in {"visible", "revealable"}
1330
- and raw.get("execution_purpose") == GENERAL_EXECUTION_PURPOSE and raw.get("is_active") is True
1429
+ capture = await _prepare_personal_password_history_capture(
1430
+ item_raw=item_raw,
1431
+ field_raw=raw,
1432
+ actor_id=actor_id,
1433
+ audit_organization_id=audit_organization_id,
1331
1434
  )
1332
- history_revision: int | None = None
1333
- capture_started_at = datetime.now(UTC)
1334
- if capture:
1335
- if not actor_id or not audit_organization_id:
1336
- raise VaultHistorySnapshotError("personal password history requires actor and admitted audit organization")
1337
- history_revision = await locked_history_revision(str(item_raw["id"]))
1338
- if history_revision == 0:
1339
- before = password_field_update_snapshot(
1340
- item_id=str(item_raw["id"]), item_user_id=str(item_raw["user_id"]),
1341
- audit_organization_id=audit_organization_id, field_id=field_id,
1342
- field_key=str(raw["field_key"]), handling=str(raw["handling"]),
1343
- execution_purpose=str(raw["execution_purpose"]), before_encrypted=raw["value_encrypted"],
1344
- after_encrypted=raw["value_encrypted"], capture_started_at=capture_started_at,
1345
- )
1346
- history_revision = await write_password_field_update_snapshot(
1347
- item_id=str(item_raw["id"]), actor_id=actor_id, audit_organization_id=audit_organization_id,
1348
- expected_revision=history_revision, snapshot=before,
1349
- )
1350
1435
  encrypted_value = encrypt_value(value)
1351
1436
  await UserSecret.update_where(
1352
1437
  {"id": field_id, "deleted_at__isnull": True},
@@ -1355,19 +1440,14 @@ async def update_credential_field_value(
1355
1440
  updated_by=actor_id,
1356
1441
  updated_at=datetime.now(UTC),
1357
1442
  )
1358
- if capture:
1359
- assert item_raw is not None and history_revision is not None and actor_id and audit_organization_id
1360
- after = password_field_update_snapshot(
1361
- item_id=str(item_raw["id"]), item_user_id=str(item_raw["user_id"]),
1362
- audit_organization_id=audit_organization_id, field_id=field_id,
1363
- field_key=str(raw["field_key"]), handling=str(raw["handling"]),
1364
- execution_purpose=str(raw["execution_purpose"]), before_encrypted=raw["value_encrypted"],
1365
- after_encrypted=encrypted_value, capture_started_at=capture_started_at,
1366
- )
1367
- await write_password_field_update_snapshot(
1368
- item_id=str(item_raw["id"]), actor_id=actor_id, audit_organization_id=audit_organization_id,
1369
- expected_revision=history_revision, snapshot=after,
1370
- )
1443
+ await _append_personal_password_history_capture(
1444
+ capture=capture,
1445
+ item_raw=item_raw,
1446
+ field_raw=raw,
1447
+ after_encrypted=encrypted_value,
1448
+ actor_id=actor_id,
1449
+ audit_organization_id=audit_organization_id,
1450
+ )
1371
1451
  await record_secret_audit(
1372
1452
  action="rotated" if system_rotation else "field_updated",
1373
1453
  actor_id=actor_id,
@@ -1408,22 +1488,43 @@ async def rotate_credential_fields(
1408
1488
  item_raw = await _lock_item_raw(item_id)
1409
1489
  rows = await UserSecret.filter(
1410
1490
  credential_item_id=item_id, field_key__in=list(new_values), deleted_at__isnull=True
1411
- ).values("id", "field_key", "credential_item_id", "execution_purpose")
1491
+ ).select_for_update().values(
1492
+ "id", "field_key", "credential_item_id", "execution_purpose", "handling", "is_active", "value_encrypted"
1493
+ )
1412
1494
  protected = [row for row in rows if _is_protected(row)]
1413
1495
  if protected:
1414
1496
  _refuse_protected(protected[0])
1415
- found = {str(r["field_key"]): str(r["id"]) for r in rows}
1497
+ found = {str(r["field_key"]): r for r in rows}
1416
1498
  missing = sorted(set(new_values) - set(found))
1417
1499
  if missing:
1418
1500
  raise SecretNotFoundError(f"item {item_id} has no fields {missing} to rotate")
1501
+ captures = {
1502
+ fk: await _prepare_personal_password_history_capture(
1503
+ item_raw=item_raw,
1504
+ field_raw=field_raw,
1505
+ actor_id=actor_id,
1506
+ audit_organization_id=audit_organization_id,
1507
+ )
1508
+ for fk, field_raw in found.items()
1509
+ }
1419
1510
  for fk, plaintext in new_values.items():
1511
+ field_raw = found[fk]
1512
+ encrypted_value = encrypt_value(plaintext)
1420
1513
  await UserSecret.update_where(
1421
- {"id": found[fk]},
1422
- value_encrypted=encrypt_value(plaintext),
1514
+ {"id": str(field_raw["id"])},
1515
+ value_encrypted=encrypted_value,
1423
1516
  value_hint=mask_value(plaintext),
1424
1517
  updated_by=actor_id,
1425
1518
  updated_at=now,
1426
1519
  )
1520
+ await _append_personal_password_history_capture(
1521
+ capture=captures[fk],
1522
+ item_raw=item_raw,
1523
+ field_raw=field_raw,
1524
+ after_encrypted=encrypted_value,
1525
+ actor_id=actor_id,
1526
+ audit_organization_id=audit_organization_id,
1527
+ )
1427
1528
  await record_secret_audit(
1428
1529
  action="rotated",
1429
1530
  actor_id=actor_id,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import re
6
6
  import uuid
7
7
  from datetime import UTC, datetime
8
+ from math import isfinite
8
9
  from typing import Any
9
10
 
10
11
  from matrx_orm import call_function
@@ -22,6 +23,7 @@ _ENVELOPE_KEYS = {
22
23
  "schema_version", "snapshot_id", "mutation_kind", "original_ownership",
23
24
  "audit_organization_id", "capture_started_at", "component_manifest", "changed_components",
24
25
  }
26
+ _ENVELOPE_KEYS_WITH_SOURCE_RECORDED_AT = _ENVELOPE_KEYS | {"source_recorded_at"}
25
27
 
26
28
 
27
29
  class VaultHistorySnapshotError(ValueError):
@@ -44,10 +46,21 @@ def _state(value_encrypted: Any, key_fingerprint: str) -> dict[str, str]:
44
46
  return {"value_encrypted": _ciphertext(value_encrypted), "key_fingerprint": key_fingerprint}
45
47
 
46
48
 
49
+ def _source_recorded_at(value: datetime) -> str:
50
+ if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
51
+ raise VaultHistorySnapshotError("Vault history source timestamp must be timezone-aware")
52
+ try:
53
+ if not isfinite(value.timestamp()):
54
+ raise ValueError
55
+ except (OverflowError, OSError, ValueError) as exc:
56
+ raise VaultHistorySnapshotError("Vault history source timestamp must be finite") from exc
57
+ return value.astimezone(UTC).isoformat()
58
+
59
+
47
60
  def password_field_update_snapshot(
48
61
  *, item_id: str, item_user_id: str, audit_organization_id: str, field_id: str,
49
62
  field_key: str, handling: str, execution_purpose: str, before_encrypted: Any,
50
- after_encrypted: Any, capture_started_at: datetime,
63
+ after_encrypted: Any, capture_started_at: datetime, source_recorded_at: datetime | None = None,
51
64
  ) -> dict[str, Any]:
52
65
  """Build the only Gate-B shape accepted for an ordinary password update."""
53
66
  if field_key != "password" or handling not in {"visible", "revealable"} or execution_purpose != "general":
@@ -79,13 +92,18 @@ def password_field_update_snapshot(
79
92
  "component_manifest": [manifest],
80
93
  "changed_components": [change],
81
94
  }
95
+ if source_recorded_at is not None:
96
+ snapshot["source_recorded_at"] = _source_recorded_at(source_recorded_at)
82
97
  validate_snapshot_v1(snapshot)
83
98
  return snapshot
84
99
 
85
100
 
86
101
  def validate_snapshot_v1(snapshot: dict[str, Any]) -> None:
87
102
  """Reject accidental plaintext, hints, unknown keys, and incomplete v1 states."""
88
- if set(snapshot) != _ENVELOPE_KEYS or snapshot.get("schema_version") != 1 or snapshot.get("mutation_kind") != "update":
103
+ if (
104
+ set(snapshot) != _ENVELOPE_KEYS
105
+ and set(snapshot) != _ENVELOPE_KEYS_WITH_SOURCE_RECORDED_AT
106
+ ) or snapshot.get("schema_version") != 1 or snapshot.get("mutation_kind") != "update":
89
107
  raise VaultHistorySnapshotError("Vault history snapshot is not the closed v1 envelope")
90
108
  try:
91
109
  uuid.UUID(str(snapshot["snapshot_id"]))
@@ -96,6 +114,11 @@ def validate_snapshot_v1(snapshot: dict[str, Any]) -> None:
96
114
  raise VaultHistorySnapshotError("Vault history snapshot has invalid identity data") from exc
97
115
  if set(snapshot["original_ownership"]) != {"user_id"}:
98
116
  raise VaultHistorySnapshotError("Vault history snapshot must have one personal owner")
117
+ if "source_recorded_at" in snapshot:
118
+ try:
119
+ _source_recorded_at(datetime.fromisoformat(str(snapshot["source_recorded_at"]).replace("Z", "+00:00")))
120
+ except (TypeError, ValueError) as exc:
121
+ raise VaultHistorySnapshotError("Vault history source timestamp is invalid") from exc
99
122
  manifest = snapshot["component_manifest"]
100
123
  changes = snapshot["changed_components"]
101
124
  if not isinstance(manifest, list) or len(manifest) != 1 or not isinstance(changes, list) or len(changes) != 1:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.183"
3
+ version = "3.1.185"
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" }
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "gitpython>=3.1.40",
42
42
  "psycopg[binary]>=3.2.5",
43
43
  "psycopg-pool>=3.2.5",
44
- "matrx-utils>=2.0.54",
44
+ "matrx-utils>=2.0.55",
45
45
  "pyyaml>=6.0.3",
46
46
  "python-dotenv>=1.0",
47
47
  "cryptography>=43.0",
@@ -0,0 +1,144 @@
1
+ """Portable backup history is all-or-nothing before database insertion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import SimpleNamespace
6
+ from datetime import UTC, datetime
7
+
8
+ import pytest
9
+
10
+ from matrx_orm.secrets_battery.backup_restore import (
11
+ BackupHistoryRevisionSpec,
12
+ _validate_history,
13
+ )
14
+ from matrx_orm.secrets_battery.items import FieldSpec
15
+
16
+
17
+ def _password(value: str = "current") -> list[FieldSpec]:
18
+ return [FieldSpec(field_key="password", value=value, handling="revealable")]
19
+
20
+
21
+ def _revision(number: int, value: str) -> BackupHistoryRevisionSpec:
22
+ return BackupHistoryRevisionSpec(number, value)
23
+
24
+
25
+ def test_history_requires_complete_chain_ending_at_current_password():
26
+ result = _validate_history(
27
+ [_revision(1, "old"), _revision(2, "current")],
28
+ _password(),
29
+ )
30
+ assert [entry.revision for entry in result] == [1, 2]
31
+
32
+
33
+ def test_history_allows_a_same_plaintext_delta_when_the_source_ciphertext_rotated():
34
+ result = _validate_history(
35
+ [_revision(1, "current"), _revision(2, "current")], _password()
36
+ )
37
+ assert len(result) == 2
38
+
39
+
40
+ def test_history_carries_an_aware_source_timestamp_without_using_it_for_ordering():
41
+ result = _validate_history(
42
+ [BackupHistoryRevisionSpec(1, "current", datetime(2040, 1, 1, tzinfo=UTC))],
43
+ _password(),
44
+ )
45
+ assert result[0].source_recorded_at == datetime(2040, 1, 1, tzinfo=UTC)
46
+
47
+
48
+ @pytest.mark.parametrize(
49
+ ("history", "fields"),
50
+ [
51
+ ([_revision(2, "current")], _password()),
52
+ ([_revision(1, "old")], _password("current")),
53
+ ([_revision(1, "current")], []),
54
+ ([_revision(1, "current")], [FieldSpec(field_key="password", value="current", handling="sealed")]),
55
+ ],
56
+ )
57
+ def test_history_refuses_gap_sealed_or_current_value_mismatch_before_insert(history, fields):
58
+ with pytest.raises(ValueError):
59
+ _validate_history(history, fields)
60
+
61
+
62
+ @pytest.mark.asyncio
63
+ @pytest.mark.parametrize(
64
+ ("fields", "history"),
65
+ [
66
+ (_password(), [_revision(2, "current")]),
67
+ (
68
+ _password() + [FieldSpec(field_key="passkey_private", value="protected")],
69
+ [_revision(1, "current")],
70
+ ),
71
+ ],
72
+ )
73
+ async def test_invalid_history_or_protected_field_refuses_before_opening_restore_transaction(
74
+ monkeypatch, fields, history
75
+ ):
76
+ from matrx_orm.secrets_battery import backup_restore
77
+
78
+ opened = False
79
+
80
+ def forbidden_transaction(*_args, **_kwargs):
81
+ nonlocal opened
82
+ opened = True
83
+ raise AssertionError("history validation must precede database insertion")
84
+
85
+ monkeypatch.setattr(backup_restore, "transaction", forbidden_transaction)
86
+ with pytest.raises(ValueError, match="Invalid backup restore record"):
87
+ await backup_restore.create_backup_record_quarantined(
88
+ actor_id="11111111-1111-4111-8111-111111111111",
89
+ audit_organization_id="22222222-2222-4222-8222-222222222222",
90
+ mutation_id="33333333-3333-4333-8333-333333333333",
91
+ request_fingerprint=b"x" * 32,
92
+ display_name="test",
93
+ definition_key="website_login",
94
+ definition_version=1,
95
+ provider_key=None,
96
+ description=None,
97
+ tags=[],
98
+ login_urls=[],
99
+ uri_match_mode=None,
100
+ notes=None,
101
+ non_secret_fields=[],
102
+ fields=fields,
103
+ attachments=[],
104
+ history=history,
105
+ )
106
+ assert opened is False
107
+
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_same_plaintext_delta_is_written_with_fresh_destination_ciphertext(monkeypatch):
111
+ from matrx_orm.secrets_battery import backup_restore, crypto
112
+
113
+ snapshots = []
114
+
115
+ async def revision(_item_id):
116
+ return 0
117
+
118
+ async def update(*_args, **_kwargs):
119
+ return SimpleNamespace(rows_affected=1)
120
+
121
+ async def write(**kwargs):
122
+ snapshots.append(kwargs["snapshot"])
123
+ return kwargs["expected_revision"] + 1
124
+
125
+ monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", "backup-history-test-key")
126
+ crypto.reset_encryption_for_tests()
127
+ monkeypatch.setattr(backup_restore, "locked_history_revision", revision)
128
+ monkeypatch.setattr(backup_restore.UserSecret, "update_where", update)
129
+ monkeypatch.setattr(backup_restore, "write_password_field_update_snapshot", write)
130
+ await backup_restore._write_quarantined_history(
131
+ item_id="11111111-1111-4111-8111-111111111111",
132
+ field_id="22222222-2222-4222-8222-222222222222",
133
+ actor_id="33333333-3333-4333-8333-333333333333",
134
+ audit_organization_id="44444444-4444-4444-8444-444444444444",
135
+ handling="revealable",
136
+ history=(
137
+ BackupHistoryRevisionSpec(1, "same", datetime(2020, 1, 1, tzinfo=UTC)),
138
+ BackupHistoryRevisionSpec(2, "same", datetime(2020, 1, 2, tzinfo=UTC)),
139
+ ),
140
+ )
141
+ assert snapshots[1]["changed_components"][0]["before"] != snapshots[1]["changed_components"][0]["after"]
142
+ assert [snapshot["source_recorded_at"] for snapshot in snapshots] == [
143
+ "2020-01-01T00:00:00+00:00", "2020-01-02T00:00:00+00:00"
144
+ ]