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