omnibase_infra 0.2.1__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/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -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 +123 -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_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 +101 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -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_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 +156 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +92 -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 +102 -0
- omnibase_infra/errors/error_infra.py +608 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +112 -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 +86 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -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 +725 -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/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +75 -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 +787 -0
- omnibase_infra/handlers/handler_db.py +1039 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +1154 -0
- omnibase_infra/handlers/handler_http.py +920 -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 +748 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +422 -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 +42 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -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 +915 -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 +747 -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 +99 -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/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -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 +2465 -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 +136 -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 +311 -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 +147 -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_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 +37 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -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/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 +590 -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 +59 -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_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +179 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +175 -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 +40 -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 +280 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +120 -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 +48 -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 +208 -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 +99 -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/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +172 -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/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 +475 -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 +609 -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 +525 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -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 +225 -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 +109 -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 +194 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -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 +416 -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 +214 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +32 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -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 +1137 -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 +435 -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 +99 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -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 +296 -0
- omnibase_infra/runtime/binding_config_resolver.py +2706 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/contract_handler_discovery.py +582 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -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/handler_contract_source.py +669 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
- omnibase_infra/runtime/handler_registry.py +321 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +378 -0
- omnibase_infra/runtime/mixins/__init__.py +17 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
- omnibase_infra/runtime/models/__init__.py +192 -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_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 +228 -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_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_scheduler_config.py +624 -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_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -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 +1102 -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 +27 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -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 +444 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1502 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2110 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1573 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
- omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
- omnibase_infra/runtime/util_container_wiring.py +1123 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +566 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/services/__init__.py +68 -0
- omnibase_infra/services/corpus_capture.py +678 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +897 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +682 -0
- omnibase_infra/services/service_timeout_scanner.py +390 -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/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 +21 -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 +89 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_datetime.py +372 -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_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -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 +1486 -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 +1710 -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 +410 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.1.dist-info/METADATA +197 -0
- omnibase_infra-0.2.1.dist-info/RECORD +675 -0
- omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
- omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""PostgreSQL Snapshot Store for Production Persistence.
|
|
4
|
+
|
|
5
|
+
This module provides a PostgreSQL implementation of ProtocolSnapshotStore
|
|
6
|
+
for production snapshot persistence. The store uses asyncpg for async
|
|
7
|
+
database operations and supports:
|
|
8
|
+
|
|
9
|
+
- Idempotent saves via content_hash deduplication
|
|
10
|
+
- Subject-based filtering and sequence ordering
|
|
11
|
+
- Atomic sequence number generation
|
|
12
|
+
- Parent reference tracking for lineage/fork scenarios
|
|
13
|
+
|
|
14
|
+
Table Schema:
|
|
15
|
+
The store expects a `snapshots` table with the following schema. Use
|
|
16
|
+
the `ensure_schema()` method to create it automatically.
|
|
17
|
+
|
|
18
|
+
.. code-block:: sql
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
21
|
+
id UUID PRIMARY KEY,
|
|
22
|
+
subject_type VARCHAR(255) NOT NULL,
|
|
23
|
+
subject_id UUID NOT NULL,
|
|
24
|
+
data JSONB NOT NULL,
|
|
25
|
+
sequence_number INTEGER NOT NULL,
|
|
26
|
+
version INTEGER DEFAULT 1,
|
|
27
|
+
content_hash VARCHAR(128),
|
|
28
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
29
|
+
parent_id UUID REFERENCES snapshots(id),
|
|
30
|
+
|
|
31
|
+
CONSTRAINT snapshots_subject_sequence_unique
|
|
32
|
+
UNIQUE (subject_type, subject_id, sequence_number)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_subject
|
|
36
|
+
ON snapshots (subject_type, subject_id, sequence_number DESC);
|
|
37
|
+
|
|
38
|
+
-- UNIQUE partial index enables atomic ON CONFLICT upserts
|
|
39
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshots_content_hash
|
|
40
|
+
ON snapshots (content_hash) WHERE content_hash IS NOT NULL;
|
|
41
|
+
|
|
42
|
+
Connection Pooling:
|
|
43
|
+
The store requires an asyncpg connection pool to be injected at
|
|
44
|
+
construction time. This allows the pool to be shared across multiple
|
|
45
|
+
stores and services, with lifecycle managed by the application.
|
|
46
|
+
|
|
47
|
+
.. code-block:: python
|
|
48
|
+
|
|
49
|
+
import asyncpg
|
|
50
|
+
from omnibase_infra.services.snapshot import StoreSnapshotPostgres
|
|
51
|
+
|
|
52
|
+
# Create pool (managed by application)
|
|
53
|
+
pool = await asyncpg.create_pool(dsn="postgresql://...")
|
|
54
|
+
|
|
55
|
+
# Inject pool into store
|
|
56
|
+
store = StoreSnapshotPostgres(pool=pool)
|
|
57
|
+
await store.ensure_schema()
|
|
58
|
+
|
|
59
|
+
# Use store
|
|
60
|
+
snapshot_id = await store.save(snapshot)
|
|
61
|
+
|
|
62
|
+
Error Handling:
|
|
63
|
+
All operations wrap database exceptions in ONEX error types:
|
|
64
|
+
- InfraConnectionError: Connection failures, pool exhaustion
|
|
65
|
+
- InfraTimeoutError: Query timeouts (from asyncpg.QueryCanceledError)
|
|
66
|
+
|
|
67
|
+
Security:
|
|
68
|
+
- All queries use parameterized statements (no SQL injection)
|
|
69
|
+
- DSN/credentials are never logged or exposed in errors
|
|
70
|
+
- Connection pool credentials managed externally
|
|
71
|
+
|
|
72
|
+
Related Tickets:
|
|
73
|
+
- OMN-1246: ServiceSnapshot Infrastructure Primitive
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import json
|
|
79
|
+
import logging
|
|
80
|
+
from datetime import UTC, datetime, timedelta
|
|
81
|
+
from uuid import UUID
|
|
82
|
+
|
|
83
|
+
import asyncpg
|
|
84
|
+
import asyncpg.exceptions
|
|
85
|
+
|
|
86
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
87
|
+
from omnibase_infra.errors import (
|
|
88
|
+
InfraConnectionError,
|
|
89
|
+
ModelInfraErrorContext,
|
|
90
|
+
ProtocolConfigurationError,
|
|
91
|
+
)
|
|
92
|
+
from omnibase_infra.models.snapshot import ModelSnapshot, ModelSubjectRef
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class StoreSnapshotPostgres:
|
|
98
|
+
"""PostgreSQL implementation of ProtocolSnapshotStore.
|
|
99
|
+
|
|
100
|
+
Provides production-grade snapshot persistence using asyncpg with:
|
|
101
|
+
- Content-hash based idempotency for duplicate detection
|
|
102
|
+
- Atomic sequence number generation using database MAX() + 1
|
|
103
|
+
- JSONB storage for snapshot data payloads
|
|
104
|
+
- Composite indexes for efficient subject-based queries
|
|
105
|
+
|
|
106
|
+
Connection Management:
|
|
107
|
+
The pool is injected at construction time and NOT managed by
|
|
108
|
+
this class. The application is responsible for pool lifecycle
|
|
109
|
+
(creation, health checks, shutdown).
|
|
110
|
+
|
|
111
|
+
Concurrency:
|
|
112
|
+
Database-level constraints ensure sequence uniqueness. For
|
|
113
|
+
high-concurrency scenarios, consider using database sequences
|
|
114
|
+
or advisory locks.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> import asyncpg
|
|
118
|
+
>>> from omnibase_infra.services.snapshot import StoreSnapshotPostgres
|
|
119
|
+
>>> from omnibase_infra.models.snapshot import ModelSnapshot, ModelSubjectRef
|
|
120
|
+
>>>
|
|
121
|
+
>>> # Create pool and store
|
|
122
|
+
>>> pool = await asyncpg.create_pool(dsn="postgresql://...")
|
|
123
|
+
>>> store = StoreSnapshotPostgres(pool=pool)
|
|
124
|
+
>>> await store.ensure_schema()
|
|
125
|
+
>>>
|
|
126
|
+
>>> # Save a snapshot
|
|
127
|
+
>>> subject = ModelSubjectRef(subject_type="agent", subject_id=uuid4())
|
|
128
|
+
>>> snapshot = ModelSnapshot(
|
|
129
|
+
... subject=subject,
|
|
130
|
+
... data={"status": "active"},
|
|
131
|
+
... sequence_number=1,
|
|
132
|
+
... )
|
|
133
|
+
>>> saved_id = await store.save(snapshot)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
137
|
+
"""Initialize the PostgreSQL snapshot store.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
pool: asyncpg connection pool. The pool must be created and
|
|
141
|
+
configured by the caller. The store does not manage pool
|
|
142
|
+
lifecycle (creation, shutdown).
|
|
143
|
+
|
|
144
|
+
Note:
|
|
145
|
+
Call ensure_schema() after construction to create the
|
|
146
|
+
required table and indexes if they don't exist.
|
|
147
|
+
"""
|
|
148
|
+
self._pool = pool
|
|
149
|
+
|
|
150
|
+
async def save(self, snapshot: ModelSnapshot) -> UUID:
|
|
151
|
+
"""Persist a snapshot with content-hash based idempotency.
|
|
152
|
+
|
|
153
|
+
If a snapshot with the same content_hash already exists,
|
|
154
|
+
returns the existing snapshot's ID instead of creating a
|
|
155
|
+
duplicate. This enables safe retries without data duplication.
|
|
156
|
+
|
|
157
|
+
Race Condition Handling:
|
|
158
|
+
This method uses INSERT ON CONFLICT with a unique partial index
|
|
159
|
+
on content_hash to achieve atomic idempotency. The database-level
|
|
160
|
+
unique constraint eliminates TOCTOU race conditions that would
|
|
161
|
+
occur with separate SELECT-then-INSERT patterns.
|
|
162
|
+
|
|
163
|
+
Conflict scenarios:
|
|
164
|
+
- Same content_hash (any sequence): Returns existing ID via ON CONFLICT
|
|
165
|
+
- Same sequence, different content_hash: Raises UniqueViolationError
|
|
166
|
+
- No conflicts: Normal insert
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
snapshot: The snapshot to persist.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
UUID of the saved or existing snapshot.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
InfraConnectionError: If database connection fails or
|
|
176
|
+
query execution fails.
|
|
177
|
+
|
|
178
|
+
Note:
|
|
179
|
+
Requires ensure_schema() to have created the unique partial index
|
|
180
|
+
on content_hash. See ensure_schema() for details.
|
|
181
|
+
"""
|
|
182
|
+
# Serialize data to JSON for JSONB storage (done outside try for clarity)
|
|
183
|
+
data_json = json.dumps(snapshot.data, sort_keys=True)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
async with self._pool.acquire() as conn:
|
|
187
|
+
if snapshot.content_hash:
|
|
188
|
+
# Atomic upsert using ON CONFLICT on the unique content_hash index.
|
|
189
|
+
# This eliminates the TOCTOU race condition by letting the database
|
|
190
|
+
# handle the check-and-insert atomically:
|
|
191
|
+
# - If content_hash exists: DO UPDATE (no-op) returns existing row
|
|
192
|
+
# - If content_hash is new: INSERT returns new row
|
|
193
|
+
# - If sequence conflicts: Raises UniqueViolationError (handled below)
|
|
194
|
+
#
|
|
195
|
+
# The DO UPDATE SET id = snapshots.id is a no-op that enables
|
|
196
|
+
# RETURNING to return the existing row's id.
|
|
197
|
+
result = await conn.fetchval(
|
|
198
|
+
"""
|
|
199
|
+
INSERT INTO snapshots (
|
|
200
|
+
id, subject_type, subject_id, data, sequence_number,
|
|
201
|
+
version, content_hash, created_at, parent_id
|
|
202
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
|
|
203
|
+
ON CONFLICT (content_hash) WHERE content_hash IS NOT NULL
|
|
204
|
+
DO UPDATE SET id = snapshots.id
|
|
205
|
+
RETURNING id
|
|
206
|
+
""",
|
|
207
|
+
snapshot.id,
|
|
208
|
+
snapshot.subject.subject_type,
|
|
209
|
+
snapshot.subject.subject_id,
|
|
210
|
+
data_json,
|
|
211
|
+
snapshot.sequence_number,
|
|
212
|
+
snapshot.version,
|
|
213
|
+
snapshot.content_hash,
|
|
214
|
+
snapshot.created_at,
|
|
215
|
+
snapshot.parent_id,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if result:
|
|
219
|
+
result_id = UUID(str(result))
|
|
220
|
+
if result_id != snapshot.id:
|
|
221
|
+
logger.debug(
|
|
222
|
+
"Duplicate snapshot detected via content_hash, "
|
|
223
|
+
"returning existing ID",
|
|
224
|
+
extra={
|
|
225
|
+
"existing_id": str(result_id),
|
|
226
|
+
"content_hash": snapshot.content_hash[:16] + "...",
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
logger.debug(
|
|
231
|
+
"Snapshot saved",
|
|
232
|
+
extra={
|
|
233
|
+
"snapshot_id": str(snapshot.id),
|
|
234
|
+
"subject_type": snapshot.subject.subject_type,
|
|
235
|
+
"sequence_number": snapshot.sequence_number,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
return result_id
|
|
239
|
+
|
|
240
|
+
# Result should never be None with DO UPDATE, but handle defensively
|
|
241
|
+
context = ModelInfraErrorContext(
|
|
242
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
243
|
+
operation="save_snapshot",
|
|
244
|
+
target_name="snapshots",
|
|
245
|
+
)
|
|
246
|
+
raise InfraConnectionError(
|
|
247
|
+
"Unexpected NULL result from upsert",
|
|
248
|
+
context=context,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# No content_hash - insert directly with conflict handling
|
|
252
|
+
result = await conn.fetchval(
|
|
253
|
+
"""
|
|
254
|
+
INSERT INTO snapshots (
|
|
255
|
+
id, subject_type, subject_id, data, sequence_number,
|
|
256
|
+
version, content_hash, created_at, parent_id
|
|
257
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
|
|
258
|
+
ON CONFLICT (subject_type, subject_id, sequence_number)
|
|
259
|
+
DO NOTHING
|
|
260
|
+
RETURNING id
|
|
261
|
+
""",
|
|
262
|
+
snapshot.id,
|
|
263
|
+
snapshot.subject.subject_type,
|
|
264
|
+
snapshot.subject.subject_id,
|
|
265
|
+
data_json,
|
|
266
|
+
snapshot.sequence_number,
|
|
267
|
+
snapshot.version,
|
|
268
|
+
snapshot.content_hash,
|
|
269
|
+
snapshot.created_at,
|
|
270
|
+
snapshot.parent_id,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if result:
|
|
274
|
+
logger.debug(
|
|
275
|
+
"Snapshot saved",
|
|
276
|
+
extra={
|
|
277
|
+
"snapshot_id": str(snapshot.id),
|
|
278
|
+
"subject_type": snapshot.subject.subject_type,
|
|
279
|
+
"sequence_number": snapshot.sequence_number,
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
return UUID(str(result))
|
|
283
|
+
|
|
284
|
+
# Sequence conflict - return error
|
|
285
|
+
context = ModelInfraErrorContext(
|
|
286
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
287
|
+
operation="save_snapshot",
|
|
288
|
+
target_name="snapshots",
|
|
289
|
+
)
|
|
290
|
+
raise InfraConnectionError(
|
|
291
|
+
f"Sequence conflict: sequence_number {snapshot.sequence_number} "
|
|
292
|
+
f"already exists for subject "
|
|
293
|
+
f"({snapshot.subject.subject_type}, {snapshot.subject.subject_id})",
|
|
294
|
+
context=context,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
except asyncpg.exceptions.UniqueViolationError as e:
|
|
298
|
+
# UniqueViolationError occurs when:
|
|
299
|
+
# 1. Sequence constraint violated (same subject + sequence, different content)
|
|
300
|
+
# 2. Rare race on content_hash unique index (concurrent identical inserts)
|
|
301
|
+
#
|
|
302
|
+
# For case 2, check if content_hash exists and return it for idempotency.
|
|
303
|
+
if snapshot.content_hash:
|
|
304
|
+
try:
|
|
305
|
+
async with self._pool.acquire() as conn:
|
|
306
|
+
existing = await conn.fetchval(
|
|
307
|
+
"SELECT id FROM snapshots WHERE content_hash = $1",
|
|
308
|
+
snapshot.content_hash,
|
|
309
|
+
)
|
|
310
|
+
if existing:
|
|
311
|
+
existing_id = UUID(str(existing))
|
|
312
|
+
logger.debug(
|
|
313
|
+
"Race condition resolved: returning existing ID "
|
|
314
|
+
"after UniqueViolationError",
|
|
315
|
+
extra={
|
|
316
|
+
"existing_id": str(existing_id),
|
|
317
|
+
"content_hash": snapshot.content_hash[:16] + "...",
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
return existing_id
|
|
321
|
+
except Exception:
|
|
322
|
+
pass # Fall through to re-raise original error
|
|
323
|
+
|
|
324
|
+
# Sequence conflict with different content - this is a real conflict
|
|
325
|
+
context = ModelInfraErrorContext(
|
|
326
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
327
|
+
operation="save_snapshot",
|
|
328
|
+
target_name="snapshots",
|
|
329
|
+
)
|
|
330
|
+
raise InfraConnectionError(
|
|
331
|
+
f"Unique constraint violation during save for subject "
|
|
332
|
+
f"({snapshot.subject.subject_type}, {snapshot.subject.subject_id}): "
|
|
333
|
+
f"{e.constraint_name or 'unknown constraint'}",
|
|
334
|
+
context=context,
|
|
335
|
+
) from e
|
|
336
|
+
|
|
337
|
+
except InfraConnectionError:
|
|
338
|
+
# Re-raise our own errors without wrapping
|
|
339
|
+
raise
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
context = ModelInfraErrorContext(
|
|
343
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
344
|
+
operation="save_snapshot",
|
|
345
|
+
target_name="snapshots",
|
|
346
|
+
)
|
|
347
|
+
raise InfraConnectionError(
|
|
348
|
+
f"Failed to save snapshot: {type(e).__name__}",
|
|
349
|
+
context=context,
|
|
350
|
+
) from e
|
|
351
|
+
|
|
352
|
+
async def load(self, snapshot_id: UUID) -> ModelSnapshot | None:
|
|
353
|
+
"""Load a snapshot by ID.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
snapshot_id: The unique identifier of the snapshot.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The snapshot if found, None otherwise.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
InfraConnectionError: If database connection fails.
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
async with self._pool.acquire() as conn:
|
|
366
|
+
row = await conn.fetchrow(
|
|
367
|
+
"SELECT * FROM snapshots WHERE id = $1",
|
|
368
|
+
snapshot_id,
|
|
369
|
+
)
|
|
370
|
+
if row is None:
|
|
371
|
+
return None
|
|
372
|
+
return self._row_to_model(row)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
context = ModelInfraErrorContext(
|
|
375
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
376
|
+
operation="load_snapshot",
|
|
377
|
+
target_name="snapshots",
|
|
378
|
+
)
|
|
379
|
+
raise InfraConnectionError(
|
|
380
|
+
f"Failed to load snapshot: {type(e).__name__}",
|
|
381
|
+
context=context,
|
|
382
|
+
) from e
|
|
383
|
+
|
|
384
|
+
async def load_many(self, snapshot_ids: list[UUID]) -> dict[UUID, ModelSnapshot]:
|
|
385
|
+
"""Load multiple snapshots by ID in a single query.
|
|
386
|
+
|
|
387
|
+
Uses a batch query with ANY() for efficient multi-row fetch,
|
|
388
|
+
avoiding N+1 query patterns when loading multiple snapshots.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
snapshot_ids: List of snapshot UUIDs to load.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dictionary mapping snapshot ID to ModelSnapshot for found
|
|
395
|
+
snapshots. Missing IDs are not included in the result.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
InfraConnectionError: If database connection fails.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> snapshots = await store.load_many([id1, id2, id3])
|
|
402
|
+
>>> for sid, snap in snapshots.items():
|
|
403
|
+
... print(f"{sid}: seq={snap.sequence_number}")
|
|
404
|
+
"""
|
|
405
|
+
if not snapshot_ids:
|
|
406
|
+
return {}
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
async with self._pool.acquire() as conn:
|
|
410
|
+
rows = await conn.fetch(
|
|
411
|
+
"SELECT * FROM snapshots WHERE id = ANY($1::uuid[])",
|
|
412
|
+
snapshot_ids,
|
|
413
|
+
)
|
|
414
|
+
return {row["id"]: self._row_to_model(row) for row in rows}
|
|
415
|
+
except Exception as e:
|
|
416
|
+
context = ModelInfraErrorContext(
|
|
417
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
418
|
+
operation="load_many_snapshots",
|
|
419
|
+
target_name="snapshots",
|
|
420
|
+
)
|
|
421
|
+
raise InfraConnectionError(
|
|
422
|
+
f"Failed to load snapshots: {type(e).__name__}",
|
|
423
|
+
context=context,
|
|
424
|
+
) from e
|
|
425
|
+
|
|
426
|
+
async def load_latest(
|
|
427
|
+
self,
|
|
428
|
+
subject: ModelSubjectRef | None = None,
|
|
429
|
+
) -> ModelSnapshot | None:
|
|
430
|
+
"""Load the most recent snapshot by sequence_number.
|
|
431
|
+
|
|
432
|
+
Retrieves the snapshot with the highest sequence_number,
|
|
433
|
+
optionally filtered by subject. "Most recent" is determined
|
|
434
|
+
by sequence_number (not created_at) for consistent ordering.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
subject: Optional filter by subject reference.
|
|
438
|
+
|
|
439
|
+
- If provided: Returns the latest snapshot for that specific
|
|
440
|
+
subject (highest sequence_number within that subject).
|
|
441
|
+
- If None: Returns the globally latest snapshot across ALL
|
|
442
|
+
subjects (highest sequence_number in the entire store).
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
The most recent snapshot matching criteria, or None if no
|
|
446
|
+
snapshots exist.
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
InfraConnectionError: If database connection fails.
|
|
450
|
+
|
|
451
|
+
Note:
|
|
452
|
+
When ``subject=None``, "globally latest" means the snapshot with
|
|
453
|
+
the highest sequence_number across all subjects. Since sequence
|
|
454
|
+
numbers are per-subject (each subject starts at 1), this may NOT
|
|
455
|
+
correspond to the most recently created snapshot by wall-clock
|
|
456
|
+
time. Use ``query(after=timestamp)`` if you need time-based
|
|
457
|
+
ordering across subjects.
|
|
458
|
+
|
|
459
|
+
Examples:
|
|
460
|
+
>>> # Get latest for a specific subject
|
|
461
|
+
>>> subject = ModelSubjectRef(
|
|
462
|
+
... subject_type="node_registration",
|
|
463
|
+
... subject_id=node_uuid,
|
|
464
|
+
... )
|
|
465
|
+
>>> latest = await store.load_latest(subject=subject)
|
|
466
|
+
>>> # Returns snapshot with highest sequence_number for this subject
|
|
467
|
+
|
|
468
|
+
>>> # Get globally latest across ALL subjects
|
|
469
|
+
>>> global_latest = await store.load_latest(subject=None)
|
|
470
|
+
>>> # Returns snapshot with highest sequence_number in entire store
|
|
471
|
+
>>> # Note: This is NOT necessarily the most recent by created_at
|
|
472
|
+
"""
|
|
473
|
+
try:
|
|
474
|
+
async with self._pool.acquire() as conn:
|
|
475
|
+
if subject:
|
|
476
|
+
row = await conn.fetchrow(
|
|
477
|
+
"""
|
|
478
|
+
SELECT * FROM snapshots
|
|
479
|
+
WHERE subject_type = $1 AND subject_id = $2
|
|
480
|
+
ORDER BY sequence_number DESC LIMIT 1
|
|
481
|
+
""",
|
|
482
|
+
subject.subject_type,
|
|
483
|
+
subject.subject_id,
|
|
484
|
+
)
|
|
485
|
+
else:
|
|
486
|
+
row = await conn.fetchrow(
|
|
487
|
+
"SELECT * FROM snapshots ORDER BY sequence_number DESC LIMIT 1"
|
|
488
|
+
)
|
|
489
|
+
if row is None:
|
|
490
|
+
return None
|
|
491
|
+
return self._row_to_model(row)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
context = ModelInfraErrorContext(
|
|
494
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
495
|
+
operation="load_latest_snapshot",
|
|
496
|
+
target_name="snapshots",
|
|
497
|
+
)
|
|
498
|
+
raise InfraConnectionError(
|
|
499
|
+
f"Failed to load latest snapshot: {type(e).__name__}",
|
|
500
|
+
context=context,
|
|
501
|
+
) from e
|
|
502
|
+
|
|
503
|
+
async def load_latest_many(
|
|
504
|
+
self,
|
|
505
|
+
subjects: list[ModelSubjectRef],
|
|
506
|
+
) -> dict[tuple[str, UUID], ModelSnapshot]:
|
|
507
|
+
"""Load the latest snapshot for multiple subjects in a single query.
|
|
508
|
+
|
|
509
|
+
Uses a window function to efficiently fetch the latest snapshot per
|
|
510
|
+
subject in one database round-trip, avoiding N+1 query patterns.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
subjects: List of subject references to load latest snapshots for.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Dictionary mapping (subject_type, subject_id) tuple to the latest
|
|
517
|
+
ModelSnapshot for that subject. Subjects with no snapshots are
|
|
518
|
+
not included in the result.
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
InfraConnectionError: If database connection fails.
|
|
522
|
+
|
|
523
|
+
Example:
|
|
524
|
+
>>> subjects = [
|
|
525
|
+
... ModelSubjectRef(subject_type="agent", subject_id=agent_id),
|
|
526
|
+
... ModelSubjectRef(subject_type="workflow", subject_id=wf_id),
|
|
527
|
+
... ]
|
|
528
|
+
>>> latest = await store.load_latest_many(subjects)
|
|
529
|
+
>>> for (stype, sid), snap in latest.items():
|
|
530
|
+
... print(f"{stype}/{sid}: seq={snap.sequence_number}")
|
|
531
|
+
"""
|
|
532
|
+
if not subjects:
|
|
533
|
+
return {}
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
async with self._pool.acquire() as conn:
|
|
537
|
+
# Build arrays for subject_type and subject_id to match
|
|
538
|
+
subject_types = [s.subject_type for s in subjects]
|
|
539
|
+
subject_ids = [s.subject_id for s in subjects]
|
|
540
|
+
|
|
541
|
+
# Use window function to get latest per subject in one query
|
|
542
|
+
# The query uses a CTE to rank snapshots per subject, then
|
|
543
|
+
# filters to only keep the top-ranked (latest) per subject.
|
|
544
|
+
rows = await conn.fetch(
|
|
545
|
+
"""
|
|
546
|
+
WITH ranked AS (
|
|
547
|
+
SELECT *,
|
|
548
|
+
ROW_NUMBER() OVER (
|
|
549
|
+
PARTITION BY subject_type, subject_id
|
|
550
|
+
ORDER BY sequence_number DESC
|
|
551
|
+
) as rn
|
|
552
|
+
FROM snapshots
|
|
553
|
+
WHERE (subject_type, subject_id) IN (
|
|
554
|
+
SELECT * FROM UNNEST($1::text[], $2::uuid[])
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
SELECT * FROM ranked WHERE rn = 1
|
|
558
|
+
""",
|
|
559
|
+
subject_types,
|
|
560
|
+
subject_ids,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
(row["subject_type"], row["subject_id"]): self._row_to_model(row)
|
|
565
|
+
for row in rows
|
|
566
|
+
}
|
|
567
|
+
except Exception as e:
|
|
568
|
+
context = ModelInfraErrorContext(
|
|
569
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
570
|
+
operation="load_latest_many_snapshots",
|
|
571
|
+
target_name="snapshots",
|
|
572
|
+
)
|
|
573
|
+
raise InfraConnectionError(
|
|
574
|
+
f"Failed to load latest snapshots: {type(e).__name__}",
|
|
575
|
+
context=context,
|
|
576
|
+
) from e
|
|
577
|
+
|
|
578
|
+
async def query(
|
|
579
|
+
self,
|
|
580
|
+
subject: ModelSubjectRef | None = None,
|
|
581
|
+
limit: int = 50,
|
|
582
|
+
after: datetime | None = None,
|
|
583
|
+
) -> list[ModelSnapshot]:
|
|
584
|
+
"""Query snapshots with optional filtering.
|
|
585
|
+
|
|
586
|
+
Returns snapshots ordered by sequence_number descending
|
|
587
|
+
(most recent first).
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
subject: Optional filter by subject reference.
|
|
591
|
+
limit: Maximum results to return (default 50).
|
|
592
|
+
after: Only return snapshots created after this time.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
List of snapshots ordered by sequence_number descending.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
InfraConnectionError: If database connection fails.
|
|
599
|
+
"""
|
|
600
|
+
try:
|
|
601
|
+
async with self._pool.acquire() as conn:
|
|
602
|
+
# Build dynamic query with parameterized conditions
|
|
603
|
+
conditions: list[str] = []
|
|
604
|
+
params: list[object] = []
|
|
605
|
+
|
|
606
|
+
if subject:
|
|
607
|
+
conditions.append(f"subject_type = ${len(params) + 1}")
|
|
608
|
+
params.append(subject.subject_type)
|
|
609
|
+
conditions.append(f"subject_id = ${len(params) + 1}")
|
|
610
|
+
params.append(subject.subject_id)
|
|
611
|
+
|
|
612
|
+
if after:
|
|
613
|
+
conditions.append(f"created_at > ${len(params) + 1}")
|
|
614
|
+
params.append(after)
|
|
615
|
+
|
|
616
|
+
where_clause = " AND ".join(conditions) if conditions else "TRUE"
|
|
617
|
+
params.append(limit)
|
|
618
|
+
|
|
619
|
+
# S608: This is NOT SQL injection - where_clause contains only
|
|
620
|
+
# safe static column names with parameterized value placeholders
|
|
621
|
+
# ($1, $2, etc). All user-supplied values go through params.
|
|
622
|
+
query = f"""
|
|
623
|
+
SELECT * FROM snapshots
|
|
624
|
+
WHERE {where_clause}
|
|
625
|
+
ORDER BY sequence_number DESC
|
|
626
|
+
LIMIT ${len(params)}
|
|
627
|
+
""" # noqa: S608
|
|
628
|
+
|
|
629
|
+
rows = await conn.fetch(query, *params)
|
|
630
|
+
return [self._row_to_model(row) for row in rows]
|
|
631
|
+
except Exception as e:
|
|
632
|
+
context = ModelInfraErrorContext(
|
|
633
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
634
|
+
operation="query_snapshots",
|
|
635
|
+
target_name="snapshots",
|
|
636
|
+
)
|
|
637
|
+
raise InfraConnectionError(
|
|
638
|
+
f"Failed to query snapshots: {type(e).__name__}",
|
|
639
|
+
context=context,
|
|
640
|
+
) from e
|
|
641
|
+
|
|
642
|
+
async def delete(self, snapshot_id: UUID) -> bool:
|
|
643
|
+
"""Delete a snapshot by ID.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
snapshot_id: The unique identifier of the snapshot to delete.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
True if the snapshot was deleted, False if not found.
|
|
650
|
+
|
|
651
|
+
Raises:
|
|
652
|
+
InfraConnectionError: If database connection fails.
|
|
653
|
+
"""
|
|
654
|
+
try:
|
|
655
|
+
async with self._pool.acquire() as conn:
|
|
656
|
+
result = await conn.execute(
|
|
657
|
+
"DELETE FROM snapshots WHERE id = $1",
|
|
658
|
+
snapshot_id,
|
|
659
|
+
)
|
|
660
|
+
# asyncpg returns "DELETE N" where N is rows affected
|
|
661
|
+
deleted: bool = str(result) == "DELETE 1"
|
|
662
|
+
if deleted:
|
|
663
|
+
logger.debug(
|
|
664
|
+
"Snapshot deleted",
|
|
665
|
+
extra={"snapshot_id": str(snapshot_id)},
|
|
666
|
+
)
|
|
667
|
+
return deleted
|
|
668
|
+
except Exception as e:
|
|
669
|
+
context = ModelInfraErrorContext(
|
|
670
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
671
|
+
operation="delete_snapshot",
|
|
672
|
+
target_name="snapshots",
|
|
673
|
+
)
|
|
674
|
+
raise InfraConnectionError(
|
|
675
|
+
f"Failed to delete snapshot: {type(e).__name__}",
|
|
676
|
+
context=context,
|
|
677
|
+
) from e
|
|
678
|
+
|
|
679
|
+
async def get_next_sequence_number(self, subject: ModelSubjectRef) -> int:
|
|
680
|
+
"""Get the next sequence number for a subject with advisory lock.
|
|
681
|
+
|
|
682
|
+
Uses PostgreSQL advisory locks to ensure atomic sequence allocation.
|
|
683
|
+
The lock is held only during the MAX() query, allowing concurrent
|
|
684
|
+
access to different subjects while preventing race conditions for
|
|
685
|
+
the same subject.
|
|
686
|
+
|
|
687
|
+
Advisory Lock Strategy:
|
|
688
|
+
Uses pg_advisory_xact_lock() with a hash of (subject_type, subject_id)
|
|
689
|
+
to create a subject-specific lock. This ensures:
|
|
690
|
+
- Concurrent calls for the SAME subject serialize
|
|
691
|
+
- Concurrent calls for DIFFERENT subjects proceed in parallel
|
|
692
|
+
- Lock is automatically released at transaction end
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
subject: The subject reference for sequence generation.
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
The next sequence number (starts at 1 for new subjects).
|
|
699
|
+
|
|
700
|
+
Raises:
|
|
701
|
+
InfraConnectionError: If database connection fails.
|
|
702
|
+
|
|
703
|
+
Note:
|
|
704
|
+
For atomic allocate-and-save operations, prefer save_with_auto_sequence()
|
|
705
|
+
which combines sequence allocation and insert in a single transaction.
|
|
706
|
+
|
|
707
|
+
Concurrency Guarantees:
|
|
708
|
+
- No duplicate sequence numbers for the same subject
|
|
709
|
+
- No gaps in sequence numbers (unless deletes occur)
|
|
710
|
+
- Monotonically increasing per subject
|
|
711
|
+
"""
|
|
712
|
+
try:
|
|
713
|
+
async with self._pool.acquire() as conn:
|
|
714
|
+
# Use a transaction to hold the advisory lock during the query.
|
|
715
|
+
# The lock key is derived from a hash of subject identifiers.
|
|
716
|
+
# pg_advisory_xact_lock takes a bigint, so we use hashtext()
|
|
717
|
+
# on the concatenated subject identifiers.
|
|
718
|
+
async with conn.transaction():
|
|
719
|
+
# Acquire advisory lock for this specific subject.
|
|
720
|
+
# hashtext() returns a stable 32-bit hash; we cast to bigint.
|
|
721
|
+
# This serializes concurrent calls for the same subject.
|
|
722
|
+
# Note: subject_id must be converted to str() for text concatenation
|
|
723
|
+
# in the hashtext() call - asyncpg requires string type for || operator.
|
|
724
|
+
await conn.execute(
|
|
725
|
+
"""
|
|
726
|
+
SELECT pg_advisory_xact_lock(
|
|
727
|
+
hashtext($1 || '::' || $2)::bigint
|
|
728
|
+
)
|
|
729
|
+
""",
|
|
730
|
+
subject.subject_type,
|
|
731
|
+
str(subject.subject_id),
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Now safely get the next sequence number while holding the lock
|
|
735
|
+
result = await conn.fetchval(
|
|
736
|
+
"""
|
|
737
|
+
SELECT COALESCE(MAX(sequence_number), 0) + 1
|
|
738
|
+
FROM snapshots
|
|
739
|
+
WHERE subject_type = $1 AND subject_id = $2
|
|
740
|
+
""",
|
|
741
|
+
subject.subject_type,
|
|
742
|
+
subject.subject_id,
|
|
743
|
+
)
|
|
744
|
+
# Lock released automatically when transaction ends
|
|
745
|
+
return int(result) if result else 1
|
|
746
|
+
except Exception as e:
|
|
747
|
+
context = ModelInfraErrorContext(
|
|
748
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
749
|
+
operation="get_sequence_number",
|
|
750
|
+
target_name="snapshots",
|
|
751
|
+
)
|
|
752
|
+
raise InfraConnectionError(
|
|
753
|
+
f"Failed to get sequence number: {type(e).__name__}",
|
|
754
|
+
context=context,
|
|
755
|
+
) from e
|
|
756
|
+
|
|
757
|
+
async def save_with_auto_sequence(
|
|
758
|
+
self,
|
|
759
|
+
subject: ModelSubjectRef,
|
|
760
|
+
data: dict[str, object],
|
|
761
|
+
*,
|
|
762
|
+
version: int = 1,
|
|
763
|
+
content_hash: str | None = None,
|
|
764
|
+
parent_id: UUID | None = None,
|
|
765
|
+
) -> tuple[UUID, int]:
|
|
766
|
+
"""Atomically allocate sequence number and save snapshot.
|
|
767
|
+
|
|
768
|
+
This method combines sequence allocation and insert into a single
|
|
769
|
+
atomic transaction, eliminating the TOCTOU race condition that
|
|
770
|
+
exists when calling get_next_sequence_number() followed by save()
|
|
771
|
+
separately.
|
|
772
|
+
|
|
773
|
+
Atomicity Guarantees:
|
|
774
|
+
1. Advisory lock prevents concurrent sequence allocation for same subject
|
|
775
|
+
2. Sequence allocation and INSERT occur in same transaction
|
|
776
|
+
3. If INSERT fails, no sequence number is "consumed"
|
|
777
|
+
4. Content-hash deduplication still applies (returns existing if duplicate)
|
|
778
|
+
|
|
779
|
+
Race Condition Handling:
|
|
780
|
+
- Same content_hash: Returns existing snapshot ID and sequence number
|
|
781
|
+
- Concurrent saves for same subject: Serialized via advisory lock
|
|
782
|
+
- Database constraint violations: Wrapped as InfraConnectionError
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
subject: The subject reference for the snapshot.
|
|
786
|
+
data: The snapshot payload as a JSON-compatible dictionary.
|
|
787
|
+
version: Version number for the snapshot (default 1).
|
|
788
|
+
content_hash: Optional content hash for deduplication. If provided
|
|
789
|
+
and a snapshot with this hash already exists, returns the
|
|
790
|
+
existing snapshot's ID and sequence number.
|
|
791
|
+
parent_id: Optional parent snapshot ID for lineage tracking.
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
Tuple of (snapshot_id, sequence_number) for the saved or existing snapshot.
|
|
795
|
+
|
|
796
|
+
Raises:
|
|
797
|
+
InfraConnectionError: If database connection fails or
|
|
798
|
+
constraint violation occurs.
|
|
799
|
+
|
|
800
|
+
Example:
|
|
801
|
+
>>> subject = ModelSubjectRef(subject_type="agent", subject_id=uuid4())
|
|
802
|
+
>>> snapshot_id, seq_num = await store.save_with_auto_sequence(
|
|
803
|
+
... subject=subject,
|
|
804
|
+
... data={"status": "active"},
|
|
805
|
+
... content_hash="sha256:abc123...",
|
|
806
|
+
... )
|
|
807
|
+
>>> print(f"Saved snapshot {snapshot_id} with sequence {seq_num}")
|
|
808
|
+
"""
|
|
809
|
+
from uuid import uuid4
|
|
810
|
+
|
|
811
|
+
snapshot_id = uuid4()
|
|
812
|
+
data_json = json.dumps(data, sort_keys=True)
|
|
813
|
+
created_at = datetime.now(UTC)
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
async with self._pool.acquire() as conn:
|
|
817
|
+
async with conn.transaction():
|
|
818
|
+
# Step 1: Check for existing content_hash FIRST (before locking).
|
|
819
|
+
# This avoids unnecessary lock acquisition for duplicates and
|
|
820
|
+
# eliminates the race condition between check and insert by
|
|
821
|
+
# performing the check within the same transaction that will
|
|
822
|
+
# do the insert. The transaction isolation ensures consistency.
|
|
823
|
+
if content_hash:
|
|
824
|
+
existing = await conn.fetchrow(
|
|
825
|
+
"""
|
|
826
|
+
SELECT id, sequence_number FROM snapshots
|
|
827
|
+
WHERE content_hash = $1
|
|
828
|
+
""",
|
|
829
|
+
content_hash,
|
|
830
|
+
)
|
|
831
|
+
if existing:
|
|
832
|
+
existing_id = UUID(str(existing["id"]))
|
|
833
|
+
existing_seq = int(existing["sequence_number"])
|
|
834
|
+
logger.debug(
|
|
835
|
+
"Duplicate snapshot detected via content_hash, "
|
|
836
|
+
"returning existing ID",
|
|
837
|
+
extra={
|
|
838
|
+
"existing_id": str(existing_id),
|
|
839
|
+
"content_hash": content_hash[:16] + "...",
|
|
840
|
+
},
|
|
841
|
+
)
|
|
842
|
+
return existing_id, existing_seq
|
|
843
|
+
|
|
844
|
+
# Step 2: Acquire advisory lock for this subject.
|
|
845
|
+
# This serializes concurrent saves for the same subject,
|
|
846
|
+
# preventing race conditions in sequence number allocation.
|
|
847
|
+
# The lock is held until the transaction commits/rollbacks.
|
|
848
|
+
# Note: subject_id must be converted to str() for text concatenation
|
|
849
|
+
# in the hashtext() call - asyncpg requires string type for || operator.
|
|
850
|
+
await conn.execute(
|
|
851
|
+
"""
|
|
852
|
+
SELECT pg_advisory_xact_lock(
|
|
853
|
+
hashtext($1 || '::' || $2)::bigint
|
|
854
|
+
)
|
|
855
|
+
""",
|
|
856
|
+
subject.subject_type,
|
|
857
|
+
str(subject.subject_id),
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
# Step 3: Allocate next sequence number while holding lock.
|
|
861
|
+
# The advisory lock ensures no concurrent transaction can
|
|
862
|
+
# read the same MAX value for this subject.
|
|
863
|
+
sequence_number = await conn.fetchval(
|
|
864
|
+
"""
|
|
865
|
+
SELECT COALESCE(MAX(sequence_number), 0) + 1
|
|
866
|
+
FROM snapshots
|
|
867
|
+
WHERE subject_type = $1 AND subject_id = $2
|
|
868
|
+
""",
|
|
869
|
+
subject.subject_type,
|
|
870
|
+
subject.subject_id,
|
|
871
|
+
)
|
|
872
|
+
sequence_number = int(sequence_number) if sequence_number else 1
|
|
873
|
+
|
|
874
|
+
# Step 4: Insert with ON CONFLICT for content_hash idempotency.
|
|
875
|
+
# Even though we checked above, a concurrent transaction might
|
|
876
|
+
# have committed between our check and lock acquisition (before
|
|
877
|
+
# the lock was acquired). The ON CONFLICT handles this edge case.
|
|
878
|
+
if content_hash:
|
|
879
|
+
result = await conn.fetchrow(
|
|
880
|
+
"""
|
|
881
|
+
INSERT INTO snapshots (
|
|
882
|
+
id, subject_type, subject_id, data, sequence_number,
|
|
883
|
+
version, content_hash, created_at, parent_id
|
|
884
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
|
|
885
|
+
ON CONFLICT (content_hash) WHERE content_hash IS NOT NULL
|
|
886
|
+
DO UPDATE SET id = snapshots.id
|
|
887
|
+
RETURNING id, sequence_number
|
|
888
|
+
""",
|
|
889
|
+
snapshot_id,
|
|
890
|
+
subject.subject_type,
|
|
891
|
+
subject.subject_id,
|
|
892
|
+
data_json,
|
|
893
|
+
sequence_number,
|
|
894
|
+
version,
|
|
895
|
+
content_hash,
|
|
896
|
+
created_at,
|
|
897
|
+
parent_id,
|
|
898
|
+
)
|
|
899
|
+
else:
|
|
900
|
+
# No content_hash - insert directly
|
|
901
|
+
result = await conn.fetchrow(
|
|
902
|
+
"""
|
|
903
|
+
INSERT INTO snapshots (
|
|
904
|
+
id, subject_type, subject_id, data, sequence_number,
|
|
905
|
+
version, content_hash, created_at, parent_id
|
|
906
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
|
|
907
|
+
RETURNING id, sequence_number
|
|
908
|
+
""",
|
|
909
|
+
snapshot_id,
|
|
910
|
+
subject.subject_type,
|
|
911
|
+
subject.subject_id,
|
|
912
|
+
data_json,
|
|
913
|
+
sequence_number,
|
|
914
|
+
version,
|
|
915
|
+
content_hash,
|
|
916
|
+
created_at,
|
|
917
|
+
parent_id,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
if result:
|
|
921
|
+
result_id = UUID(str(result["id"]))
|
|
922
|
+
result_seq = int(result["sequence_number"])
|
|
923
|
+
|
|
924
|
+
if result_id != snapshot_id:
|
|
925
|
+
# Existing snapshot returned via ON CONFLICT
|
|
926
|
+
logger.debug(
|
|
927
|
+
"Duplicate snapshot detected during insert, "
|
|
928
|
+
"returning existing ID",
|
|
929
|
+
extra={
|
|
930
|
+
"existing_id": str(result_id),
|
|
931
|
+
"sequence_number": result_seq,
|
|
932
|
+
},
|
|
933
|
+
)
|
|
934
|
+
else:
|
|
935
|
+
logger.debug(
|
|
936
|
+
"Snapshot saved atomically",
|
|
937
|
+
extra={
|
|
938
|
+
"snapshot_id": str(snapshot_id),
|
|
939
|
+
"subject_type": subject.subject_type,
|
|
940
|
+
"sequence_number": result_seq,
|
|
941
|
+
},
|
|
942
|
+
)
|
|
943
|
+
return result_id, result_seq
|
|
944
|
+
|
|
945
|
+
# Should never reach here with valid INSERT
|
|
946
|
+
context = ModelInfraErrorContext(
|
|
947
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
948
|
+
operation="save_with_auto_sequence",
|
|
949
|
+
target_name="snapshots",
|
|
950
|
+
)
|
|
951
|
+
raise InfraConnectionError(
|
|
952
|
+
"Unexpected NULL result from atomic insert",
|
|
953
|
+
context=context,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
except asyncpg.exceptions.UniqueViolationError as e:
|
|
957
|
+
# This can occur if:
|
|
958
|
+
# 1. Very rare race on content_hash between check and insert
|
|
959
|
+
# 2. Sequence constraint violated (shouldn't happen with advisory lock)
|
|
960
|
+
#
|
|
961
|
+
# For content_hash conflicts, try to return the existing row.
|
|
962
|
+
# Use the same connection pool but a new connection to avoid
|
|
963
|
+
# transaction state issues.
|
|
964
|
+
if content_hash:
|
|
965
|
+
try:
|
|
966
|
+
async with self._pool.acquire() as recovery_conn:
|
|
967
|
+
existing = await recovery_conn.fetchrow(
|
|
968
|
+
"SELECT id, sequence_number FROM snapshots "
|
|
969
|
+
"WHERE content_hash = $1",
|
|
970
|
+
content_hash,
|
|
971
|
+
)
|
|
972
|
+
if existing:
|
|
973
|
+
existing_id = UUID(str(existing["id"]))
|
|
974
|
+
existing_seq = int(existing["sequence_number"])
|
|
975
|
+
logger.debug(
|
|
976
|
+
"Race condition resolved: returning existing ID "
|
|
977
|
+
"after UniqueViolationError",
|
|
978
|
+
extra={
|
|
979
|
+
"existing_id": str(existing_id),
|
|
980
|
+
"content_hash": content_hash[:16] + "...",
|
|
981
|
+
},
|
|
982
|
+
)
|
|
983
|
+
return existing_id, existing_seq
|
|
984
|
+
except Exception:
|
|
985
|
+
pass # Fall through to re-raise original error
|
|
986
|
+
|
|
987
|
+
context = ModelInfraErrorContext(
|
|
988
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
989
|
+
operation="save_with_auto_sequence",
|
|
990
|
+
target_name="snapshots",
|
|
991
|
+
)
|
|
992
|
+
raise InfraConnectionError(
|
|
993
|
+
f"Unique constraint violation: {e.constraint_name or 'unknown'}",
|
|
994
|
+
context=context,
|
|
995
|
+
) from e
|
|
996
|
+
|
|
997
|
+
except InfraConnectionError:
|
|
998
|
+
raise
|
|
999
|
+
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
context = ModelInfraErrorContext(
|
|
1002
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
1003
|
+
operation="save_with_auto_sequence",
|
|
1004
|
+
target_name="snapshots",
|
|
1005
|
+
)
|
|
1006
|
+
raise InfraConnectionError(
|
|
1007
|
+
f"Failed to save snapshot atomically: {type(e).__name__}",
|
|
1008
|
+
context=context,
|
|
1009
|
+
) from e
|
|
1010
|
+
|
|
1011
|
+
async def cleanup_expired(
|
|
1012
|
+
self,
|
|
1013
|
+
*,
|
|
1014
|
+
max_age_seconds: int | None = None,
|
|
1015
|
+
keep_latest_n: int | None = None,
|
|
1016
|
+
subject: ModelSubjectRef | None = None,
|
|
1017
|
+
) -> int:
|
|
1018
|
+
"""Remove expired snapshots based on retention policy.
|
|
1019
|
+
|
|
1020
|
+
Supports multiple retention strategies:
|
|
1021
|
+
- Time-based: Delete snapshots older than max_age_seconds
|
|
1022
|
+
- Count-based: Keep only the N most recent per subject
|
|
1023
|
+
- Subject-scoped: Apply policy only to a specific subject
|
|
1024
|
+
|
|
1025
|
+
When both max_age_seconds and keep_latest_n are provided, snapshots
|
|
1026
|
+
must satisfy BOTH conditions to be deleted (i.e., be older than
|
|
1027
|
+
max_age AND not in the latest N).
|
|
1028
|
+
|
|
1029
|
+
Args:
|
|
1030
|
+
max_age_seconds: Delete snapshots older than this many seconds.
|
|
1031
|
+
keep_latest_n: Always retain the N most recent per subject.
|
|
1032
|
+
subject: If provided, apply cleanup only to this subject.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
Number of snapshots deleted.
|
|
1036
|
+
|
|
1037
|
+
Raises:
|
|
1038
|
+
ProtocolConfigurationError: If keep_latest_n is provided but < 1.
|
|
1039
|
+
InfraConnectionError: If database connection fails.
|
|
1040
|
+
|
|
1041
|
+
Note:
|
|
1042
|
+
For keep_latest_n, this uses a window function to identify
|
|
1043
|
+
snapshots outside the retention window per subject. This is
|
|
1044
|
+
efficient for moderate numbers of subjects but may require
|
|
1045
|
+
batching for very large datasets.
|
|
1046
|
+
"""
|
|
1047
|
+
if keep_latest_n is not None and keep_latest_n < 1:
|
|
1048
|
+
raise ProtocolConfigurationError(
|
|
1049
|
+
"keep_latest_n must be >= 1",
|
|
1050
|
+
keep_latest_n=keep_latest_n,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
# If neither policy is specified, no-op
|
|
1054
|
+
if max_age_seconds is None and keep_latest_n is None:
|
|
1055
|
+
return 0
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
async with self._pool.acquire() as conn:
|
|
1059
|
+
# Build conditions for deletion
|
|
1060
|
+
conditions: list[str] = []
|
|
1061
|
+
params: list[object] = []
|
|
1062
|
+
|
|
1063
|
+
# Subject filter (applies to all strategies)
|
|
1064
|
+
if subject is not None:
|
|
1065
|
+
conditions.append(f"subject_type = ${len(params) + 1}")
|
|
1066
|
+
params.append(subject.subject_type)
|
|
1067
|
+
conditions.append(f"subject_id = ${len(params) + 1}")
|
|
1068
|
+
params.append(subject.subject_id)
|
|
1069
|
+
|
|
1070
|
+
subject_filter = " AND ".join(conditions) if conditions else "TRUE"
|
|
1071
|
+
|
|
1072
|
+
# Strategy 1: Age-based only (simpler query)
|
|
1073
|
+
if max_age_seconds is not None and keep_latest_n is None:
|
|
1074
|
+
cutoff_time = datetime.now(UTC) - timedelta(seconds=max_age_seconds)
|
|
1075
|
+
params.append(cutoff_time)
|
|
1076
|
+
|
|
1077
|
+
# S608: Safe - subject_filter contains only parameterized placeholders
|
|
1078
|
+
delete_query = f"""
|
|
1079
|
+
DELETE FROM snapshots
|
|
1080
|
+
WHERE {subject_filter}
|
|
1081
|
+
AND created_at < ${len(params)}
|
|
1082
|
+
""" # noqa: S608
|
|
1083
|
+
|
|
1084
|
+
result = await conn.execute(delete_query, *params)
|
|
1085
|
+
# asyncpg returns "DELETE N"
|
|
1086
|
+
deleted_str = str(result).replace("DELETE ", "")
|
|
1087
|
+
return int(deleted_str) if deleted_str.isdigit() else 0
|
|
1088
|
+
|
|
1089
|
+
# Strategy 2: Keep latest N only
|
|
1090
|
+
if keep_latest_n is not None and max_age_seconds is None:
|
|
1091
|
+
params.append(keep_latest_n)
|
|
1092
|
+
|
|
1093
|
+
# Use window function to rank snapshots per subject
|
|
1094
|
+
# S608: Safe - subject_filter contains only parameterized placeholders
|
|
1095
|
+
delete_query = f"""
|
|
1096
|
+
DELETE FROM snapshots
|
|
1097
|
+
WHERE id IN (
|
|
1098
|
+
SELECT id FROM (
|
|
1099
|
+
SELECT id,
|
|
1100
|
+
ROW_NUMBER() OVER (
|
|
1101
|
+
PARTITION BY subject_type, subject_id
|
|
1102
|
+
ORDER BY sequence_number DESC
|
|
1103
|
+
) as rn
|
|
1104
|
+
FROM snapshots
|
|
1105
|
+
WHERE {subject_filter}
|
|
1106
|
+
) ranked
|
|
1107
|
+
WHERE rn > ${len(params)}
|
|
1108
|
+
)
|
|
1109
|
+
""" # noqa: S608
|
|
1110
|
+
|
|
1111
|
+
result = await conn.execute(delete_query, *params)
|
|
1112
|
+
deleted_str = str(result).replace("DELETE ", "")
|
|
1113
|
+
return int(deleted_str) if deleted_str.isdigit() else 0
|
|
1114
|
+
|
|
1115
|
+
# Strategy 3: Combined (both age and count)
|
|
1116
|
+
# Delete if: older than max_age AND NOT in latest N
|
|
1117
|
+
# NOTE: max_age_seconds is validated non-None by strategy check above,
|
|
1118
|
+
# but mypy cannot narrow the Optional[float] type through control flow.
|
|
1119
|
+
cutoff_time = datetime.now(UTC) - timedelta(
|
|
1120
|
+
seconds=max_age_seconds # type: ignore[arg-type] # NOTE: control flow narrowing limitation
|
|
1121
|
+
)
|
|
1122
|
+
params.append(cutoff_time)
|
|
1123
|
+
cutoff_param_idx = len(params)
|
|
1124
|
+
|
|
1125
|
+
params.append(keep_latest_n)
|
|
1126
|
+
keep_n_param_idx = len(params)
|
|
1127
|
+
|
|
1128
|
+
# S608: Safe - subject_filter contains only parameterized placeholders
|
|
1129
|
+
delete_query = f"""
|
|
1130
|
+
DELETE FROM snapshots
|
|
1131
|
+
WHERE id IN (
|
|
1132
|
+
SELECT id FROM (
|
|
1133
|
+
SELECT id,
|
|
1134
|
+
created_at,
|
|
1135
|
+
ROW_NUMBER() OVER (
|
|
1136
|
+
PARTITION BY subject_type, subject_id
|
|
1137
|
+
ORDER BY sequence_number DESC
|
|
1138
|
+
) as rn
|
|
1139
|
+
FROM snapshots
|
|
1140
|
+
WHERE {subject_filter}
|
|
1141
|
+
) ranked
|
|
1142
|
+
WHERE rn > ${keep_n_param_idx}
|
|
1143
|
+
AND created_at < ${cutoff_param_idx}
|
|
1144
|
+
)
|
|
1145
|
+
""" # noqa: S608
|
|
1146
|
+
|
|
1147
|
+
result = await conn.execute(delete_query, *params)
|
|
1148
|
+
deleted_str = str(result).replace("DELETE ", "")
|
|
1149
|
+
return int(deleted_str) if deleted_str.isdigit() else 0
|
|
1150
|
+
|
|
1151
|
+
except ProtocolConfigurationError:
|
|
1152
|
+
# Re-raise configuration validation errors
|
|
1153
|
+
raise
|
|
1154
|
+
except Exception as e:
|
|
1155
|
+
context = ModelInfraErrorContext(
|
|
1156
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
1157
|
+
operation="cleanup_expired",
|
|
1158
|
+
target_name="snapshots",
|
|
1159
|
+
)
|
|
1160
|
+
raise InfraConnectionError(
|
|
1161
|
+
f"Failed to cleanup expired snapshots: {type(e).__name__}",
|
|
1162
|
+
context=context,
|
|
1163
|
+
) from e
|
|
1164
|
+
|
|
1165
|
+
def _row_to_model(self, row: asyncpg.Record) -> ModelSnapshot:
|
|
1166
|
+
"""Convert a database row to a ModelSnapshot.
|
|
1167
|
+
|
|
1168
|
+
Args:
|
|
1169
|
+
row: asyncpg Record from a SELECT query.
|
|
1170
|
+
|
|
1171
|
+
Returns:
|
|
1172
|
+
ModelSnapshot instance populated from the row.
|
|
1173
|
+
"""
|
|
1174
|
+
# asyncpg returns JSONB as dict automatically
|
|
1175
|
+
data = row["data"]
|
|
1176
|
+
if isinstance(data, str):
|
|
1177
|
+
data = json.loads(data)
|
|
1178
|
+
|
|
1179
|
+
return ModelSnapshot(
|
|
1180
|
+
id=row["id"],
|
|
1181
|
+
subject=ModelSubjectRef(
|
|
1182
|
+
subject_type=row["subject_type"],
|
|
1183
|
+
subject_id=row["subject_id"],
|
|
1184
|
+
),
|
|
1185
|
+
data=data,
|
|
1186
|
+
sequence_number=row["sequence_number"],
|
|
1187
|
+
version=row["version"],
|
|
1188
|
+
content_hash=row["content_hash"],
|
|
1189
|
+
created_at=row["created_at"],
|
|
1190
|
+
parent_id=row["parent_id"],
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
async def ensure_schema(self) -> None:
|
|
1194
|
+
"""Create the snapshots table and indexes if they don't exist.
|
|
1195
|
+
|
|
1196
|
+
This method is idempotent and safe to call on every startup.
|
|
1197
|
+
Uses IF NOT EXISTS clauses to avoid errors on existing objects.
|
|
1198
|
+
|
|
1199
|
+
Schema Design:
|
|
1200
|
+
The content_hash column has a UNIQUE partial index to enable
|
|
1201
|
+
atomic idempotency checks via INSERT ON CONFLICT. This eliminates
|
|
1202
|
+
TOCTOU race conditions that would occur with separate SELECT-then-
|
|
1203
|
+
INSERT patterns.
|
|
1204
|
+
|
|
1205
|
+
Constraints:
|
|
1206
|
+
- Primary key on id (UUID)
|
|
1207
|
+
- Unique constraint on (subject_type, subject_id, sequence_number)
|
|
1208
|
+
- Unique partial index on content_hash WHERE content_hash IS NOT NULL
|
|
1209
|
+
|
|
1210
|
+
Raises:
|
|
1211
|
+
InfraConnectionError: If schema creation fails.
|
|
1212
|
+
|
|
1213
|
+
Note:
|
|
1214
|
+
This method uses multi-statement execution via transaction.
|
|
1215
|
+
Each statement is executed separately to work within asyncpg's
|
|
1216
|
+
single-statement limitation.
|
|
1217
|
+
|
|
1218
|
+
Migration Note:
|
|
1219
|
+
If upgrading from a schema with non-unique idx_snapshots_content_hash,
|
|
1220
|
+
the old index will be dropped and replaced with a unique index.
|
|
1221
|
+
Ensure no duplicate content_hash values exist before migration.
|
|
1222
|
+
"""
|
|
1223
|
+
try:
|
|
1224
|
+
async with self._pool.acquire() as conn:
|
|
1225
|
+
# Create table
|
|
1226
|
+
await conn.execute("""
|
|
1227
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
1228
|
+
id UUID PRIMARY KEY,
|
|
1229
|
+
subject_type VARCHAR(255) NOT NULL,
|
|
1230
|
+
subject_id UUID NOT NULL,
|
|
1231
|
+
data JSONB NOT NULL,
|
|
1232
|
+
sequence_number INTEGER NOT NULL,
|
|
1233
|
+
version INTEGER DEFAULT 1,
|
|
1234
|
+
content_hash VARCHAR(128),
|
|
1235
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1236
|
+
parent_id UUID REFERENCES snapshots(id),
|
|
1237
|
+
|
|
1238
|
+
CONSTRAINT snapshots_subject_sequence_unique
|
|
1239
|
+
UNIQUE (subject_type, subject_id, sequence_number)
|
|
1240
|
+
)
|
|
1241
|
+
""")
|
|
1242
|
+
|
|
1243
|
+
# Create subject index
|
|
1244
|
+
await conn.execute("""
|
|
1245
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_subject
|
|
1246
|
+
ON snapshots (subject_type, subject_id, sequence_number DESC)
|
|
1247
|
+
""")
|
|
1248
|
+
|
|
1249
|
+
# Drop old non-unique content_hash index if it exists (for migration).
|
|
1250
|
+
# This is safe because CREATE UNIQUE INDEX IF NOT EXISTS will fail
|
|
1251
|
+
# if a non-unique index with the same name exists.
|
|
1252
|
+
await conn.execute("""
|
|
1253
|
+
DROP INDEX IF EXISTS idx_snapshots_content_hash
|
|
1254
|
+
""")
|
|
1255
|
+
|
|
1256
|
+
# Create UNIQUE partial index on content_hash.
|
|
1257
|
+
# This enables atomic ON CONFLICT upserts and prevents duplicate
|
|
1258
|
+
# content_hash entries, eliminating TOCTOU race conditions.
|
|
1259
|
+
await conn.execute("""
|
|
1260
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshots_content_hash
|
|
1261
|
+
ON snapshots (content_hash) WHERE content_hash IS NOT NULL
|
|
1262
|
+
""")
|
|
1263
|
+
|
|
1264
|
+
logger.info(
|
|
1265
|
+
"Snapshot schema ensured (table and indexes created/verified)"
|
|
1266
|
+
)
|
|
1267
|
+
except Exception as e:
|
|
1268
|
+
context = ModelInfraErrorContext(
|
|
1269
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
1270
|
+
operation="ensure_schema",
|
|
1271
|
+
target_name="snapshots",
|
|
1272
|
+
)
|
|
1273
|
+
raise InfraConnectionError(
|
|
1274
|
+
f"Failed to ensure schema: {type(e).__name__}",
|
|
1275
|
+
context=context,
|
|
1276
|
+
) from e
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
__all__: list[str] = ["StoreSnapshotPostgres"]
|