matrx-orm 3.1.9__tar.gz → 3.1.11__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.
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/PKG-INFO +1 -1
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/__init__.py +10 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/async_db_manager.py +15 -1
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/config.py +18 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/query/executor.py +6 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/__init__.py +14 -1
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/errors.py +28 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/managed.py +256 -18
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/session.py +28 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/pyproject.toml +1 -1
- matrx_orm-3.1.11/tests/level1/test_managed_write_guard.py +226 -0
- matrx_orm-3.1.9/tests/level1/test_managed_write_guard.py +0 -104
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/.gitignore +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/CLAUDE.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/MODEL_API.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/docs/migrations.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/scripts/release.sh +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/conftest.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_execute_admin_sql.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_filter_raw.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.9 → matrx_orm-3.1.11}/tests/test_sync_twins_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.11
|
|
4
4
|
Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
|
|
5
5
|
Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
|
|
6
6
|
Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
|
|
@@ -270,6 +270,7 @@ def _lazy_api():
|
|
|
270
270
|
# ordering, coalescing, pending-aware reads, managed-write enforcement,
|
|
271
271
|
# transactional flush, watchdog integration. See matrx_orm/session/.
|
|
272
272
|
from matrx_orm.session import (
|
|
273
|
+
CoordinatorWriteViolation,
|
|
273
274
|
DependencyCycleError,
|
|
274
275
|
FlushError,
|
|
275
276
|
FlushReport,
|
|
@@ -281,12 +282,16 @@ from matrx_orm.session import (
|
|
|
281
282
|
SessionPhase,
|
|
282
283
|
WatchedLifecycleConfig,
|
|
283
284
|
WatchedLifecycleRegistry,
|
|
285
|
+
COORDINATOR_BYPASS_ACKNOWLEDGEMENT,
|
|
286
|
+
allow_direct_coordinator_write,
|
|
287
|
+
coordinator_policy_tables,
|
|
284
288
|
current_session,
|
|
285
289
|
declare_managed,
|
|
286
290
|
drain_spilled_ops,
|
|
287
291
|
is_in_session,
|
|
288
292
|
record_error,
|
|
289
293
|
record_failures,
|
|
294
|
+
register_coordinator_owned_model,
|
|
290
295
|
)
|
|
291
296
|
from matrx_orm.session import (
|
|
292
297
|
run_sweeper as run_lifecycle_sweeper,
|
|
@@ -296,6 +301,11 @@ from matrx_orm.session import (
|
|
|
296
301
|
)
|
|
297
302
|
|
|
298
303
|
__all__ = [
|
|
304
|
+
"CoordinatorWriteViolation",
|
|
305
|
+
"COORDINATOR_BYPASS_ACKNOWLEDGEMENT",
|
|
306
|
+
"allow_direct_coordinator_write",
|
|
307
|
+
"coordinator_policy_tables",
|
|
308
|
+
"register_coordinator_owned_model",
|
|
299
309
|
# ── Configuration ──────────────────────────────────────────────────────────
|
|
300
310
|
"DatabaseProjectConfig",
|
|
301
311
|
"register_database",
|
|
@@ -36,6 +36,20 @@ from matrx_orm.exceptions import (
|
|
|
36
36
|
|
|
37
37
|
logger = logging.getLogger("matrx_orm.async_db_manager")
|
|
38
38
|
|
|
39
|
+
|
|
40
|
+
def _resolve_ssl(value: Any) -> Any:
|
|
41
|
+
"""Normalize DatabaseProjectConfig.ssl for asyncpg's ``ssl=`` kwarg.
|
|
42
|
+
|
|
43
|
+
``False`` / ``"disable"`` / ``"off"`` → no TLS (Coolify / local Postgres).
|
|
44
|
+
Everything else is passed through (default ``"require"`` for Supabase).
|
|
45
|
+
"""
|
|
46
|
+
if value is False or value is None:
|
|
47
|
+
return False
|
|
48
|
+
if isinstance(value, str) and value.strip().lower() in {"disable", "off", "false", "0"}:
|
|
49
|
+
return False
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
# asyncpg SQLSTATE class 42 "undefined …" — the SQL named something the live DB
|
|
40
54
|
# does not have. All siblings of SyntaxOrAccessError; PostgresSyntaxError is
|
|
41
55
|
# caught separately (it's an ORM-bug shape, not a schema-drift shape). These map
|
|
@@ -409,7 +423,7 @@ class AsyncDatabaseManager:
|
|
|
409
423
|
max_size=config.get("pool_max", 30),
|
|
410
424
|
command_timeout=config.get("command_timeout", 10),
|
|
411
425
|
max_inactive_connection_lifetime=max_inactive_connection_lifetime,
|
|
412
|
-
ssl="require",
|
|
426
|
+
ssl=_resolve_ssl(config.get("ssl", "require")),
|
|
413
427
|
statement_cache_size=statement_cache_size,
|
|
414
428
|
server_settings=server_settings,
|
|
415
429
|
init=init_callback,
|
|
@@ -83,6 +83,14 @@ class DatabaseProjectConfig:
|
|
|
83
83
|
# Supabase compute can spike first-query latency on cold connections).
|
|
84
84
|
server_settings: dict[str, str] = field(default_factory=dict)
|
|
85
85
|
|
|
86
|
+
# TLS for the asyncpg pool. Default ``"require"`` matches Supabase /
|
|
87
|
+
# managed Postgres (which reject plaintext). Direct Coolify / local
|
|
88
|
+
# Postgres images typically have ``ssl=off`` — set ``ssl=False`` (or
|
|
89
|
+
# ``"disable"``) for those. Accepted values are whatever asyncpg accepts
|
|
90
|
+
# for its ``ssl=`` kwarg (``False``, ``True``, ``"require"``, an
|
|
91
|
+
# ``ssl.SSLContext``, …).
|
|
92
|
+
ssl: Any = "require"
|
|
93
|
+
|
|
86
94
|
# Write queue — gates write operations behind an async queue so they wait in
|
|
87
95
|
# order instead of racing for pool connections and timing out under load.
|
|
88
96
|
write_queue_enabled: bool = True
|
|
@@ -242,6 +250,7 @@ class DatabaseRegistry:
|
|
|
242
250
|
"command_timeout": config.command_timeout,
|
|
243
251
|
"session_init_sql": list(config.session_init_sql),
|
|
244
252
|
"server_settings": dict(config.server_settings),
|
|
253
|
+
"ssl": config.ssl,
|
|
245
254
|
"write_queue_enabled": config.write_queue_enabled,
|
|
246
255
|
"write_concurrency": config.write_concurrency,
|
|
247
256
|
"write_queue_size": config.write_queue_size,
|
|
@@ -408,6 +417,7 @@ def register_database_from_env(
|
|
|
408
417
|
command_timeout: int | None = None,
|
|
409
418
|
session_init_sql: list[str] | None = None,
|
|
410
419
|
server_settings: dict[str, str] | None = None,
|
|
420
|
+
ssl: Any = None,
|
|
411
421
|
write_queue_enabled: bool | None = None,
|
|
412
422
|
write_concurrency: int | None = None,
|
|
413
423
|
write_queue_size: int | None = None,
|
|
@@ -562,6 +572,13 @@ def register_database_from_env(
|
|
|
562
572
|
supabase_secret_key or os.environ.get(_supabase_secret_env, "").strip()
|
|
563
573
|
)
|
|
564
574
|
|
|
575
|
+
_ssl_env = _overrides.get("SSL", f"{env_prefix}_SSL")
|
|
576
|
+
if ssl is not None:
|
|
577
|
+
_resolved_ssl: Any = ssl
|
|
578
|
+
else:
|
|
579
|
+
_ssl_raw = os.environ.get(_ssl_env, "").strip()
|
|
580
|
+
_resolved_ssl = _ssl_raw if _ssl_raw else "require"
|
|
581
|
+
|
|
565
582
|
try:
|
|
566
583
|
config = DatabaseProjectConfig(
|
|
567
584
|
name=name,
|
|
@@ -582,6 +599,7 @@ def register_database_from_env(
|
|
|
582
599
|
supabase_secret_key=_resolved_supabase_secret_key,
|
|
583
600
|
session_init_sql=list(session_init_sql or []),
|
|
584
601
|
server_settings=dict(server_settings or {}),
|
|
602
|
+
ssl=_resolved_ssl,
|
|
585
603
|
**pool_kwargs,
|
|
586
604
|
)
|
|
587
605
|
registry.register(config)
|
|
@@ -957,6 +957,12 @@ class QueryExecutor:
|
|
|
957
957
|
(default ``BaseAdapter`` behaviour) so callers can fall through to the
|
|
958
958
|
SQL path.
|
|
959
959
|
"""
|
|
960
|
+
from matrx_orm.session.managed import assert_model_write_allowed
|
|
961
|
+
|
|
962
|
+
assert_model_write_allowed(
|
|
963
|
+
self.model,
|
|
964
|
+
str(query.get("operation") or "adapter write"),
|
|
965
|
+
)
|
|
960
966
|
return await self.db.execute_write_dict(query)
|
|
961
967
|
|
|
962
968
|
async def insert(self, query: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -50,6 +50,7 @@ Usage:
|
|
|
50
50
|
from matrx_orm.session.coalesce import coalesce_ops, summarize
|
|
51
51
|
from matrx_orm.session.dag import DependencyCycleError, tier_ops, tier_summary
|
|
52
52
|
from matrx_orm.session.errors import (
|
|
53
|
+
CoordinatorWriteViolation,
|
|
53
54
|
FlushError,
|
|
54
55
|
ManagedWriteViolation,
|
|
55
56
|
SessionClosedError,
|
|
@@ -65,7 +66,14 @@ from matrx_orm.session.lifecycle import (
|
|
|
65
66
|
run_sweeper,
|
|
66
67
|
sweep_once,
|
|
67
68
|
)
|
|
68
|
-
from matrx_orm.session.managed import
|
|
69
|
+
from matrx_orm.session.managed import (
|
|
70
|
+
COORDINATOR_BYPASS_ACKNOWLEDGEMENT,
|
|
71
|
+
allow_direct_coordinator_write,
|
|
72
|
+
coordinator_policy_tables,
|
|
73
|
+
declare_managed,
|
|
74
|
+
install_managed_writes,
|
|
75
|
+
register_coordinator_owned_model,
|
|
76
|
+
)
|
|
69
77
|
from matrx_orm.session.op import (
|
|
70
78
|
OpType,
|
|
71
79
|
SessionOp,
|
|
@@ -90,6 +98,7 @@ from matrx_orm.session.session import (
|
|
|
90
98
|
|
|
91
99
|
__all__ = [
|
|
92
100
|
"DependencyCycleError",
|
|
101
|
+
"CoordinatorWriteViolation",
|
|
93
102
|
"FlushError",
|
|
94
103
|
"FlushReport",
|
|
95
104
|
"ManagedWriteViolation",
|
|
@@ -106,9 +115,13 @@ __all__ = [
|
|
|
106
115
|
"coalesce_ops",
|
|
107
116
|
"config_from_meta",
|
|
108
117
|
"current_session",
|
|
118
|
+
"COORDINATOR_BYPASS_ACKNOWLEDGEMENT",
|
|
119
|
+
"allow_direct_coordinator_write",
|
|
120
|
+
"coordinator_policy_tables",
|
|
109
121
|
"declare_managed",
|
|
110
122
|
"has_pending_in_stack",
|
|
111
123
|
"install_managed_writes",
|
|
124
|
+
"register_coordinator_owned_model",
|
|
112
125
|
"is_in_session",
|
|
113
126
|
"pending_ops_across_stack",
|
|
114
127
|
"make_delete",
|
|
@@ -48,6 +48,34 @@ class ManagedWriteViolation(SessionError):
|
|
|
48
48
|
self.op_kind = op_kind
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class CoordinatorWriteViolation(ManagedWriteViolation):
|
|
52
|
+
"""Raised when a coordinator-owned Model is written by another path."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
model_name: str,
|
|
57
|
+
op_kind: str,
|
|
58
|
+
*,
|
|
59
|
+
required_scope: str,
|
|
60
|
+
actual_scope: str | None,
|
|
61
|
+
) -> None:
|
|
62
|
+
SessionError.__init__(
|
|
63
|
+
self,
|
|
64
|
+
f"{model_name} is coordinator-owned (required write scope: "
|
|
65
|
+
f"{required_scope!r}). Direct {op_kind} is forbidden; the active "
|
|
66
|
+
f"Session scope is {actual_scope!r}. Queue this write through its "
|
|
67
|
+
"Coordinator so parent and child rows share one transactional DAG. "
|
|
68
|
+
"For a genuine out-of-request maintenance operation, use "
|
|
69
|
+
"allow_direct_coordinator_write(model, reason='...', "
|
|
70
|
+
"acknowledgement="
|
|
71
|
+
"'YES_I_KNOW_I_AM_SKIPPING_THE_COORDINATOR_ON_PURPOSE') explicitly.",
|
|
72
|
+
)
|
|
73
|
+
self.model_name = model_name
|
|
74
|
+
self.op_kind = op_kind
|
|
75
|
+
self.required_scope = required_scope
|
|
76
|
+
self.actual_scope = actual_scope
|
|
77
|
+
|
|
78
|
+
|
|
51
79
|
class SessionClosedError(SessionError):
|
|
52
80
|
"""Raised when an op is queued onto a Session that has already flushed
|
|
53
81
|
or errored. Generally callers should use ``async with Session():`` so
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Managed Model write interception — the runtime enforcement layer.
|
|
2
2
|
|
|
3
3
|
A Model opts into Session-aware writes by declaring
|
|
4
|
-
``Meta.managed_writes`` as
|
|
5
|
-
``
|
|
4
|
+
``Meta.managed_writes`` as ``"elevate"`` (default behavior when ``True``),
|
|
5
|
+
``"strict"``, or ``"coordinator"``::
|
|
6
6
|
|
|
7
7
|
class Conversation(Model):
|
|
8
8
|
...
|
|
@@ -10,6 +10,8 @@ A Model opts into Session-aware writes by declaring
|
|
|
10
10
|
managed_writes = "elevate" # auto-open one-shot if no Session
|
|
11
11
|
# OR
|
|
12
12
|
managed_writes = "strict" # raise ManagedWriteViolation if no Session
|
|
13
|
+
# OR
|
|
14
|
+
managed_writes = "coordinator" # audit writes lacking owner authority
|
|
13
15
|
|
|
14
16
|
When the Model's ``create`` / ``update`` / ``delete`` / ``bulk_*`` /
|
|
15
17
|
``upsert`` methods are called:
|
|
@@ -22,6 +24,9 @@ When the Model's ``create`` / ``update`` / ``delete`` / ``bulk_*`` /
|
|
|
22
24
|
was a direct write.
|
|
23
25
|
3. If NO active Session AND mode == ``"strict"``, raise
|
|
24
26
|
:class:`ManagedWriteViolation` — loud red banner, full stack trace.
|
|
27
|
+
4. Coordinator mode never blocks a write. A write without the Coordinator's
|
|
28
|
+
opaque authority (or an acknowledged exact-model override) continues and
|
|
29
|
+
emits a large red ERROR diagnostic. Authorized writes are silent.
|
|
25
30
|
|
|
26
31
|
For backward compatibility, this layer activates only when
|
|
27
32
|
``managed_writes`` is truthy. Unmanaged Models keep their current
|
|
@@ -49,12 +54,18 @@ from __future__ import annotations
|
|
|
49
54
|
import functools
|
|
50
55
|
import logging
|
|
51
56
|
import re
|
|
52
|
-
from contextlib import asynccontextmanager
|
|
57
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
58
|
+
from contextvars import ContextVar
|
|
59
|
+
from dataclasses import dataclass
|
|
60
|
+
from collections.abc import Iterator
|
|
53
61
|
from typing import Any
|
|
54
62
|
|
|
55
63
|
from matrx_utils import vcprint
|
|
56
64
|
|
|
57
|
-
from matrx_orm.session.errors import
|
|
65
|
+
from matrx_orm.session.errors import (
|
|
66
|
+
MANAGED_WRITE_SKILL_HINT,
|
|
67
|
+
ManagedWriteViolation,
|
|
68
|
+
)
|
|
58
69
|
from matrx_orm.session.session import Session, SessionPhase, session_stack
|
|
59
70
|
|
|
60
71
|
logger = logging.getLogger("matrx_orm.session.managed")
|
|
@@ -82,6 +93,71 @@ logger = logging.getLogger("matrx_orm.session.managed")
|
|
|
82
93
|
|
|
83
94
|
_STRICT_TABLES: set[str] = set()
|
|
84
95
|
|
|
96
|
+
# table token -> required Session.write_scope. This is stronger than strict:
|
|
97
|
+
# merely opening a Session/transaction is not authority to write the table.
|
|
98
|
+
_COORDINATOR_TABLES: dict[str, str] = {}
|
|
99
|
+
|
|
100
|
+
_COORDINATOR_AUTHORITY = object()
|
|
101
|
+
|
|
102
|
+
COORDINATOR_BYPASS_ACKNOWLEDGEMENT = (
|
|
103
|
+
"YES_I_KNOW_I_AM_SKIPPING_THE_COORDINATOR_ON_PURPOSE"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class DirectWriteException:
|
|
109
|
+
model_cls: type
|
|
110
|
+
reason: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_direct_write_exceptions: ContextVar[tuple[DirectWriteException, ...]] = ContextVar(
|
|
114
|
+
"matrx_orm_direct_write_exceptions",
|
|
115
|
+
default=(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@contextmanager
|
|
120
|
+
def allow_direct_coordinator_write(
|
|
121
|
+
model_cls: type,
|
|
122
|
+
*,
|
|
123
|
+
reason: str,
|
|
124
|
+
acknowledgement: str,
|
|
125
|
+
) -> Iterator[None]:
|
|
126
|
+
"""Temporarily acknowledge one model's intentional Coordinator bypass."""
|
|
127
|
+
cleaned = reason.strip()
|
|
128
|
+
if not cleaned:
|
|
129
|
+
raise ValueError("allow_direct_coordinator_write requires a non-empty reason")
|
|
130
|
+
if acknowledgement != COORDINATOR_BYPASS_ACKNOWLEDGEMENT:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"allow_direct_coordinator_write requires acknowledgement="
|
|
133
|
+
f"{COORDINATOR_BYPASS_ACKNOWLEDGEMENT!r}"
|
|
134
|
+
)
|
|
135
|
+
exception = DirectWriteException(model_cls, cleaned)
|
|
136
|
+
token = _direct_write_exceptions.set(_direct_write_exceptions.get() + (exception,))
|
|
137
|
+
try:
|
|
138
|
+
yield
|
|
139
|
+
finally:
|
|
140
|
+
_direct_write_exceptions.reset(token)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _has_direct_exception(model_cls: type) -> bool:
|
|
144
|
+
return any(item.model_cls is model_cls for item in _direct_write_exceptions.get())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _coordinator_session(*, database: str | None = None) -> Session:
|
|
148
|
+
"""Create the Session capability reserved for matrx-ai's Coordinator."""
|
|
149
|
+
from matrx_orm.session.session import Session as SessionClass
|
|
150
|
+
|
|
151
|
+
return SessionClass(
|
|
152
|
+
database=database,
|
|
153
|
+
write_scope="coordinator",
|
|
154
|
+
_write_authority=_COORDINATOR_AUTHORITY,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _has_coordinator_authority(session: Session | None) -> bool:
|
|
159
|
+
return session is not None and session._has_write_authority(_COORDINATOR_AUTHORITY)
|
|
160
|
+
|
|
85
161
|
# Leading INSERT INTO / UPDATE / DELETE FROM target — ORM-generated SQL only.
|
|
86
162
|
_WRITE_TARGET_RE = re.compile(
|
|
87
163
|
r"^\s*(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([^\s(]+)",
|
|
@@ -101,6 +177,71 @@ def _register_strict_table(model_cls: type) -> None:
|
|
|
101
177
|
_STRICT_TABLES.add(str(name).strip('"'))
|
|
102
178
|
|
|
103
179
|
|
|
180
|
+
def _register_coordinator_table(model_cls: type, required_scope: str) -> None:
|
|
181
|
+
meta = getattr(model_cls, "_meta", None)
|
|
182
|
+
if meta is None:
|
|
183
|
+
return
|
|
184
|
+
for name in (
|
|
185
|
+
getattr(meta, "qualified_table_name", None),
|
|
186
|
+
getattr(meta, "table_name", None),
|
|
187
|
+
):
|
|
188
|
+
if name:
|
|
189
|
+
normalized = str(name).strip('"')
|
|
190
|
+
_COORDINATOR_TABLES[normalized] = required_scope
|
|
191
|
+
_STRICT_TABLES.discard(normalized)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def register_coordinator_owned_model(entry: Any) -> None:
|
|
195
|
+
"""Host registrar consumed by matrx-ai's package-independent registry."""
|
|
196
|
+
model_cls = entry.model_cls
|
|
197
|
+
declare_managed(model_cls, mode="coordinator", write_scope="coordinator")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def coordinator_policy_tables() -> dict[str, str]:
|
|
201
|
+
"""Return installed table policies for startup/release diagnostics."""
|
|
202
|
+
return dict(_COORDINATOR_TABLES)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _coordinator_scope_for_model(model_cls: type) -> str | None:
|
|
206
|
+
meta = getattr(model_cls, "_meta", None)
|
|
207
|
+
if meta is None:
|
|
208
|
+
return None
|
|
209
|
+
for name in (
|
|
210
|
+
getattr(meta, "qualified_table_name", None),
|
|
211
|
+
getattr(meta, "table_name", None),
|
|
212
|
+
):
|
|
213
|
+
if name and (scope := _COORDINATOR_TABLES.get(str(name).strip('"'))):
|
|
214
|
+
return scope
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def assert_model_write_allowed(
|
|
219
|
+
model_cls: type,
|
|
220
|
+
op_kind: str,
|
|
221
|
+
*,
|
|
222
|
+
session: Session | None = None,
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""Warn on a Coordinator-owned Model write without Coordinator authority."""
|
|
225
|
+
required_scope = _coordinator_scope_for_model(model_cls)
|
|
226
|
+
if required_scope is None or _has_direct_exception(model_cls):
|
|
227
|
+
return True
|
|
228
|
+
active = session
|
|
229
|
+
if active is None:
|
|
230
|
+
for candidate in reversed(session_stack()):
|
|
231
|
+
if candidate.phase is SessionPhase.OPEN:
|
|
232
|
+
active = candidate
|
|
233
|
+
break
|
|
234
|
+
actual_scope = active.write_scope if active is not None else None
|
|
235
|
+
if _has_coordinator_authority(active):
|
|
236
|
+
return True
|
|
237
|
+
banner = _format_coordinator_banner(
|
|
238
|
+
model_cls.__name__, op_kind, required_scope, actual_scope
|
|
239
|
+
)
|
|
240
|
+
vcprint(banner, color="red")
|
|
241
|
+
logger.error("CoordinatorWriteViolation: %s", banner)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
104
245
|
def _has_active_open_session() -> bool:
|
|
105
246
|
for candidate in reversed(session_stack()):
|
|
106
247
|
if candidate.phase is SessionPhase.OPEN:
|
|
@@ -116,12 +257,37 @@ def assert_managed_write_allowed(query: str) -> None:
|
|
|
116
257
|
— when no strict table exists. Best-effort table parsing: ORM-generated DML
|
|
117
258
|
only; a query we can't parse is allowed through (the wrappers + the strict
|
|
118
259
|
create path remain the first line of defense)."""
|
|
119
|
-
if not _STRICT_TABLES:
|
|
260
|
+
if not _STRICT_TABLES and not _COORDINATOR_TABLES:
|
|
120
261
|
return
|
|
121
|
-
#
|
|
262
|
+
# Session.defer_* is authority-checked before an op can enter a flush.
|
|
263
|
+
# The flush may run in a deliberately detached context where its Session
|
|
264
|
+
# stack is absent, so the internal execution phase is trusted here.
|
|
122
265
|
from matrx_orm.session.flush import is_flushing
|
|
123
266
|
|
|
124
|
-
if is_flushing()
|
|
267
|
+
if is_flushing():
|
|
268
|
+
return
|
|
269
|
+
m = _WRITE_TARGET_RE.match(query)
|
|
270
|
+
if not m:
|
|
271
|
+
return
|
|
272
|
+
token = m.group(1).strip().strip('"')
|
|
273
|
+
bare_token = token.split(".")[-1].strip('"')
|
|
274
|
+
|
|
275
|
+
required_scope = _COORDINATOR_TABLES.get(token) or _COORDINATOR_TABLES.get(bare_token)
|
|
276
|
+
if required_scope is not None:
|
|
277
|
+
model_cls = _model_for_coordinator_table(token, bare_token)
|
|
278
|
+
if model_cls is not None:
|
|
279
|
+
assert_model_write_allowed(model_cls, "write")
|
|
280
|
+
else:
|
|
281
|
+
actual = _active_write_scope()
|
|
282
|
+
if actual != required_scope:
|
|
283
|
+
banner = _format_coordinator_banner(
|
|
284
|
+
token, "write", required_scope, actual
|
|
285
|
+
)
|
|
286
|
+
vcprint(banner, color="red", log_level="ERROR")
|
|
287
|
+
logger.error("CoordinatorWriteViolation: %s", banner)
|
|
288
|
+
return
|
|
289
|
+
# Governed contexts — always allowed:
|
|
290
|
+
if _has_active_open_session():
|
|
125
291
|
return
|
|
126
292
|
try:
|
|
127
293
|
from matrx_orm.core.transaction import get_active_connection
|
|
@@ -131,10 +297,6 @@ def assert_managed_write_allowed(query: str) -> None:
|
|
|
131
297
|
except ImportError:
|
|
132
298
|
pass
|
|
133
299
|
|
|
134
|
-
m = _WRITE_TARGET_RE.match(query)
|
|
135
|
-
if not m:
|
|
136
|
-
return
|
|
137
|
-
token = m.group(1).strip().strip('"')
|
|
138
300
|
if token in _STRICT_TABLES or token.split(".")[-1].strip('"') in _STRICT_TABLES:
|
|
139
301
|
banner = _format_chokepoint_banner(token)
|
|
140
302
|
vcprint(banner, color="red")
|
|
@@ -146,6 +308,33 @@ def assert_managed_write_allowed(query: str) -> None:
|
|
|
146
308
|
raise ManagedWriteViolation(token, "write (ungoverned)")
|
|
147
309
|
|
|
148
310
|
|
|
311
|
+
def _active_write_scope() -> str | None:
|
|
312
|
+
for candidate in reversed(session_stack()):
|
|
313
|
+
if candidate.phase is SessionPhase.OPEN:
|
|
314
|
+
return candidate.write_scope
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _model_for_coordinator_table(token: str, bare_token: str) -> type | None:
|
|
319
|
+
from matrx_orm.core.registry import ModelRegistry
|
|
320
|
+
|
|
321
|
+
for model_cls in ModelRegistry.all_models().values():
|
|
322
|
+
meta = getattr(model_cls, "_meta", None)
|
|
323
|
+
if meta is None:
|
|
324
|
+
continue
|
|
325
|
+
names = {
|
|
326
|
+
str(name).strip('"')
|
|
327
|
+
for name in (
|
|
328
|
+
getattr(meta, "qualified_table_name", None),
|
|
329
|
+
getattr(meta, "table_name", None),
|
|
330
|
+
)
|
|
331
|
+
if name
|
|
332
|
+
}
|
|
333
|
+
if token in names or bare_token in names:
|
|
334
|
+
return model_cls
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
149
338
|
def is_strict_table(model_cls: type) -> bool:
|
|
150
339
|
"""True if ``model_cls``'s table is registered ``strict`` (execute_write-guarded).
|
|
151
340
|
|
|
@@ -235,6 +424,13 @@ def _apply_wrapping(model_cls: type, mode: str) -> None:
|
|
|
235
424
|
# Register for the universal execute_write guard so update/delete/
|
|
236
425
|
# QueryBuilder/raw-SQL paths are enforced too, not just create/bulk_*.
|
|
237
426
|
_register_strict_table(model_cls)
|
|
427
|
+
elif mode == "coordinator":
|
|
428
|
+
required_scope = getattr(
|
|
429
|
+
getattr(model_cls, "_meta", None),
|
|
430
|
+
"managed_write_scope",
|
|
431
|
+
"coordinator",
|
|
432
|
+
)
|
|
433
|
+
_register_coordinator_table(model_cls, str(required_scope))
|
|
238
434
|
for method_name in _WRITE_METHODS:
|
|
239
435
|
original = getattr(model_cls, method_name, None)
|
|
240
436
|
if original is None:
|
|
@@ -251,6 +447,7 @@ def declare_managed(
|
|
|
251
447
|
model_cls: type,
|
|
252
448
|
*,
|
|
253
449
|
mode: str = "elevate",
|
|
450
|
+
write_scope: str = "coordinator",
|
|
254
451
|
watched_lifecycle: dict | None = None,
|
|
255
452
|
) -> None:
|
|
256
453
|
"""Programmatic equivalent of declaring ``Meta.managed_writes`` /
|
|
@@ -281,15 +478,19 @@ def declare_managed(
|
|
|
281
478
|
|
|
282
479
|
Args:
|
|
283
480
|
model_cls: a matrx-orm Model class.
|
|
284
|
-
mode: ``"elevate"`` (
|
|
285
|
-
|
|
286
|
-
|
|
481
|
+
mode: ``"elevate"`` (auto-open a one-shot), ``"strict"`` (require
|
|
482
|
+
any governed Session), or ``"coordinator"`` (require the named
|
|
483
|
+
owner Session and reject ordinary Session-of-one writes).
|
|
484
|
+
write_scope: authority required by coordinator mode.
|
|
287
485
|
watched_lifecycle: optional dict matching the
|
|
288
486
|
:class:`WatchedLifecycleConfig` fields (``predicate``,
|
|
289
487
|
``max_age_secs``, ``transition_to_on_timeout``, etc.).
|
|
290
488
|
"""
|
|
291
|
-
if mode not in ("strict", "elevate"):
|
|
292
|
-
raise ValueError(
|
|
489
|
+
if mode not in ("strict", "elevate", "coordinator"):
|
|
490
|
+
raise ValueError(
|
|
491
|
+
"declare_managed: mode must be 'strict', 'elevate', or "
|
|
492
|
+
f"'coordinator', got {mode!r}"
|
|
493
|
+
)
|
|
293
494
|
meta = getattr(model_cls, "_meta", None)
|
|
294
495
|
if meta is None:
|
|
295
496
|
raise ValueError(
|
|
@@ -304,6 +505,10 @@ def declare_managed(
|
|
|
304
505
|
object.__setattr__(meta, "managed_writes", mode)
|
|
305
506
|
|
|
306
507
|
_apply_wrapping(model_cls, mode)
|
|
508
|
+
if mode == "coordinator":
|
|
509
|
+
if not write_scope.strip():
|
|
510
|
+
raise ValueError("declare_managed coordinator mode requires write_scope")
|
|
511
|
+
_register_coordinator_table(model_cls, write_scope.strip())
|
|
307
512
|
|
|
308
513
|
if watched_lifecycle is not None:
|
|
309
514
|
try:
|
|
@@ -337,7 +542,7 @@ def _managed_mode(model_cls: type) -> str | None:
|
|
|
337
542
|
return None
|
|
338
543
|
if declared is True:
|
|
339
544
|
return "elevate"
|
|
340
|
-
if isinstance(declared, str) and declared in ("strict", "elevate"):
|
|
545
|
+
if isinstance(declared, str) and declared in ("strict", "elevate", "coordinator"):
|
|
341
546
|
return declared
|
|
342
547
|
logger.warning(
|
|
343
548
|
"managed_writes on %s must be True | 'strict' | 'elevate' — got %r. Treating as 'elevate'.",
|
|
@@ -358,6 +563,7 @@ def _wrap_classmethod(model_cls: type, method_name: str, original: Any, mode: st
|
|
|
358
563
|
|
|
359
564
|
@functools.wraps(original_func)
|
|
360
565
|
async def wrapped(cls, *args, **kwargs):
|
|
566
|
+
active_mode = _managed_mode(cls) or mode
|
|
361
567
|
# Reentrancy guard: when the flush executor calls bulk_create /
|
|
362
568
|
# bulk_update / bulk_delete to actually write rows, those calls
|
|
363
569
|
# must pass through unchanged. Without this check we'd recurse
|
|
@@ -381,6 +587,8 @@ def _wrap_classmethod(model_cls: type, method_name: str, original: Any, mode: st
|
|
|
381
587
|
session = candidate
|
|
382
588
|
break
|
|
383
589
|
if session is not None:
|
|
590
|
+
if active_mode == "coordinator":
|
|
591
|
+
assert_model_write_allowed(cls, method_name, session=session)
|
|
384
592
|
# Defer to the active Session.
|
|
385
593
|
return await _defer_to_session(
|
|
386
594
|
session,
|
|
@@ -392,7 +600,10 @@ def _wrap_classmethod(model_cls: type, method_name: str, original: Any, mode: st
|
|
|
392
600
|
)
|
|
393
601
|
|
|
394
602
|
# No active Session.
|
|
395
|
-
if
|
|
603
|
+
if active_mode == "coordinator":
|
|
604
|
+
assert_model_write_allowed(cls, method_name)
|
|
605
|
+
|
|
606
|
+
if active_mode == "strict":
|
|
396
607
|
banner = _format_violation_banner(cls.__name__, method_name)
|
|
397
608
|
vcprint(banner, color="red")
|
|
398
609
|
logger.error(
|
|
@@ -565,6 +776,33 @@ def _format_chokepoint_banner(table: str) -> str:
|
|
|
565
776
|
)
|
|
566
777
|
|
|
567
778
|
|
|
779
|
+
def _format_coordinator_banner(
|
|
780
|
+
model_name: str,
|
|
781
|
+
op_kind: str,
|
|
782
|
+
required_scope: str,
|
|
783
|
+
actual_scope: str | None,
|
|
784
|
+
) -> str:
|
|
785
|
+
return (
|
|
786
|
+
"\n"
|
|
787
|
+
"████████████████████████████████████████████████████████████████████\n"
|
|
788
|
+
"██ COORDINATOR OWNERSHIP BYPASS — WRITE CONTINUED ██\n"
|
|
789
|
+
"████████████████████████████████████████████████████████████████████\n"
|
|
790
|
+
f" Model : {model_name}\n"
|
|
791
|
+
f" Operation : {op_kind}\n"
|
|
792
|
+
f" Required : Session(write_scope={required_scope!r})\n"
|
|
793
|
+
f" Actual : Session(write_scope={actual_scope!r})\n"
|
|
794
|
+
" Result : the write was NOT blocked, but bypassed its owner.\n"
|
|
795
|
+
" Fix : queue this write through the registered Coordinator.\n"
|
|
796
|
+
" Override : allow_direct_coordinator_write(\n"
|
|
797
|
+
" Model, reason='...',\n"
|
|
798
|
+
" acknowledgement=\n"
|
|
799
|
+
f" {COORDINATOR_BYPASS_ACKNOWLEDGEMENT!r},\n"
|
|
800
|
+
" )\n"
|
|
801
|
+
" is reserved for intentional out-of-request writes.\n"
|
|
802
|
+
"████████████████████████████████████████████████████████████████████\n"
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
568
806
|
def _format_violation_banner(model_name: str, op_kind: str) -> str:
|
|
569
807
|
return (
|
|
570
808
|
"\n"
|