omnibase_infra 0.2.6__py3-none-any.whl
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.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +132 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +111 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_mode.py +86 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +160 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +95 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +105 -0
- omnibase_infra/errors/error_infra.py +610 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +115 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +82 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +795 -0
- omnibase_infra/handlers/handler_db.py +1046 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +2015 -0
- omnibase_infra/handlers/handler_http.py +926 -0
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +1430 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +428 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +47 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +144 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +330 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +155 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +80 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +68 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +190 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +49 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +296 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +57 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +167 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
- omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
- omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +37 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +449 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +104 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +445 -0
- omnibase_infra/runtime/binding_config_resolver.py +2771 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_handler_discovery.py +587 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +750 -0
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
- omnibase_infra/runtime/handler_registry.py +329 -0
- omnibase_infra/runtime/handler_source_resolver.py +367 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +402 -0
- omnibase_infra/runtime/mixins/__init__.py +24 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
- omnibase_infra/runtime/models/__init__.py +229 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +229 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1330 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +37 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1185 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2112 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1651 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
- omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +765 -0
- omnibase_infra/runtime/util_container_wiring.py +1124 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +723 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +89 -0
- omnibase_infra/services/corpus_capture.py +684 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +898 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +699 -0
- omnibase_infra/services/service_timeout_scanner.py +394 -0
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +137 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +29 -0
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +117 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1514 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +2033 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +513 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.6.dist-info/METADATA +197 -0
- omnibase_infra-0.2.6.dist-info/RECORD +833 -0
- omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
- omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Generic Contract-Driven Projector Shell.
|
|
4
|
+
|
|
5
|
+
Implements ProtocolEventProjector for contract-based event-to-state projection.
|
|
6
|
+
All behavior is driven by ModelProjectorContract - NO domain-specific logic.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Event type matching via envelope metadata, payload attribute, or classname
|
|
10
|
+
- Dynamic column value extraction from nested event payloads
|
|
11
|
+
- Three projection modes: upsert, insert_only, append
|
|
12
|
+
- Parameterized SQL queries for injection protection
|
|
13
|
+
- Bulk state queries for N+1 optimization
|
|
14
|
+
- Configurable query timeouts
|
|
15
|
+
- Optional state transition notification publishing
|
|
16
|
+
|
|
17
|
+
Notification Publishing:
|
|
18
|
+
When configured with a notification_publisher and notification_config,
|
|
19
|
+
ProjectorShell will automatically publish state transition notifications
|
|
20
|
+
after successful projection commits. This enables the Observer pattern
|
|
21
|
+
for orchestrator coordination without tight coupling to reducers.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> from omnibase_infra.runtime import ProjectorShell, TransitionNotificationPublisher
|
|
25
|
+
>>> from omnibase_infra.runtime.models import ModelProjectorNotificationConfig
|
|
26
|
+
>>>
|
|
27
|
+
>>> publisher = TransitionNotificationPublisher(event_bus, topic="transitions.v1")
|
|
28
|
+
>>> config = ModelProjectorNotificationConfig(
|
|
29
|
+
... topic="transitions.v1",
|
|
30
|
+
... state_column="current_state",
|
|
31
|
+
... aggregate_id_column="entity_id",
|
|
32
|
+
... version_column="version",
|
|
33
|
+
... )
|
|
34
|
+
>>> projector = ProjectorShell(
|
|
35
|
+
... contract=contract,
|
|
36
|
+
... pool=pool,
|
|
37
|
+
... notification_publisher=publisher,
|
|
38
|
+
... notification_config=config,
|
|
39
|
+
... )
|
|
40
|
+
|
|
41
|
+
See Also:
|
|
42
|
+
- ProtocolEventProjector: Protocol definition from omnibase_infra.protocols
|
|
43
|
+
- ModelProjectorContract: Contract model from omnibase_core
|
|
44
|
+
- ProjectorPluginLoader: Loader that instantiates ProjectorShell
|
|
45
|
+
- TransitionNotificationPublisher: Publisher for state transition notifications
|
|
46
|
+
|
|
47
|
+
Related Tickets:
|
|
48
|
+
- OMN-1169: ProjectorShell contract-driven projections (implemented)
|
|
49
|
+
- OMN-1139: TransitionNotificationPublisher integration (implemented)
|
|
50
|
+
|
|
51
|
+
.. versionadded:: 0.7.0
|
|
52
|
+
Created as part of OMN-1169 projector shell implementation.
|
|
53
|
+
|
|
54
|
+
.. versionchanged:: 0.8.0
|
|
55
|
+
Added notification publishing support as part of OMN-1139.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import logging
|
|
61
|
+
from typing import TYPE_CHECKING
|
|
62
|
+
from uuid import UUID
|
|
63
|
+
|
|
64
|
+
import asyncpg
|
|
65
|
+
from pydantic import BaseModel
|
|
66
|
+
|
|
67
|
+
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
68
|
+
from omnibase_core.models.projectors import (
|
|
69
|
+
ModelProjectionResult,
|
|
70
|
+
ModelProjectorContract,
|
|
71
|
+
)
|
|
72
|
+
from omnibase_core.protocols.notifications import (
|
|
73
|
+
ProtocolTransitionNotificationPublisher,
|
|
74
|
+
)
|
|
75
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
76
|
+
from omnibase_infra.errors import (
|
|
77
|
+
InfraConnectionError,
|
|
78
|
+
InfraTimeoutError,
|
|
79
|
+
ModelInfraErrorContext,
|
|
80
|
+
ModelTimeoutErrorContext,
|
|
81
|
+
ProtocolConfigurationError,
|
|
82
|
+
RuntimeHostError,
|
|
83
|
+
)
|
|
84
|
+
from omnibase_infra.models.projectors.util_sql_identifiers import quote_identifier
|
|
85
|
+
from omnibase_infra.runtime.mixins import MixinProjectorSqlOperations
|
|
86
|
+
from omnibase_infra.runtime.mixins.mixin_projector_notification_publishing import (
|
|
87
|
+
MixinProjectorNotificationPublishing,
|
|
88
|
+
)
|
|
89
|
+
from omnibase_infra.runtime.models.model_projector_notification_config import (
|
|
90
|
+
ModelProjectorNotificationConfig,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
logger = logging.getLogger(__name__)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProjectorShell(MixinProjectorNotificationPublishing, MixinProjectorSqlOperations):
|
|
97
|
+
"""Generic contract-driven projector implementation.
|
|
98
|
+
|
|
99
|
+
Transforms events into persistent state projections based on a
|
|
100
|
+
ModelProjectorContract definition. All behavior is declarative -
|
|
101
|
+
no domain-specific logic in this class.
|
|
102
|
+
|
|
103
|
+
The projector supports three projection modes:
|
|
104
|
+
- upsert: INSERT or UPDATE based on upsert_key (default)
|
|
105
|
+
- insert_only: INSERT only, fail on conflict
|
|
106
|
+
- append: Always INSERT, event-log style
|
|
107
|
+
|
|
108
|
+
Notification Publishing:
|
|
109
|
+
When configured with a notification_publisher and notification_config,
|
|
110
|
+
the projector will automatically publish state transition notifications
|
|
111
|
+
after successful projection commits. The notification includes:
|
|
112
|
+
|
|
113
|
+
- aggregate_type: From the projector contract
|
|
114
|
+
- aggregate_id: Extracted from projection values
|
|
115
|
+
- from_state: Previous state (fetched before projection)
|
|
116
|
+
- to_state: New state (extracted from projection values)
|
|
117
|
+
- projection_version: Version from projection values (if configured)
|
|
118
|
+
- correlation_id: Propagated from the incoming event
|
|
119
|
+
- causation_id: The envelope_id of the triggering event
|
|
120
|
+
|
|
121
|
+
Notifications are best-effort - publishing failures are logged but
|
|
122
|
+
do not cause the projection to fail.
|
|
123
|
+
|
|
124
|
+
UniqueViolationError Handling:
|
|
125
|
+
The projector handles ``asyncpg.UniqueViolationError`` differently based
|
|
126
|
+
on the projection mode. Understanding these semantics is critical for
|
|
127
|
+
correct schema design and error handling.
|
|
128
|
+
|
|
129
|
+
**insert_only mode**:
|
|
130
|
+
A unique violation indicates duplicate event processing (idempotency).
|
|
131
|
+
This is expected behavior when replaying events or when at-least-once
|
|
132
|
+
delivery causes duplicates. Returns ``ModelProjectionResult(success=False)``
|
|
133
|
+
with an error message. The caller can decide whether to log, retry,
|
|
134
|
+
or ignore based on their requirements.
|
|
135
|
+
|
|
136
|
+
**upsert mode**:
|
|
137
|
+
Unique violations should NEVER occur because the generated SQL uses
|
|
138
|
+
``ON CONFLICT ... DO UPDATE``. If a violation is raised, it indicates
|
|
139
|
+
a schema mismatch (e.g., the ``upsert_key`` in the contract doesn't
|
|
140
|
+
match the actual unique constraint). Raises ``RuntimeHostError`` to
|
|
141
|
+
signal a configuration error that needs investigation.
|
|
142
|
+
|
|
143
|
+
**append mode**:
|
|
144
|
+
This mode assumes **event-driven primary keys** where each row is
|
|
145
|
+
uniquely identified by event-specific data (e.g., ``envelope_id``,
|
|
146
|
+
``event_sequence``, or composite keys including timestamp). With this
|
|
147
|
+
assumption, a unique violation indicates duplicate event processing -
|
|
148
|
+
the same event was projected twice.
|
|
149
|
+
|
|
150
|
+
**IMPORTANT ASSUMPTION**: Append mode primary key design must ensure
|
|
151
|
+
that each event produces a unique key. Common patterns:
|
|
152
|
+
- ``envelope_id`` (UUID from event envelope)
|
|
153
|
+
- ``(aggregate_id, event_sequence)`` composite key
|
|
154
|
+
- ``(aggregate_id, event_type, timestamp)`` composite key
|
|
155
|
+
|
|
156
|
+
If your schema uses **non-event primary keys** (e.g., auto-increment,
|
|
157
|
+
domain-specific business keys), a ``UniqueViolationError`` in append
|
|
158
|
+
mode may indicate a **legitimate conflict** rather than a duplicate
|
|
159
|
+
event. In such cases, you should either:
|
|
160
|
+
1. Switch to ``upsert`` mode with appropriate conflict resolution
|
|
161
|
+
2. Redesign the primary key to be event-driven
|
|
162
|
+
3. Implement custom projection logic outside ProjectorShell
|
|
163
|
+
|
|
164
|
+
When a violation occurs in append mode, ``RuntimeHostError`` is raised
|
|
165
|
+
to fail fast and signal the need for investigation.
|
|
166
|
+
|
|
167
|
+
Composite Primary Key Handling:
|
|
168
|
+
The contract model (``ModelProjectorContract``) only supports single-column
|
|
169
|
+
``primary_key`` and ``upsert_key`` values (type: ``str``). For schemas with
|
|
170
|
+
composite primary keys, there are two approaches:
|
|
171
|
+
|
|
172
|
+
**Approach 1: Add UNIQUE constraint (Recommended)**
|
|
173
|
+
Add a UNIQUE constraint on the single column specified in the contract.
|
|
174
|
+
This allows ``ON CONFLICT (column)`` to work even with composite PKs.
|
|
175
|
+
|
|
176
|
+
Example SQL schema::
|
|
177
|
+
|
|
178
|
+
PRIMARY KEY (entity_id, domain),
|
|
179
|
+
UNIQUE (entity_id) -- Enables ON CONFLICT (entity_id)
|
|
180
|
+
|
|
181
|
+
**Approach 2: Use upsert_partial() with explicit conflict_columns**
|
|
182
|
+
For operations requiring composite key semantics, use ``upsert_partial()``
|
|
183
|
+
with the ``conflict_columns`` parameter instead of ``project()``.
|
|
184
|
+
|
|
185
|
+
Example::
|
|
186
|
+
|
|
187
|
+
await projector.upsert_partial(
|
|
188
|
+
aggregate_id=entity_id,
|
|
189
|
+
values={"entity_id": entity_id, "domain": domain, ...},
|
|
190
|
+
correlation_id=correlation_id,
|
|
191
|
+
conflict_columns=["entity_id", "domain"], # Composite key
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
**get_state() / get_states() behavior with composite keys**:
|
|
195
|
+
These methods use only the first column of the primary key for lookups.
|
|
196
|
+
For schemas where the single-column lookup is not unique, the results
|
|
197
|
+
may be ambiguous. Consider querying the database directly in such cases.
|
|
198
|
+
|
|
199
|
+
Thread Safety:
|
|
200
|
+
This implementation is coroutine-safe for concurrent async calls.
|
|
201
|
+
Uses asyncpg connection pool for connection management.
|
|
202
|
+
|
|
203
|
+
Security:
|
|
204
|
+
All queries use parameterized statements for SQL injection protection.
|
|
205
|
+
Table and column names are validated by the contract model validators
|
|
206
|
+
and quoted using ``quote_identifier()`` for safe SQL generation.
|
|
207
|
+
|
|
208
|
+
Query Timeout:
|
|
209
|
+
Configurable via ``query_timeout_seconds`` parameter. Defaults to 30
|
|
210
|
+
seconds. Set to None to disable timeout (not recommended for production).
|
|
211
|
+
|
|
212
|
+
Default Values:
|
|
213
|
+
Column defaults specified in the contract (``column.default``) are treated
|
|
214
|
+
as **runtime literal values**, not SQL expressions. They are inserted as
|
|
215
|
+
parameter values, not embedded in SQL. For database-level defaults (e.g.,
|
|
216
|
+
``CURRENT_TIMESTAMP``), use PostgreSQL column defaults instead.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> from omnibase_core.models.projectors import ModelProjectorContract
|
|
220
|
+
>>> contract = ModelProjectorContract.model_validate(yaml_data)
|
|
221
|
+
>>> pool = await asyncpg.create_pool(dsn)
|
|
222
|
+
>>> projector = ProjectorShell(contract, pool, query_timeout_seconds=10.0)
|
|
223
|
+
>>> result = await projector.project(event_envelope, correlation_id)
|
|
224
|
+
>>> if result.success:
|
|
225
|
+
... print(f"Projected {result.rows_affected} rows")
|
|
226
|
+
|
|
227
|
+
Related:
|
|
228
|
+
- OMN-1169: ProjectorShell contract-driven projections (implemented)
|
|
229
|
+
- OMN-1168: ProjectorPluginLoader contract discovery
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
# Default query timeout in seconds (30s is reasonable for projections)
|
|
233
|
+
DEFAULT_QUERY_TIMEOUT_SECONDS: float = 30.0
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
contract: ModelProjectorContract,
|
|
238
|
+
pool: asyncpg.Pool,
|
|
239
|
+
query_timeout_seconds: float | None = None,
|
|
240
|
+
notification_publisher: ProtocolTransitionNotificationPublisher | None = None,
|
|
241
|
+
notification_config: ModelProjectorNotificationConfig | None = None,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Initialize projector shell with contract and database pool.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
contract: The projector contract defining projection behavior.
|
|
247
|
+
All projection rules (table, columns, modes) come from this.
|
|
248
|
+
pool: asyncpg connection pool for database access.
|
|
249
|
+
Pool should be created by the caller (e.g., from container).
|
|
250
|
+
query_timeout_seconds: Timeout for individual database queries in
|
|
251
|
+
seconds. Defaults to 30.0 seconds. Set to None to disable
|
|
252
|
+
timeout (not recommended for production).
|
|
253
|
+
notification_publisher: Optional publisher for state transition
|
|
254
|
+
notifications. If provided along with notification_config,
|
|
255
|
+
notifications will be published after successful projections.
|
|
256
|
+
notification_config: Optional configuration for notification
|
|
257
|
+
publishing. Specifies which columns contain state, aggregate ID,
|
|
258
|
+
and version information for notification creation.
|
|
259
|
+
|
|
260
|
+
Note:
|
|
261
|
+
Both notification_publisher and notification_config must be provided
|
|
262
|
+
for notification publishing to be enabled. If only one is provided,
|
|
263
|
+
notifications will not be published (silent no-op).
|
|
264
|
+
|
|
265
|
+
**Topic Consistency**: When both notification_publisher and
|
|
266
|
+
notification_config are provided, be aware that:
|
|
267
|
+
|
|
268
|
+
- The ``notification_config.expected_topic`` field is for
|
|
269
|
+
**documentation and validation purposes only**
|
|
270
|
+
- The **actual topic** used for publishing is determined solely by
|
|
271
|
+
the ``notification_publisher``'s internal configuration (the topic
|
|
272
|
+
passed to ``TransitionNotificationPublisher.__init__``)
|
|
273
|
+
- A **warning is logged** if these values differ to catch configuration
|
|
274
|
+
errors early
|
|
275
|
+
- **Users should ensure these match** when configuring both components
|
|
276
|
+
|
|
277
|
+
Example of consistent configuration::
|
|
278
|
+
|
|
279
|
+
# Publisher determines actual destination
|
|
280
|
+
publisher = TransitionNotificationPublisher(event_bus, "transitions.v1")
|
|
281
|
+
|
|
282
|
+
# Config expected_topic should match for consistency validation
|
|
283
|
+
config = ModelProjectorNotificationConfig(
|
|
284
|
+
expected_topic="transitions.v1", # Match publisher's topic
|
|
285
|
+
state_column="current_state",
|
|
286
|
+
...
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
>>> from omnibase_infra.runtime import TransitionNotificationPublisher
|
|
291
|
+
>>> from omnibase_infra.runtime.models import ModelProjectorNotificationConfig
|
|
292
|
+
>>>
|
|
293
|
+
>>> publisher = TransitionNotificationPublisher(event_bus, "transitions.v1")
|
|
294
|
+
>>> config = ModelProjectorNotificationConfig(
|
|
295
|
+
... expected_topic="transitions.v1",
|
|
296
|
+
... state_column="current_state",
|
|
297
|
+
... aggregate_id_column="entity_id",
|
|
298
|
+
... version_column="version",
|
|
299
|
+
... )
|
|
300
|
+
>>> projector = ProjectorShell(
|
|
301
|
+
... contract=contract,
|
|
302
|
+
... pool=pool,
|
|
303
|
+
... notification_publisher=publisher,
|
|
304
|
+
... notification_config=config,
|
|
305
|
+
... )
|
|
306
|
+
"""
|
|
307
|
+
self._contract = contract
|
|
308
|
+
self._pool = pool
|
|
309
|
+
self._query_timeout = (
|
|
310
|
+
query_timeout_seconds
|
|
311
|
+
if query_timeout_seconds is not None
|
|
312
|
+
else self.DEFAULT_QUERY_TIMEOUT_SECONDS
|
|
313
|
+
)
|
|
314
|
+
self._notification_publisher = notification_publisher
|
|
315
|
+
self._notification_config = notification_config
|
|
316
|
+
|
|
317
|
+
# Validate notification config against contract schema if provided
|
|
318
|
+
if notification_config is not None:
|
|
319
|
+
self._validate_notification_config(notification_config)
|
|
320
|
+
|
|
321
|
+
# Warn if notification config expected_topic differs from publisher topic
|
|
322
|
+
# The config.expected_topic is for validation only; publisher determines destination
|
|
323
|
+
if (
|
|
324
|
+
notification_config is not None
|
|
325
|
+
and notification_publisher is not None
|
|
326
|
+
and hasattr(notification_publisher, "topic")
|
|
327
|
+
and notification_config.expected_topic != notification_publisher.topic
|
|
328
|
+
):
|
|
329
|
+
logger.warning(
|
|
330
|
+
"Notification config expected_topic differs from publisher topic - "
|
|
331
|
+
"expected_topic is for validation only, publisher determines actual destination",
|
|
332
|
+
extra={
|
|
333
|
+
"projector_id": contract.projector_id,
|
|
334
|
+
"config_expected_topic": notification_config.expected_topic,
|
|
335
|
+
"publisher_topic": notification_publisher.topic,
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
logger.debug(
|
|
340
|
+
"ProjectorShell initialized for projector '%s'",
|
|
341
|
+
contract.projector_id,
|
|
342
|
+
extra={
|
|
343
|
+
"projector_id": contract.projector_id,
|
|
344
|
+
"aggregate_type": contract.aggregate_type,
|
|
345
|
+
"consumed_events": contract.consumed_events,
|
|
346
|
+
"mode": contract.behavior.mode,
|
|
347
|
+
"query_timeout_seconds": self._query_timeout,
|
|
348
|
+
"notifications_enabled": self._is_notification_enabled(),
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _validate_notification_config(
|
|
353
|
+
self,
|
|
354
|
+
config: ModelProjectorNotificationConfig,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Validate notification config against the contract schema.
|
|
357
|
+
|
|
358
|
+
Ensures that the configured column names exist in the projection schema.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
config: The notification configuration to validate.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
ProtocolConfigurationError: If any configured column does not exist
|
|
365
|
+
in the projection schema.
|
|
366
|
+
"""
|
|
367
|
+
schema = self._contract.projection_schema
|
|
368
|
+
column_names = {col.name for col in schema.columns}
|
|
369
|
+
|
|
370
|
+
# Check state_column
|
|
371
|
+
if config.state_column not in column_names:
|
|
372
|
+
context = ModelInfraErrorContext(
|
|
373
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
374
|
+
operation="validate_notification_config",
|
|
375
|
+
target_name=f"projector.{self._contract.projector_id}",
|
|
376
|
+
)
|
|
377
|
+
raise ProtocolConfigurationError(
|
|
378
|
+
f"notification_config.state_column '{config.state_column}' "
|
|
379
|
+
f"not found in projection schema. "
|
|
380
|
+
f"Available columns: {sorted(column_names)}",
|
|
381
|
+
context=context,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Check aggregate_id_column
|
|
385
|
+
if config.aggregate_id_column not in column_names:
|
|
386
|
+
context = ModelInfraErrorContext(
|
|
387
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
388
|
+
operation="validate_notification_config",
|
|
389
|
+
target_name=f"projector.{self._contract.projector_id}",
|
|
390
|
+
)
|
|
391
|
+
raise ProtocolConfigurationError(
|
|
392
|
+
f"notification_config.aggregate_id_column '{config.aggregate_id_column}' "
|
|
393
|
+
f"not found in projection schema. "
|
|
394
|
+
f"Available columns: {sorted(column_names)}",
|
|
395
|
+
context=context,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Check version_column if specified
|
|
399
|
+
if (
|
|
400
|
+
config.version_column is not None
|
|
401
|
+
and config.version_column not in column_names
|
|
402
|
+
):
|
|
403
|
+
context = ModelInfraErrorContext(
|
|
404
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
405
|
+
operation="validate_notification_config",
|
|
406
|
+
target_name=f"projector.{self._contract.projector_id}",
|
|
407
|
+
)
|
|
408
|
+
raise ProtocolConfigurationError(
|
|
409
|
+
f"notification_config.version_column '{config.version_column}' "
|
|
410
|
+
f"not found in projection schema. "
|
|
411
|
+
f"Available columns: {sorted(column_names)}",
|
|
412
|
+
context=context,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def projector_id(self) -> str:
|
|
417
|
+
"""Unique identifier from contract."""
|
|
418
|
+
return str(self._contract.projector_id)
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def aggregate_type(self) -> str:
|
|
422
|
+
"""Aggregate type from contract."""
|
|
423
|
+
return str(self._contract.aggregate_type)
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def consumed_events(self) -> list[str]:
|
|
427
|
+
"""Event types from contract."""
|
|
428
|
+
return list(self._contract.consumed_events)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def contract(self) -> ModelProjectorContract:
|
|
432
|
+
"""Access the underlying contract."""
|
|
433
|
+
return self._contract
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def is_placeholder(self) -> bool:
|
|
437
|
+
"""Whether this is a placeholder implementation.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
False, as this is a full implementation.
|
|
441
|
+
"""
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
async def project(
|
|
445
|
+
self,
|
|
446
|
+
event: ModelEventEnvelope[object],
|
|
447
|
+
correlation_id: UUID,
|
|
448
|
+
) -> ModelProjectionResult:
|
|
449
|
+
"""Project event to persistence store.
|
|
450
|
+
|
|
451
|
+
Transforms the event into a database row based on the contract
|
|
452
|
+
configuration. The projection mode (upsert, insert_only, append)
|
|
453
|
+
determines how conflicts are handled.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
event: The event envelope to project. The payload is accessed
|
|
457
|
+
via dot-notation paths defined in the contract columns.
|
|
458
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
ModelProjectionResult with success status and rows affected.
|
|
462
|
+
Returns skipped=True if event type is not in consumed_events.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
InfraConnectionError: If database connection fails.
|
|
466
|
+
InfraTimeoutError: If projection times out.
|
|
467
|
+
RuntimeHostError: For other database errors.
|
|
468
|
+
"""
|
|
469
|
+
ctx = ModelInfraErrorContext(
|
|
470
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
471
|
+
operation="project",
|
|
472
|
+
target_name=f"projector.{self.projector_id}",
|
|
473
|
+
correlation_id=correlation_id,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Get event type from envelope
|
|
477
|
+
event_type = self._get_event_type(event)
|
|
478
|
+
|
|
479
|
+
# Check if this projector consumes this event type
|
|
480
|
+
if event_type not in self._contract.consumed_events:
|
|
481
|
+
logger.debug(
|
|
482
|
+
"Skipping event type '%s' not in consumed_events",
|
|
483
|
+
event_type,
|
|
484
|
+
extra={
|
|
485
|
+
"projector_id": self.projector_id,
|
|
486
|
+
"event_type": event_type,
|
|
487
|
+
"consumed_events": self._contract.consumed_events,
|
|
488
|
+
"correlation_id": str(correlation_id),
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
return ModelProjectionResult(success=True, skipped=True, rows_affected=0)
|
|
492
|
+
|
|
493
|
+
# Extract column values from event
|
|
494
|
+
values = self._extract_values(event, event_type)
|
|
495
|
+
|
|
496
|
+
# Extract notification-related values before projection
|
|
497
|
+
# These are needed for both fetching previous state and publishing notification
|
|
498
|
+
aggregate_id_for_notification = self._extract_aggregate_id_from_values(values)
|
|
499
|
+
from_state: str | None = None
|
|
500
|
+
|
|
501
|
+
# Fetch previous state if notifications are enabled
|
|
502
|
+
if (
|
|
503
|
+
self._is_notification_enabled()
|
|
504
|
+
and aggregate_id_for_notification is not None
|
|
505
|
+
):
|
|
506
|
+
from_state = await self._fetch_current_state_for_notification(
|
|
507
|
+
aggregate_id_for_notification, correlation_id
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Execute projection based on mode
|
|
511
|
+
try:
|
|
512
|
+
rows_affected = await self._execute_projection(
|
|
513
|
+
values, correlation_id, event_type
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
logger.debug(
|
|
517
|
+
"Projection completed",
|
|
518
|
+
extra={
|
|
519
|
+
"projector_id": self.projector_id,
|
|
520
|
+
"event_type": event_type,
|
|
521
|
+
"rows_affected": rows_affected,
|
|
522
|
+
"correlation_id": str(correlation_id),
|
|
523
|
+
},
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Publish transition notification if configured and projection succeeded
|
|
527
|
+
if rows_affected > 0 and aggregate_id_for_notification is not None:
|
|
528
|
+
to_state = self._extract_state_from_values(values)
|
|
529
|
+
if to_state is not None:
|
|
530
|
+
projection_version = self._extract_version_from_values(values)
|
|
531
|
+
await self._publish_transition_notification(
|
|
532
|
+
event=event,
|
|
533
|
+
from_state=from_state,
|
|
534
|
+
to_state=to_state,
|
|
535
|
+
projection_version=projection_version,
|
|
536
|
+
aggregate_id=aggregate_id_for_notification,
|
|
537
|
+
correlation_id=correlation_id,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
return ModelProjectionResult(
|
|
541
|
+
success=True,
|
|
542
|
+
skipped=False,
|
|
543
|
+
rows_affected=rows_affected,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
except asyncpg.PostgresConnectionError as e:
|
|
547
|
+
raise InfraConnectionError(
|
|
548
|
+
f"Failed to connect to database for projection: {self.projector_id}",
|
|
549
|
+
context=ctx,
|
|
550
|
+
) from e
|
|
551
|
+
|
|
552
|
+
except asyncpg.QueryCanceledError as e:
|
|
553
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
554
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
555
|
+
operation="project",
|
|
556
|
+
target_name=f"projector.{self.projector_id}",
|
|
557
|
+
correlation_id=correlation_id,
|
|
558
|
+
# timeout_seconds omitted - database timeout is connection-pool level, not available here
|
|
559
|
+
)
|
|
560
|
+
raise InfraTimeoutError(
|
|
561
|
+
f"Projection timed out for: {self.projector_id}",
|
|
562
|
+
context=timeout_ctx,
|
|
563
|
+
) from e
|
|
564
|
+
|
|
565
|
+
except asyncpg.UniqueViolationError as e:
|
|
566
|
+
# ============================================================
|
|
567
|
+
# UniqueViolationError Handling by Projection Mode
|
|
568
|
+
# ============================================================
|
|
569
|
+
#
|
|
570
|
+
# Different projection modes have different semantics for unique
|
|
571
|
+
# constraint violations. See class docstring for full documentation.
|
|
572
|
+
#
|
|
573
|
+
# insert_only mode:
|
|
574
|
+
# - EXPECTED: Duplicate events (idempotency, at-least-once delivery)
|
|
575
|
+
# - BEHAVIOR: Return failure result, let caller decide how to handle
|
|
576
|
+
# - RATIONALE: insert_only is designed for idempotent projections
|
|
577
|
+
# where duplicates are tolerated but should be reported
|
|
578
|
+
#
|
|
579
|
+
# upsert mode:
|
|
580
|
+
# - UNEXPECTED: Should NEVER occur (ON CONFLICT handles duplicates)
|
|
581
|
+
# - BEHAVIOR: Raise RuntimeHostError
|
|
582
|
+
# - RATIONALE: If we get here, the upsert_key in the contract doesn't
|
|
583
|
+
# match the actual unique constraint in the database schema
|
|
584
|
+
# - ACTION REQUIRED: Verify contract.upsert_key matches DB constraint
|
|
585
|
+
#
|
|
586
|
+
# append mode:
|
|
587
|
+
# - UNEXPECTED: Assumes event-driven primary keys (envelope_id, etc.)
|
|
588
|
+
# - BEHAVIOR: Raise RuntimeHostError
|
|
589
|
+
# - RATIONALE: With event-driven PKs, duplicates indicate either:
|
|
590
|
+
# (a) Same event processed twice (infrastructure issue), or
|
|
591
|
+
# (b) Schema uses non-event PKs (design mismatch)
|
|
592
|
+
# - ACTION REQUIRED:
|
|
593
|
+
# * If (a): Investigate event delivery/replay logic
|
|
594
|
+
# * If (b): Consider switching to upsert mode or redesigning PK
|
|
595
|
+
#
|
|
596
|
+
# IMPORTANT ASSUMPTION for append mode:
|
|
597
|
+
# The primary key MUST be derived from event data (e.g., envelope_id,
|
|
598
|
+
# event_sequence) such that each unique event produces a unique key.
|
|
599
|
+
# If using auto-increment or business keys, append mode violations
|
|
600
|
+
# may indicate legitimate conflicts, not duplicates.
|
|
601
|
+
#
|
|
602
|
+
# ============================================================
|
|
603
|
+
|
|
604
|
+
if self._contract.behavior.mode == "insert_only":
|
|
605
|
+
# insert_only: Expected duplicate - report as failure, don't raise
|
|
606
|
+
logger.warning(
|
|
607
|
+
"Unique constraint violation during insert_only projection "
|
|
608
|
+
"(likely duplicate event - expected for idempotent processing)",
|
|
609
|
+
extra={
|
|
610
|
+
"projector_id": self.projector_id,
|
|
611
|
+
"event_type": event_type,
|
|
612
|
+
"correlation_id": str(correlation_id),
|
|
613
|
+
"mode": "insert_only",
|
|
614
|
+
"hint": "This is expected behavior for at-least-once delivery",
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
return ModelProjectionResult(
|
|
618
|
+
success=False,
|
|
619
|
+
skipped=False,
|
|
620
|
+
rows_affected=0,
|
|
621
|
+
error="Unique constraint violation: duplicate key for insert_only mode",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# upsert/append modes: Unexpected violation - fail fast with error
|
|
625
|
+
# For upsert: indicates contract.upsert_key doesn't match DB constraint
|
|
626
|
+
# For append: indicates either duplicate event or non-event-driven PK
|
|
627
|
+
mode = self._contract.behavior.mode
|
|
628
|
+
if mode == "upsert":
|
|
629
|
+
hint = (
|
|
630
|
+
"Verify that contract.behavior.upsert_key matches the actual "
|
|
631
|
+
"unique constraint in the database schema"
|
|
632
|
+
)
|
|
633
|
+
else: # append mode
|
|
634
|
+
hint = (
|
|
635
|
+
"Append mode assumes event-driven primary keys (e.g., envelope_id). "
|
|
636
|
+
"If using non-event PKs, consider switching to upsert mode or "
|
|
637
|
+
"redesigning the primary key to be event-derived"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
logger.exception(
|
|
641
|
+
"Unexpected unique constraint violation in %s mode",
|
|
642
|
+
mode,
|
|
643
|
+
extra={
|
|
644
|
+
"projector_id": self.projector_id,
|
|
645
|
+
"event_type": event_type,
|
|
646
|
+
"correlation_id": str(correlation_id),
|
|
647
|
+
"mode": mode,
|
|
648
|
+
"hint": hint,
|
|
649
|
+
},
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
raise RuntimeHostError(
|
|
653
|
+
f"Unexpected unique constraint violation in {mode} mode "
|
|
654
|
+
f"for projector: {self.projector_id}. {hint}",
|
|
655
|
+
context=ctx,
|
|
656
|
+
) from e
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
raise RuntimeHostError(
|
|
660
|
+
f"Failed to execute projection: {type(e).__name__}",
|
|
661
|
+
context=ctx,
|
|
662
|
+
) from e
|
|
663
|
+
|
|
664
|
+
async def get_state(
|
|
665
|
+
self,
|
|
666
|
+
aggregate_id: UUID,
|
|
667
|
+
correlation_id: UUID,
|
|
668
|
+
) -> dict[str, object] | None:
|
|
669
|
+
"""Get current projected state for an aggregate.
|
|
670
|
+
|
|
671
|
+
Queries the projection table for the current state of the
|
|
672
|
+
specified aggregate. Uses configurable query timeout.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
aggregate_id: The unique identifier of the aggregate.
|
|
676
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
Dictionary mapping column names (str) to their values if found,
|
|
680
|
+
None if no state exists. Values are typed as ``object`` because
|
|
681
|
+
asyncpg can return various PostgreSQL types (str, int, float,
|
|
682
|
+
datetime, UUID, etc.) and the schema is defined at runtime via
|
|
683
|
+
the projector contract.
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
InfraConnectionError: If database connection fails.
|
|
687
|
+
InfraTimeoutError: If query times out.
|
|
688
|
+
RuntimeHostError: For other database errors.
|
|
689
|
+
|
|
690
|
+
Note:
|
|
691
|
+
**Composite Primary Key Handling**: For schemas with composite primary
|
|
692
|
+
keys (e.g., ``(entity_id, domain)``), this method queries using only
|
|
693
|
+
the first key column. This works correctly when the first column is
|
|
694
|
+
globally unique (e.g., UUID), but may return arbitrary results if
|
|
695
|
+
multiple rows share the same first-column value. For composite key
|
|
696
|
+
queries, use a direct database query with all key columns.
|
|
697
|
+
"""
|
|
698
|
+
ctx = ModelInfraErrorContext(
|
|
699
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
700
|
+
operation="get_state",
|
|
701
|
+
target_name=f"projector.{self.projector_id}",
|
|
702
|
+
correlation_id=correlation_id,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
schema = self._contract.projection_schema
|
|
706
|
+
table_quoted = quote_identifier(schema.table)
|
|
707
|
+
# primary_key can be str | list[str] - normalize to first column for single-value queries
|
|
708
|
+
pk_column = (
|
|
709
|
+
schema.primary_key[0]
|
|
710
|
+
if isinstance(schema.primary_key, list)
|
|
711
|
+
else schema.primary_key
|
|
712
|
+
)
|
|
713
|
+
pk_quoted = quote_identifier(pk_column)
|
|
714
|
+
|
|
715
|
+
# Build SELECT query - table/column names from trusted contract
|
|
716
|
+
# S608: Safe - identifiers quoted via quote_identifier(), not user input
|
|
717
|
+
query = f"SELECT * FROM {table_quoted} WHERE {pk_quoted} = $1" # noqa: S608
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
async with self._pool.acquire() as conn:
|
|
721
|
+
row = await conn.fetchrow(
|
|
722
|
+
query, aggregate_id, timeout=self._query_timeout
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
if row is None:
|
|
726
|
+
logger.debug(
|
|
727
|
+
"No state found for aggregate",
|
|
728
|
+
extra={
|
|
729
|
+
"projector_id": self.projector_id,
|
|
730
|
+
"aggregate_id": str(aggregate_id),
|
|
731
|
+
"correlation_id": str(correlation_id),
|
|
732
|
+
},
|
|
733
|
+
)
|
|
734
|
+
return None
|
|
735
|
+
|
|
736
|
+
# Convert asyncpg Record to dict
|
|
737
|
+
result: dict[str, object] = dict(row)
|
|
738
|
+
logger.debug(
|
|
739
|
+
"State retrieved for aggregate",
|
|
740
|
+
extra={
|
|
741
|
+
"projector_id": self.projector_id,
|
|
742
|
+
"aggregate_id": str(aggregate_id),
|
|
743
|
+
"correlation_id": str(correlation_id),
|
|
744
|
+
},
|
|
745
|
+
)
|
|
746
|
+
return result
|
|
747
|
+
|
|
748
|
+
except asyncpg.PostgresConnectionError as e:
|
|
749
|
+
raise InfraConnectionError(
|
|
750
|
+
f"Failed to connect to database for state query: {self.projector_id}",
|
|
751
|
+
context=ctx,
|
|
752
|
+
) from e
|
|
753
|
+
|
|
754
|
+
except asyncpg.QueryCanceledError as e:
|
|
755
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
756
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
757
|
+
operation="get_state",
|
|
758
|
+
target_name=f"projector.{self.projector_id}",
|
|
759
|
+
correlation_id=correlation_id,
|
|
760
|
+
# timeout_seconds omitted - database timeout is connection-pool level, not available here
|
|
761
|
+
)
|
|
762
|
+
raise InfraTimeoutError(
|
|
763
|
+
f"State query timed out for: {self.projector_id}",
|
|
764
|
+
context=timeout_ctx,
|
|
765
|
+
) from e
|
|
766
|
+
|
|
767
|
+
except Exception as e:
|
|
768
|
+
raise RuntimeHostError(
|
|
769
|
+
f"Failed to get state: {type(e).__name__}",
|
|
770
|
+
context=ctx,
|
|
771
|
+
) from e
|
|
772
|
+
|
|
773
|
+
async def get_states(
|
|
774
|
+
self,
|
|
775
|
+
aggregate_ids: list[UUID],
|
|
776
|
+
correlation_id: UUID,
|
|
777
|
+
) -> dict[UUID, dict[str, object]]:
|
|
778
|
+
"""Get current projected states for multiple aggregates.
|
|
779
|
+
|
|
780
|
+
Bulk query for N+1 optimization. Fetches states for all provided
|
|
781
|
+
aggregate IDs in a single database query.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
aggregate_ids: List of unique aggregate identifiers to query.
|
|
785
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
Dictionary mapping aggregate_id (UUID) to its state (dict).
|
|
789
|
+
Aggregates with no state are omitted from the result.
|
|
790
|
+
Empty dict if no aggregate_ids provided or none found.
|
|
791
|
+
|
|
792
|
+
Raises:
|
|
793
|
+
InfraConnectionError: If database connection fails.
|
|
794
|
+
InfraTimeoutError: If query times out.
|
|
795
|
+
RuntimeHostError: For other database errors.
|
|
796
|
+
|
|
797
|
+
Note:
|
|
798
|
+
**Composite Primary Key Handling**: For schemas with composite primary
|
|
799
|
+
keys, this method queries using only the first key column. See the
|
|
800
|
+
note on ``get_state()`` for implications and alternatives.
|
|
801
|
+
|
|
802
|
+
Example:
|
|
803
|
+
>>> states = await projector.get_states(
|
|
804
|
+
... [order_id_1, order_id_2, order_id_3],
|
|
805
|
+
... correlation_id,
|
|
806
|
+
... )
|
|
807
|
+
>>> for order_id, state in states.items():
|
|
808
|
+
... print(f"Order {order_id}: {state['status']}")
|
|
809
|
+
"""
|
|
810
|
+
if not aggregate_ids:
|
|
811
|
+
return {}
|
|
812
|
+
|
|
813
|
+
ctx = ModelInfraErrorContext(
|
|
814
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
815
|
+
operation="get_states",
|
|
816
|
+
target_name=f"projector.{self.projector_id}",
|
|
817
|
+
correlation_id=correlation_id,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
schema = self._contract.projection_schema
|
|
821
|
+
table_quoted = quote_identifier(schema.table)
|
|
822
|
+
# primary_key can be str | list[str] - normalize to first column for single-value queries
|
|
823
|
+
pk_column = (
|
|
824
|
+
schema.primary_key[0]
|
|
825
|
+
if isinstance(schema.primary_key, list)
|
|
826
|
+
else schema.primary_key
|
|
827
|
+
)
|
|
828
|
+
pk_quoted = quote_identifier(pk_column)
|
|
829
|
+
|
|
830
|
+
# Build SELECT query with IN clause for bulk fetch
|
|
831
|
+
# S608: Safe - identifiers quoted via quote_identifier(), not user input
|
|
832
|
+
query = f"SELECT * FROM {table_quoted} WHERE {pk_quoted} = ANY($1)" # noqa: S608
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
async with self._pool.acquire() as conn:
|
|
836
|
+
rows = await conn.fetch(
|
|
837
|
+
query, aggregate_ids, timeout=self._query_timeout
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Build result dict keyed by aggregate ID
|
|
841
|
+
result: dict[UUID, dict[str, object]] = {}
|
|
842
|
+
for row in rows:
|
|
843
|
+
row_dict: dict[str, object] = dict(row)
|
|
844
|
+
aggregate_id = row_dict.get(pk_column)
|
|
845
|
+
if isinstance(aggregate_id, UUID):
|
|
846
|
+
result[aggregate_id] = row_dict
|
|
847
|
+
|
|
848
|
+
logger.debug(
|
|
849
|
+
"Bulk state retrieval completed",
|
|
850
|
+
extra={
|
|
851
|
+
"projector_id": self.projector_id,
|
|
852
|
+
"requested_count": len(aggregate_ids),
|
|
853
|
+
"found_count": len(result),
|
|
854
|
+
"correlation_id": str(correlation_id),
|
|
855
|
+
},
|
|
856
|
+
)
|
|
857
|
+
return result
|
|
858
|
+
|
|
859
|
+
except asyncpg.PostgresConnectionError as e:
|
|
860
|
+
raise InfraConnectionError(
|
|
861
|
+
f"Failed to connect to database for bulk state query: {self.projector_id}",
|
|
862
|
+
context=ctx,
|
|
863
|
+
) from e
|
|
864
|
+
|
|
865
|
+
except asyncpg.QueryCanceledError as e:
|
|
866
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
867
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
868
|
+
operation="get_states",
|
|
869
|
+
target_name=f"projector.{self.projector_id}",
|
|
870
|
+
correlation_id=correlation_id,
|
|
871
|
+
# timeout_seconds omitted - database timeout is connection-pool level, not available here
|
|
872
|
+
)
|
|
873
|
+
raise InfraTimeoutError(
|
|
874
|
+
f"Bulk state query timed out for: {self.projector_id}",
|
|
875
|
+
context=timeout_ctx,
|
|
876
|
+
) from e
|
|
877
|
+
|
|
878
|
+
except Exception as e:
|
|
879
|
+
raise RuntimeHostError(
|
|
880
|
+
f"Failed to get states: {type(e).__name__}",
|
|
881
|
+
context=ctx,
|
|
882
|
+
) from e
|
|
883
|
+
|
|
884
|
+
async def partial_update(
|
|
885
|
+
self,
|
|
886
|
+
aggregate_id: UUID,
|
|
887
|
+
updates: dict[str, object],
|
|
888
|
+
correlation_id: UUID,
|
|
889
|
+
) -> bool:
|
|
890
|
+
"""Perform a partial update on specific columns.
|
|
891
|
+
|
|
892
|
+
Updates only the specified columns for the row matching the aggregate_id,
|
|
893
|
+
without requiring a full event envelope or event-driven value extraction.
|
|
894
|
+
This is useful for lightweight operations like:
|
|
895
|
+
|
|
896
|
+
- Updating heartbeat timestamps (last_heartbeat_at, liveness_deadline)
|
|
897
|
+
- Setting timeout marker columns (ack_timeout_emitted_at)
|
|
898
|
+
- Updating tracking fields (updated_at)
|
|
899
|
+
|
|
900
|
+
Unlike project() which performs full event-driven projection based on
|
|
901
|
+
consumed_events and column source paths, partial_update() directly updates
|
|
902
|
+
the specified columns using the provided values.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
aggregate_id: The primary key value identifying the row to update.
|
|
906
|
+
Must match the contract's primary_key column type (typically UUID).
|
|
907
|
+
updates: Dictionary mapping column names to their new values.
|
|
908
|
+
Column names are quoted for SQL safety. Values are passed as
|
|
909
|
+
parameterized query arguments for injection protection.
|
|
910
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
True if a row was updated (entity found and modified).
|
|
914
|
+
False if no row was found matching the aggregate_id.
|
|
915
|
+
|
|
916
|
+
Raises:
|
|
917
|
+
ProtocolConfigurationError: If updates dict is empty.
|
|
918
|
+
InfraConnectionError: If database connection fails.
|
|
919
|
+
InfraTimeoutError: If update times out.
|
|
920
|
+
RuntimeHostError: For other database errors.
|
|
921
|
+
|
|
922
|
+
Security:
|
|
923
|
+
- Column names are quoted using quote_identifier() for SQL safety
|
|
924
|
+
- Values use parameterized queries ($1, $2, etc.) to prevent injection
|
|
925
|
+
- Table and primary key names come from the trusted contract definition
|
|
926
|
+
|
|
927
|
+
Example:
|
|
928
|
+
>>> from datetime import UTC, datetime, timedelta
|
|
929
|
+
>>> # Update heartbeat tracking fields
|
|
930
|
+
>>> updated = await projector.partial_update(
|
|
931
|
+
... aggregate_id=node_id,
|
|
932
|
+
... updates={
|
|
933
|
+
... "last_heartbeat_at": datetime.now(UTC),
|
|
934
|
+
... "liveness_deadline": datetime.now(UTC) + timedelta(seconds=90),
|
|
935
|
+
... "updated_at": datetime.now(UTC),
|
|
936
|
+
... },
|
|
937
|
+
... correlation_id=correlation_id,
|
|
938
|
+
... )
|
|
939
|
+
>>> if not updated:
|
|
940
|
+
... logger.warning("Entity not found for heartbeat update")
|
|
941
|
+
|
|
942
|
+
>>> # Set a timeout marker
|
|
943
|
+
>>> await projector.partial_update(
|
|
944
|
+
... aggregate_id=node_id,
|
|
945
|
+
... updates={"ack_timeout_emitted_at": datetime.now(UTC)},
|
|
946
|
+
... correlation_id=correlation_id,
|
|
947
|
+
... )
|
|
948
|
+
|
|
949
|
+
Related:
|
|
950
|
+
- OMN-1170: Converting ProjectorRegistration to declarative contracts
|
|
951
|
+
- project(): Full event-driven projection (uses event envelopes)
|
|
952
|
+
- get_state(): Read current projected state
|
|
953
|
+
"""
|
|
954
|
+
ctx = ModelInfraErrorContext(
|
|
955
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
956
|
+
operation="partial_update",
|
|
957
|
+
target_name=f"projector.{self.projector_id}",
|
|
958
|
+
correlation_id=correlation_id,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
return await self._partial_update(aggregate_id, updates, correlation_id)
|
|
963
|
+
|
|
964
|
+
except asyncpg.PostgresConnectionError as e:
|
|
965
|
+
raise InfraConnectionError(
|
|
966
|
+
f"Failed to connect to database for partial update: {self.projector_id}",
|
|
967
|
+
context=ctx,
|
|
968
|
+
) from e
|
|
969
|
+
|
|
970
|
+
except asyncpg.QueryCanceledError as e:
|
|
971
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
972
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
973
|
+
operation="partial_update",
|
|
974
|
+
target_name=f"projector.{self.projector_id}",
|
|
975
|
+
correlation_id=correlation_id,
|
|
976
|
+
# timeout_seconds omitted - database timeout is connection-pool level, not available here
|
|
977
|
+
)
|
|
978
|
+
raise InfraTimeoutError(
|
|
979
|
+
f"Partial update timed out for: {self.projector_id}",
|
|
980
|
+
context=timeout_ctx,
|
|
981
|
+
) from e
|
|
982
|
+
|
|
983
|
+
except ProtocolConfigurationError:
|
|
984
|
+
# Re-raise ProtocolConfigurationError (empty updates) as-is
|
|
985
|
+
raise
|
|
986
|
+
|
|
987
|
+
except Exception as e:
|
|
988
|
+
raise RuntimeHostError(
|
|
989
|
+
f"Failed to execute partial update: {type(e).__name__}",
|
|
990
|
+
context=ctx,
|
|
991
|
+
) from e
|
|
992
|
+
|
|
993
|
+
async def upsert_partial(
|
|
994
|
+
self,
|
|
995
|
+
aggregate_id: UUID,
|
|
996
|
+
values: dict[str, object],
|
|
997
|
+
correlation_id: UUID,
|
|
998
|
+
conflict_columns: list[str] | None = None,
|
|
999
|
+
) -> bool:
|
|
1000
|
+
"""Perform a partial UPSERT (INSERT ON CONFLICT DO UPDATE) on specific columns.
|
|
1001
|
+
|
|
1002
|
+
Inserts a new row if no row exists with the given conflict key(s), or updates
|
|
1003
|
+
only the specified columns if the row already exists. This is useful for
|
|
1004
|
+
state transition operations where:
|
|
1005
|
+
|
|
1006
|
+
- A new entity may be created if it doesn't exist (e.g., first registration)
|
|
1007
|
+
- An existing entity should be updated with new state
|
|
1008
|
+
|
|
1009
|
+
Unlike partial_update() which only does UPDATE, upsert_partial() handles
|
|
1010
|
+
both INSERT and UPDATE cases atomically using PostgreSQL's
|
|
1011
|
+
INSERT ON CONFLICT DO UPDATE.
|
|
1012
|
+
|
|
1013
|
+
Supports composite conflict keys for tables with unique constraints on
|
|
1014
|
+
multiple columns (e.g., ``(entity_id, domain)``).
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
aggregate_id: The primary aggregate identifier (for logging/tracing).
|
|
1018
|
+
values: Dictionary mapping column names to their values.
|
|
1019
|
+
MUST include all conflict columns specified.
|
|
1020
|
+
Column names are quoted for SQL safety. Values are passed as
|
|
1021
|
+
parameterized query arguments for injection protection.
|
|
1022
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
1023
|
+
conflict_columns: Optional list of column names for ON CONFLICT clause.
|
|
1024
|
+
If not provided, defaults to the contract's primary_key.
|
|
1025
|
+
Use this for composite unique constraints (e.g., ["entity_id", "domain"]).
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
True if a row was inserted or updated successfully.
|
|
1029
|
+
|
|
1030
|
+
Raises:
|
|
1031
|
+
ProtocolConfigurationError: If values dict is empty or missing required conflict columns.
|
|
1032
|
+
InfraConnectionError: If database connection fails.
|
|
1033
|
+
InfraTimeoutError: If upsert times out.
|
|
1034
|
+
RuntimeHostError: For other database errors.
|
|
1035
|
+
|
|
1036
|
+
Security:
|
|
1037
|
+
- Column names are quoted using quote_identifier() for SQL safety
|
|
1038
|
+
- Values use parameterized queries ($1, $2, etc.) to prevent injection
|
|
1039
|
+
- Table and primary key names come from the trusted contract definition
|
|
1040
|
+
|
|
1041
|
+
Example:
|
|
1042
|
+
>>> from datetime import UTC, datetime
|
|
1043
|
+
>>> # Upsert with composite conflict key (creates row if not exists)
|
|
1044
|
+
>>> result = await projector.upsert_partial(
|
|
1045
|
+
... aggregate_id=node_id,
|
|
1046
|
+
... values={
|
|
1047
|
+
... "entity_id": node_id,
|
|
1048
|
+
... "domain": "registration",
|
|
1049
|
+
... "current_state": "pending_registration",
|
|
1050
|
+
... "node_type": "effect",
|
|
1051
|
+
... "node_version": "1.0.0",
|
|
1052
|
+
... "capabilities": "{}",
|
|
1053
|
+
... "registered_at": datetime.now(UTC),
|
|
1054
|
+
... "updated_at": datetime.now(UTC),
|
|
1055
|
+
... },
|
|
1056
|
+
... correlation_id=correlation_id,
|
|
1057
|
+
... conflict_columns=["entity_id", "domain"], # Composite key
|
|
1058
|
+
... )
|
|
1059
|
+
|
|
1060
|
+
Related:
|
|
1061
|
+
- OMN-1170: Converting ProjectorRegistration to declarative contracts
|
|
1062
|
+
- partial_update(): UPDATE only (row must exist)
|
|
1063
|
+
- project(): Full event-driven projection (uses event envelopes)
|
|
1064
|
+
"""
|
|
1065
|
+
ctx = ModelInfraErrorContext(
|
|
1066
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
1067
|
+
operation="upsert_partial",
|
|
1068
|
+
target_name=f"projector.{self.projector_id}",
|
|
1069
|
+
correlation_id=correlation_id,
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
try:
|
|
1073
|
+
return await self._partial_upsert(
|
|
1074
|
+
aggregate_id, values, correlation_id, conflict_columns
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
except asyncpg.PostgresConnectionError as e:
|
|
1078
|
+
raise InfraConnectionError(
|
|
1079
|
+
f"Failed to connect to database for partial upsert: {self.projector_id}",
|
|
1080
|
+
context=ctx,
|
|
1081
|
+
) from e
|
|
1082
|
+
|
|
1083
|
+
except asyncpg.QueryCanceledError as e:
|
|
1084
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
1085
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
1086
|
+
operation="upsert_partial",
|
|
1087
|
+
target_name=f"projector.{self.projector_id}",
|
|
1088
|
+
correlation_id=correlation_id,
|
|
1089
|
+
# timeout_seconds omitted - database timeout is connection-pool level, not available here
|
|
1090
|
+
)
|
|
1091
|
+
raise InfraTimeoutError(
|
|
1092
|
+
f"Partial upsert timed out for: {self.projector_id}",
|
|
1093
|
+
context=timeout_ctx,
|
|
1094
|
+
) from e
|
|
1095
|
+
|
|
1096
|
+
except ProtocolConfigurationError:
|
|
1097
|
+
# Re-raise ProtocolConfigurationError (empty values or missing PK) as-is
|
|
1098
|
+
raise
|
|
1099
|
+
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
raise RuntimeHostError(
|
|
1102
|
+
f"Failed to execute partial upsert: {type(e).__name__}",
|
|
1103
|
+
context=ctx,
|
|
1104
|
+
) from e
|
|
1105
|
+
|
|
1106
|
+
def _get_event_type(self, event: ModelEventEnvelope[object]) -> str:
|
|
1107
|
+
"""Extract event type from envelope.
|
|
1108
|
+
|
|
1109
|
+
Event type is resolved in the following order:
|
|
1110
|
+
1. envelope.metadata.tags['event_type'] if present
|
|
1111
|
+
2. payload.event_type attribute if present
|
|
1112
|
+
3. payload class name
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
event: The event envelope to extract type from.
|
|
1116
|
+
|
|
1117
|
+
Returns:
|
|
1118
|
+
Event type string.
|
|
1119
|
+
"""
|
|
1120
|
+
# Check metadata tags first
|
|
1121
|
+
if event.metadata and event.metadata.tags:
|
|
1122
|
+
event_type_tag = event.metadata.tags.get("event_type")
|
|
1123
|
+
if event_type_tag is not None:
|
|
1124
|
+
return str(event_type_tag)
|
|
1125
|
+
|
|
1126
|
+
# Check payload attribute
|
|
1127
|
+
payload = event.payload
|
|
1128
|
+
if hasattr(payload, "event_type"):
|
|
1129
|
+
event_type_attr = payload.event_type
|
|
1130
|
+
if event_type_attr:
|
|
1131
|
+
return str(event_type_attr)
|
|
1132
|
+
|
|
1133
|
+
# Fall back to class name
|
|
1134
|
+
return type(payload).__name__
|
|
1135
|
+
|
|
1136
|
+
def _extract_values(
|
|
1137
|
+
self,
|
|
1138
|
+
event: ModelEventEnvelope[object],
|
|
1139
|
+
event_type: str,
|
|
1140
|
+
) -> dict[str, object]:
|
|
1141
|
+
"""Extract column values from event based on contract schema.
|
|
1142
|
+
|
|
1143
|
+
Iterates through the contract's column definitions and resolves
|
|
1144
|
+
each column's source path to extract the value from the event.
|
|
1145
|
+
|
|
1146
|
+
Path Resolution Failures:
|
|
1147
|
+
When path resolution fails (returns None), a WARNING is logged
|
|
1148
|
+
to alert operators of potential contract configuration issues.
|
|
1149
|
+
This is critical for production monitoring as silent None values
|
|
1150
|
+
could indicate typos in contract source paths.
|
|
1151
|
+
|
|
1152
|
+
Args:
|
|
1153
|
+
event: The event envelope containing the data.
|
|
1154
|
+
event_type: The resolved event type for filtering.
|
|
1155
|
+
|
|
1156
|
+
Returns:
|
|
1157
|
+
Dictionary mapping column names to their extracted values.
|
|
1158
|
+
"""
|
|
1159
|
+
values: dict[str, object] = {}
|
|
1160
|
+
schema = self._contract.projection_schema
|
|
1161
|
+
|
|
1162
|
+
for column in schema.columns:
|
|
1163
|
+
# Skip columns with on_event filter that doesn't match
|
|
1164
|
+
if column.on_event is not None and column.on_event != event_type:
|
|
1165
|
+
continue
|
|
1166
|
+
|
|
1167
|
+
# Resolve the source path
|
|
1168
|
+
value = self._resolve_path(event, column.source)
|
|
1169
|
+
|
|
1170
|
+
# Log warning for path resolution failures
|
|
1171
|
+
if value is None:
|
|
1172
|
+
if column.default is not None:
|
|
1173
|
+
# Default will be applied - less critical but still noteworthy
|
|
1174
|
+
logger.warning(
|
|
1175
|
+
"Path resolution failed for column '%s' with source '%s' on "
|
|
1176
|
+
"event type '%s'. Using default value '%s'. "
|
|
1177
|
+
"Check contract source path for typos.",
|
|
1178
|
+
column.name,
|
|
1179
|
+
column.source,
|
|
1180
|
+
event_type,
|
|
1181
|
+
column.default,
|
|
1182
|
+
extra={
|
|
1183
|
+
"projector_id": self.projector_id,
|
|
1184
|
+
"column_name": column.name,
|
|
1185
|
+
"source_path": column.source,
|
|
1186
|
+
"event_type": event_type,
|
|
1187
|
+
"default_applied": True,
|
|
1188
|
+
"default_value": column.default,
|
|
1189
|
+
},
|
|
1190
|
+
)
|
|
1191
|
+
value = column.default
|
|
1192
|
+
else:
|
|
1193
|
+
# No default - value will be None, potentially risky
|
|
1194
|
+
logger.warning(
|
|
1195
|
+
"Path resolution failed for column '%s' with source '%s' on "
|
|
1196
|
+
"event type '%s'. Value will be None. "
|
|
1197
|
+
"Check contract source path for typos.",
|
|
1198
|
+
column.name,
|
|
1199
|
+
column.source,
|
|
1200
|
+
event_type,
|
|
1201
|
+
extra={
|
|
1202
|
+
"projector_id": self.projector_id,
|
|
1203
|
+
"column_name": column.name,
|
|
1204
|
+
"source_path": column.source,
|
|
1205
|
+
"event_type": event_type,
|
|
1206
|
+
"default_applied": False,
|
|
1207
|
+
},
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
values[column.name] = value
|
|
1211
|
+
|
|
1212
|
+
return values
|
|
1213
|
+
|
|
1214
|
+
def _resolve_path(
|
|
1215
|
+
self,
|
|
1216
|
+
root: object,
|
|
1217
|
+
path: str,
|
|
1218
|
+
) -> object | None:
|
|
1219
|
+
"""Resolve a dot-notation path to extract a value.
|
|
1220
|
+
|
|
1221
|
+
Supports navigation through:
|
|
1222
|
+
- Dictionary keys
|
|
1223
|
+
- Object attributes
|
|
1224
|
+
- Pydantic model fields (via model_dump())
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
root: The root object to start navigation from.
|
|
1228
|
+
path: Dot-notation path (e.g., "event.payload.node_name").
|
|
1229
|
+
|
|
1230
|
+
Returns:
|
|
1231
|
+
The resolved value, or None if path resolution fails.
|
|
1232
|
+
|
|
1233
|
+
Example:
|
|
1234
|
+
>>> event = ModelEventEnvelope(payload={"node_name": "test"})
|
|
1235
|
+
>>> self._resolve_path(event, "payload.node_name")
|
|
1236
|
+
'test'
|
|
1237
|
+
"""
|
|
1238
|
+
parts = path.split(".")
|
|
1239
|
+
current: object = root
|
|
1240
|
+
|
|
1241
|
+
for part in parts:
|
|
1242
|
+
if current is None:
|
|
1243
|
+
return None
|
|
1244
|
+
|
|
1245
|
+
# Try dictionary access
|
|
1246
|
+
if isinstance(current, dict):
|
|
1247
|
+
current = current.get(part)
|
|
1248
|
+
continue
|
|
1249
|
+
|
|
1250
|
+
# Try attribute access first (avoids Pydantic model_dump side effects)
|
|
1251
|
+
if hasattr(current, part):
|
|
1252
|
+
current = getattr(current, part)
|
|
1253
|
+
continue
|
|
1254
|
+
|
|
1255
|
+
# Fall back to Pydantic model_dump for nested access
|
|
1256
|
+
if isinstance(current, BaseModel):
|
|
1257
|
+
dumped = current.model_dump()
|
|
1258
|
+
current = dumped.get(part)
|
|
1259
|
+
continue
|
|
1260
|
+
|
|
1261
|
+
# Path resolution failed
|
|
1262
|
+
logger.debug(
|
|
1263
|
+
"Path resolution failed at part '%s'",
|
|
1264
|
+
part,
|
|
1265
|
+
extra={
|
|
1266
|
+
"path": path,
|
|
1267
|
+
"current_type": type(current).__name__,
|
|
1268
|
+
},
|
|
1269
|
+
)
|
|
1270
|
+
return None
|
|
1271
|
+
|
|
1272
|
+
return current
|
|
1273
|
+
|
|
1274
|
+
async def _execute_projection(
|
|
1275
|
+
self,
|
|
1276
|
+
values: dict[str, object],
|
|
1277
|
+
correlation_id: UUID,
|
|
1278
|
+
event_type: str,
|
|
1279
|
+
) -> int:
|
|
1280
|
+
"""Execute the projection based on behavior mode.
|
|
1281
|
+
|
|
1282
|
+
Dispatches to the appropriate SQL execution method based on
|
|
1283
|
+
the contract's behavior.mode setting.
|
|
1284
|
+
|
|
1285
|
+
Args:
|
|
1286
|
+
values: Column name to value mapping.
|
|
1287
|
+
correlation_id: Correlation ID for tracing.
|
|
1288
|
+
event_type: The event type being projected (for logging context).
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
Number of rows affected.
|
|
1292
|
+
|
|
1293
|
+
Raises:
|
|
1294
|
+
asyncpg exceptions on database errors.
|
|
1295
|
+
"""
|
|
1296
|
+
mode = self._contract.behavior.mode
|
|
1297
|
+
|
|
1298
|
+
if mode == "upsert":
|
|
1299
|
+
return await self._upsert(values, correlation_id, event_type)
|
|
1300
|
+
elif mode == "insert_only":
|
|
1301
|
+
return await self._insert(values, correlation_id, event_type)
|
|
1302
|
+
elif mode == "append":
|
|
1303
|
+
return await self._append(values, correlation_id, event_type)
|
|
1304
|
+
else:
|
|
1305
|
+
# This should never happen due to contract validation
|
|
1306
|
+
context = ModelInfraErrorContext(
|
|
1307
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1308
|
+
operation="execute_projection",
|
|
1309
|
+
target_name=f"projector.{self.projector_id}",
|
|
1310
|
+
correlation_id=correlation_id,
|
|
1311
|
+
)
|
|
1312
|
+
raise ProtocolConfigurationError(
|
|
1313
|
+
f"Unknown projection mode: {mode}",
|
|
1314
|
+
context=context,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
def __repr__(self) -> str:
|
|
1318
|
+
"""Return string representation."""
|
|
1319
|
+
return (
|
|
1320
|
+
f"ProjectorShell("
|
|
1321
|
+
f"id={self.projector_id!r}, "
|
|
1322
|
+
f"aggregate_type={self.aggregate_type!r}, "
|
|
1323
|
+
f"events={len(self.consumed_events)}, "
|
|
1324
|
+
f"mode={self._contract.behavior.mode!r})"
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
__all__ = [
|
|
1329
|
+
"ProjectorShell",
|
|
1330
|
+
]
|