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