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,1152 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Topic Category Validator for ONEX Execution Shape Validation.
|
|
4
|
+
|
|
5
|
+
Validates that message categories match their topic naming patterns in the
|
|
6
|
+
ONEX event-driven architecture. This ensures architectural consistency by
|
|
7
|
+
enforcing topic naming conventions at both static analysis and runtime.
|
|
8
|
+
|
|
9
|
+
Topic Naming Conventions:
|
|
10
|
+
- EVENTs: Read from `<domain>.events` topics (e.g., `order.events`)
|
|
11
|
+
- COMMANDs: Read from `<domain>.commands` topics (e.g., `order.commands`)
|
|
12
|
+
- INTENTs: Read from `<domain>.intents` topics (e.g., `checkout.intents`)
|
|
13
|
+
- PROJECTIONs: Can be anywhere (internal state projections)
|
|
14
|
+
|
|
15
|
+
Validation Modes:
|
|
16
|
+
- Runtime: Validate messages as they flow through the system
|
|
17
|
+
- Static (AST): Analyze Python files for topic/category mismatches in CI
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
>>> from omnibase_infra.validation import TopicCategoryValidator
|
|
21
|
+
>>> from omnibase_infra.enums import EnumMessageCategory
|
|
22
|
+
>>>
|
|
23
|
+
>>> validator = TopicCategoryValidator()
|
|
24
|
+
>>> result = validator.validate_message_topic(
|
|
25
|
+
... EnumMessageCategory.EVENT,
|
|
26
|
+
... "order.commands", # Wrong topic for events
|
|
27
|
+
... )
|
|
28
|
+
>>> if result is not None:
|
|
29
|
+
... print(f"Violation: {result.message}")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ast
|
|
35
|
+
import logging
|
|
36
|
+
import re
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from omnibase_infra.enums import (
|
|
40
|
+
EnumExecutionShapeViolation,
|
|
41
|
+
EnumMessageCategory,
|
|
42
|
+
EnumNodeArchetype,
|
|
43
|
+
EnumNodeOutputType,
|
|
44
|
+
EnumValidationSeverity,
|
|
45
|
+
)
|
|
46
|
+
from omnibase_infra.models.validation.model_execution_shape_violation import (
|
|
47
|
+
ModelExecutionShapeViolationResult,
|
|
48
|
+
)
|
|
49
|
+
from omnibase_infra.types import MessageOutputCategory
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
# Topic naming patterns for each message category
|
|
54
|
+
# Matches patterns like: order.events, user-service.commands, checkout.intents
|
|
55
|
+
#
|
|
56
|
+
# DESIGN DECISION - Regex vs Substring Matching:
|
|
57
|
+
# We use regex patterns instead of simple substring/suffix checks for validation
|
|
58
|
+
# because:
|
|
59
|
+
#
|
|
60
|
+
# 1. **Domain validation**: The pattern `^[\w-]+\.` ensures the domain portion
|
|
61
|
+
# contains only valid characters (alphanumeric, underscore, hyphen). A simple
|
|
62
|
+
# `.endswith(".events")` check would accept malformed topics like "...events"
|
|
63
|
+
# or topics with invalid characters in the domain.
|
|
64
|
+
#
|
|
65
|
+
# 2. **Exactness**: The `^` and `$` anchors ensure we match the ENTIRE topic name.
|
|
66
|
+
# This prevents false positives on topics like "order.events.dlq" or
|
|
67
|
+
# "prefix.order.events" which would incorrectly match a suffix check.
|
|
68
|
+
#
|
|
69
|
+
# 3. **Consistency**: All patterns use the same validation logic, making it
|
|
70
|
+
# easier to reason about and extend (e.g., adding new patterns for other
|
|
71
|
+
# topic types).
|
|
72
|
+
#
|
|
73
|
+
# Trade-off: Regex is slightly slower than substring checks, but the validation
|
|
74
|
+
# accuracy and correctness benefits outweigh the performance cost for this
|
|
75
|
+
# use case (topic names are short strings, validation happens at configuration
|
|
76
|
+
# time, not in hot paths).
|
|
77
|
+
TOPIC_CATEGORY_PATTERNS: dict[EnumMessageCategory, re.Pattern[str]] = {
|
|
78
|
+
EnumMessageCategory.EVENT: re.compile(r"^[\w-]+\.events$"),
|
|
79
|
+
EnumMessageCategory.COMMAND: re.compile(r"^[\w-]+\.commands$"),
|
|
80
|
+
EnumMessageCategory.INTENT: re.compile(r"^[\w-]+\.intents$"),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Topic suffix mapping for each message category
|
|
84
|
+
# Note: PROJECTION uses EnumNodeOutputType because it's a node output type, not a message category.
|
|
85
|
+
# Projections are internal state outputs from REDUCER nodes, not routed messages on Kafka topics.
|
|
86
|
+
TOPIC_SUFFIXES: dict[MessageOutputCategory, str] = {
|
|
87
|
+
EnumMessageCategory.EVENT: "events",
|
|
88
|
+
EnumMessageCategory.COMMAND: "commands",
|
|
89
|
+
EnumMessageCategory.INTENT: "intents",
|
|
90
|
+
EnumNodeOutputType.PROJECTION: "", # Projections have no suffix requirement
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Node archetype to expected message categories mapping
|
|
94
|
+
#
|
|
95
|
+
# DUAL PURPOSE EXPLANATION:
|
|
96
|
+
# This mapping serves two purposes in validation:
|
|
97
|
+
# 1. INPUT VALIDATION: Which message categories each node archetype can CONSUME
|
|
98
|
+
# (e.g., REDUCER can consume EVENT messages from *.events topics)
|
|
99
|
+
# 2. OUTPUT VALIDATION: Which output types each node archetype can PRODUCE
|
|
100
|
+
# (e.g., REDUCER can produce PROJECTION outputs)
|
|
101
|
+
#
|
|
102
|
+
# Why the union type (MessageOutputCategory)?
|
|
103
|
+
# - EnumMessageCategory values (EVENT, COMMAND, INTENT) are for message routing
|
|
104
|
+
# - EnumNodeOutputType values (including PROJECTION) are for node output validation
|
|
105
|
+
# - REDUCER is unique: it consumes EVENTs (message category) and produces PROJECTIONs
|
|
106
|
+
# (node output type that is NOT routed as a message)
|
|
107
|
+
#
|
|
108
|
+
# See ADR: docs/decisions/adr-enum-message-category-vs-node-output-type.md
|
|
109
|
+
NODE_ARCHETYPE_EXPECTED_CATEGORIES: dict[
|
|
110
|
+
EnumNodeArchetype, list[MessageOutputCategory]
|
|
111
|
+
] = {
|
|
112
|
+
EnumNodeArchetype.EFFECT: [
|
|
113
|
+
EnumMessageCategory.COMMAND,
|
|
114
|
+
EnumMessageCategory.EVENT,
|
|
115
|
+
],
|
|
116
|
+
EnumNodeArchetype.COMPUTE: [
|
|
117
|
+
EnumMessageCategory.EVENT,
|
|
118
|
+
EnumMessageCategory.COMMAND,
|
|
119
|
+
EnumMessageCategory.INTENT,
|
|
120
|
+
],
|
|
121
|
+
EnumNodeArchetype.REDUCER: [
|
|
122
|
+
EnumMessageCategory.EVENT,
|
|
123
|
+
EnumNodeOutputType.PROJECTION,
|
|
124
|
+
],
|
|
125
|
+
EnumNodeArchetype.ORCHESTRATOR: [
|
|
126
|
+
EnumMessageCategory.EVENT,
|
|
127
|
+
EnumMessageCategory.COMMAND,
|
|
128
|
+
EnumMessageCategory.INTENT,
|
|
129
|
+
],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TopicCategoryValidator:
|
|
134
|
+
"""Validator for ensuring message categories match topic patterns.
|
|
135
|
+
|
|
136
|
+
Enforces ONEX topic naming conventions by validating that:
|
|
137
|
+
- Events are only read from `*.events` topics
|
|
138
|
+
- Commands are only read from `*.commands` topics
|
|
139
|
+
- Intents are only read from `*.intents` topics
|
|
140
|
+
- Projections can exist anywhere (no naming constraint)
|
|
141
|
+
|
|
142
|
+
This validator supports both runtime validation (for message processing)
|
|
143
|
+
and subscription validation (for handler configuration).
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
patterns: Compiled regex patterns for topic validation.
|
|
147
|
+
suffixes: Expected topic suffixes for each message category.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> validator = TopicCategoryValidator()
|
|
151
|
+
>>> # Valid: Event on events topic
|
|
152
|
+
>>> result = validator.validate_message_topic(
|
|
153
|
+
... EnumMessageCategory.EVENT, "order.events"
|
|
154
|
+
... )
|
|
155
|
+
>>> assert result is None # No violation
|
|
156
|
+
>>>
|
|
157
|
+
>>> # Invalid: Event on commands topic
|
|
158
|
+
>>> result = validator.validate_message_topic(
|
|
159
|
+
... EnumMessageCategory.EVENT, "order.commands"
|
|
160
|
+
... )
|
|
161
|
+
>>> assert result is not None # Violation detected
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self) -> None:
|
|
165
|
+
"""Initialize the topic category validator with default patterns."""
|
|
166
|
+
self.patterns = TOPIC_CATEGORY_PATTERNS
|
|
167
|
+
self.suffixes = TOPIC_SUFFIXES
|
|
168
|
+
self.archetype_categories = NODE_ARCHETYPE_EXPECTED_CATEGORIES
|
|
169
|
+
|
|
170
|
+
def validate_message_topic(
|
|
171
|
+
self,
|
|
172
|
+
message_category: MessageOutputCategory,
|
|
173
|
+
topic_name: str,
|
|
174
|
+
) -> ModelExecutionShapeViolationResult | None:
|
|
175
|
+
"""Validate that a message category matches its topic pattern.
|
|
176
|
+
|
|
177
|
+
Checks if the message category is being read from or written to
|
|
178
|
+
an appropriately named topic according to ONEX conventions.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
message_category: The category of the message (EVENT, COMMAND, etc.)
|
|
182
|
+
or node output type (PROJECTION). Projections have no topic
|
|
183
|
+
naming constraint and are always valid.
|
|
184
|
+
topic_name: The Kafka topic name being used.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A ModelExecutionShapeViolationResult if there's a mismatch,
|
|
188
|
+
or None if the message/topic combination is valid.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> validator = TopicCategoryValidator()
|
|
192
|
+
>>> # This should pass - event on events topic
|
|
193
|
+
>>> result = validator.validate_message_topic(
|
|
194
|
+
... EnumMessageCategory.EVENT, "order.events"
|
|
195
|
+
... )
|
|
196
|
+
>>> assert result is None
|
|
197
|
+
>>>
|
|
198
|
+
>>> # This should fail - event on commands topic
|
|
199
|
+
>>> result = validator.validate_message_topic(
|
|
200
|
+
... EnumMessageCategory.EVENT, "order.commands"
|
|
201
|
+
... )
|
|
202
|
+
>>> assert result is not None
|
|
203
|
+
>>> assert result.violation_type == EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH
|
|
204
|
+
"""
|
|
205
|
+
# Projections have no topic naming constraint (they are node outputs, not routed messages)
|
|
206
|
+
if message_category == EnumNodeOutputType.PROJECTION:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
# Get the expected pattern for this category
|
|
210
|
+
# Note: patterns dict uses EnumMessageCategory keys. EnumNodeOutputType values
|
|
211
|
+
# won't match (different enum types even with same string values), so lookup
|
|
212
|
+
# will return None for any EnumNodeOutputType passed here. This is correct
|
|
213
|
+
# behavior - we only validate EnumMessageCategory values against topic patterns.
|
|
214
|
+
if isinstance(message_category, EnumMessageCategory):
|
|
215
|
+
expected_pattern = self.patterns.get(message_category)
|
|
216
|
+
else:
|
|
217
|
+
# EnumNodeOutputType values (other than PROJECTION which is handled above)
|
|
218
|
+
# don't have topic naming constraints
|
|
219
|
+
expected_pattern = None
|
|
220
|
+
if expected_pattern is None:
|
|
221
|
+
# Unknown category or node output type - no topic constraint
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# Check if topic matches the expected pattern
|
|
225
|
+
if expected_pattern.match(topic_name):
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# Violation detected - category doesn't match topic pattern
|
|
229
|
+
expected_suffix = self.suffixes.get(message_category, "unknown")
|
|
230
|
+
return ModelExecutionShapeViolationResult(
|
|
231
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
232
|
+
node_archetype=None, # Unknown at runtime validation without handler context
|
|
233
|
+
file_path="<runtime>", # Runtime validation has no file context
|
|
234
|
+
line_number=1,
|
|
235
|
+
message=(
|
|
236
|
+
f"Topic category mismatch: Message category '{message_category.name}' "
|
|
237
|
+
f"(EnumMessageCategory.{message_category.name}) requires a topic matching "
|
|
238
|
+
f"pattern '<domain>.{expected_suffix}'. Found topic: '{topic_name}'. "
|
|
239
|
+
f"Expected pattern: '*.{expected_suffix}' (e.g., 'order.{expected_suffix}')."
|
|
240
|
+
),
|
|
241
|
+
severity=EnumValidationSeverity.ERROR,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def validate_subscription(
|
|
245
|
+
self,
|
|
246
|
+
node_archetype: EnumNodeArchetype,
|
|
247
|
+
subscribed_topics: list[str],
|
|
248
|
+
expected_categories: list[MessageOutputCategory],
|
|
249
|
+
) -> list[ModelExecutionShapeViolationResult]:
|
|
250
|
+
"""Validate that handler subscriptions match expected message types.
|
|
251
|
+
|
|
252
|
+
Checks if a handler is subscribed to topics that match the message
|
|
253
|
+
categories it should be consuming based on ONEX architecture rules.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
node_archetype: The node archetype (EFFECT, COMPUTE, REDUCER, ORCHESTRATOR).
|
|
257
|
+
subscribed_topics: List of Kafka topics the handler subscribes to.
|
|
258
|
+
expected_categories: List of message categories or node output types
|
|
259
|
+
the handler should process (e.g., EVENT, COMMAND, PROJECTION).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of violations for any topic that doesn't match expected categories.
|
|
263
|
+
Empty list if all subscriptions are valid or if inputs are invalid types.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> validator = TopicCategoryValidator()
|
|
267
|
+
>>> violations = validator.validate_subscription(
|
|
268
|
+
... EnumNodeArchetype.REDUCER,
|
|
269
|
+
... ["order.events", "order.commands"], # commands not valid for reducer
|
|
270
|
+
... [EnumMessageCategory.EVENT, EnumNodeOutputType.PROJECTION],
|
|
271
|
+
... )
|
|
272
|
+
>>> assert len(violations) == 1
|
|
273
|
+
>>> assert "order.commands" in violations[0].message
|
|
274
|
+
"""
|
|
275
|
+
violations: list[ModelExecutionShapeViolationResult] = []
|
|
276
|
+
|
|
277
|
+
# Defensive type checks for list inputs
|
|
278
|
+
if not isinstance(subscribed_topics, list):
|
|
279
|
+
return violations
|
|
280
|
+
if not isinstance(expected_categories, list):
|
|
281
|
+
return violations
|
|
282
|
+
|
|
283
|
+
for topic in subscribed_topics:
|
|
284
|
+
# Skip non-string topics
|
|
285
|
+
if not isinstance(topic, str):
|
|
286
|
+
continue
|
|
287
|
+
# Determine what category this topic implies
|
|
288
|
+
inferred_category = self._infer_category_from_topic(topic)
|
|
289
|
+
|
|
290
|
+
if inferred_category is None:
|
|
291
|
+
# Topic doesn't follow any known pattern - warning
|
|
292
|
+
violations.append(
|
|
293
|
+
ModelExecutionShapeViolationResult(
|
|
294
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
295
|
+
node_archetype=node_archetype,
|
|
296
|
+
file_path="<runtime>",
|
|
297
|
+
line_number=1,
|
|
298
|
+
message=(
|
|
299
|
+
f"Topic naming convention violation: Topic '{topic}' does not match "
|
|
300
|
+
f"ONEX naming conventions. Node archetype: '{node_archetype.name}' "
|
|
301
|
+
f"(EnumNodeArchetype.{node_archetype.name}). Expected topic patterns: "
|
|
302
|
+
f"'<domain>.events', '<domain>.commands', or '<domain>.intents'. "
|
|
303
|
+
f"Example valid topics: 'order.events', 'user.commands'."
|
|
304
|
+
),
|
|
305
|
+
severity=EnumValidationSeverity.WARNING,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# Check if the inferred category is in the expected categories
|
|
311
|
+
if inferred_category not in expected_categories:
|
|
312
|
+
expected_names = [c.name for c in expected_categories]
|
|
313
|
+
violations.append(
|
|
314
|
+
ModelExecutionShapeViolationResult(
|
|
315
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
316
|
+
node_archetype=node_archetype,
|
|
317
|
+
file_path="<runtime>",
|
|
318
|
+
line_number=1,
|
|
319
|
+
message=(
|
|
320
|
+
f"Subscription category mismatch: Node archetype '{node_archetype.name}' "
|
|
321
|
+
f"(EnumNodeArchetype.{node_archetype.name}) subscribed to topic '{topic}' "
|
|
322
|
+
f"which implies '{inferred_category.name}' messages. "
|
|
323
|
+
f"Expected message categories for this archetype: [{', '.join(expected_names)}]. "
|
|
324
|
+
f"Found: {inferred_category.name}. "
|
|
325
|
+
f"Review NODE_ARCHETYPE_EXPECTED_CATEGORIES for valid subscriptions."
|
|
326
|
+
),
|
|
327
|
+
severity=EnumValidationSeverity.ERROR,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return violations
|
|
332
|
+
|
|
333
|
+
def extract_domain_from_topic(self, topic: str) -> str | None:
|
|
334
|
+
"""Extract the domain name from a topic.
|
|
335
|
+
|
|
336
|
+
Parses a topic name and returns the domain prefix before the
|
|
337
|
+
category suffix (events, commands, intents).
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
topic: The Kafka topic name (e.g., 'order.events', 'user-service.commands').
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The domain portion of the topic name, or None if the topic
|
|
344
|
+
doesn't follow the expected pattern.
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> validator = TopicCategoryValidator()
|
|
348
|
+
>>> validator.extract_domain_from_topic("order.events")
|
|
349
|
+
'order'
|
|
350
|
+
>>> validator.extract_domain_from_topic("user-service.commands")
|
|
351
|
+
'user-service'
|
|
352
|
+
>>> validator.extract_domain_from_topic("invalid-topic")
|
|
353
|
+
None
|
|
354
|
+
"""
|
|
355
|
+
for suffix in ("events", "commands", "intents"):
|
|
356
|
+
if topic.endswith(f".{suffix}"):
|
|
357
|
+
domain = topic[: -(len(suffix) + 1)] # Remove '.' + suffix
|
|
358
|
+
if domain:
|
|
359
|
+
return domain
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def get_expected_topic_suffix(
|
|
363
|
+
self,
|
|
364
|
+
category: MessageOutputCategory,
|
|
365
|
+
) -> str:
|
|
366
|
+
"""Get the expected topic suffix for a message category or node output type.
|
|
367
|
+
|
|
368
|
+
Returns the topic suffix that should be used for topics containing
|
|
369
|
+
messages of the specified category or output type.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
category: The message category (EVENT, COMMAND, INTENT) or node
|
|
373
|
+
output type (PROJECTION). Projections have no suffix requirement.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
The expected topic suffix ('events', 'commands', 'intents', or ''
|
|
377
|
+
for projections).
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
>>> validator = TopicCategoryValidator()
|
|
381
|
+
>>> validator.get_expected_topic_suffix(EnumMessageCategory.EVENT)
|
|
382
|
+
'events'
|
|
383
|
+
>>> validator.get_expected_topic_suffix(EnumMessageCategory.COMMAND)
|
|
384
|
+
'commands'
|
|
385
|
+
"""
|
|
386
|
+
return self.suffixes.get(category, "")
|
|
387
|
+
|
|
388
|
+
def _infer_category_from_topic(
|
|
389
|
+
self,
|
|
390
|
+
topic: str,
|
|
391
|
+
) -> EnumMessageCategory | None:
|
|
392
|
+
"""Infer the message category from a topic name.
|
|
393
|
+
|
|
394
|
+
Internal method that determines what type of messages a topic
|
|
395
|
+
is expected to contain based on its naming pattern.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
topic: The Kafka topic name.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The inferred message category, or None if the topic doesn't
|
|
402
|
+
match any known pattern.
|
|
403
|
+
"""
|
|
404
|
+
for category, pattern in self.patterns.items():
|
|
405
|
+
if pattern.match(topic):
|
|
406
|
+
return category
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class TopicCategoryASTVisitor(ast.NodeVisitor):
|
|
411
|
+
"""AST visitor for detecting topic/category mismatches in Python code.
|
|
412
|
+
|
|
413
|
+
Analyzes Python source files to detect potential mismatches between
|
|
414
|
+
message categories and topic names used in producer/consumer calls.
|
|
415
|
+
|
|
416
|
+
This visitor looks for patterns like:
|
|
417
|
+
- consumer.subscribe("order.events") with handler processing commands
|
|
418
|
+
- producer.send("user.commands", event_data) - sending event to commands topic
|
|
419
|
+
|
|
420
|
+
Attributes:
|
|
421
|
+
violations: List of detected violations.
|
|
422
|
+
file_path: Path to the file being analyzed.
|
|
423
|
+
validator: TopicCategoryValidator instance for validation logic.
|
|
424
|
+
current_node_archetype: Inferred node archetype from class context.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def __init__(
|
|
428
|
+
self,
|
|
429
|
+
file_path: Path,
|
|
430
|
+
validator: TopicCategoryValidator,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Initialize the AST visitor.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
file_path: Path to the file being analyzed.
|
|
436
|
+
validator: TopicCategoryValidator instance for validation logic.
|
|
437
|
+
"""
|
|
438
|
+
self.violations: list[ModelExecutionShapeViolationResult] = []
|
|
439
|
+
self.file_path = file_path
|
|
440
|
+
self.validator = validator
|
|
441
|
+
self.current_node_archetype: EnumNodeArchetype | None = None
|
|
442
|
+
self.current_class_name: str | None = None
|
|
443
|
+
|
|
444
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
|
|
445
|
+
"""Visit class definitions to infer node archetype from class name.
|
|
446
|
+
|
|
447
|
+
Infers the node archetype based on class name conventions:
|
|
448
|
+
- *Effect -> EFFECT
|
|
449
|
+
- *Compute -> COMPUTE
|
|
450
|
+
- *Reducer -> REDUCER
|
|
451
|
+
- *Orchestrator -> ORCHESTRATOR
|
|
452
|
+
|
|
453
|
+
PATTERN MATCHING ORDER:
|
|
454
|
+
The order of keyword checks (effect, compute, reducer, orchestrator)
|
|
455
|
+
matters when a class name contains multiple keywords. The checks are
|
|
456
|
+
ordered by specificity of the ONEX node archetypes:
|
|
457
|
+
|
|
458
|
+
1. "effect" - Checked first because EFFECT nodes are most common
|
|
459
|
+
for I/O operations and have the most restrictive constraints
|
|
460
|
+
2. "compute" - Pure computation nodes, checked second
|
|
461
|
+
3. "reducer" - State projection nodes with strict determinism rules
|
|
462
|
+
4. "orchestrator" - Workflow coordination nodes
|
|
463
|
+
|
|
464
|
+
Example edge cases:
|
|
465
|
+
- "EffectReducer" would be classified as EFFECT (first match wins)
|
|
466
|
+
- "ComputeOrchestrator" would be classified as COMPUTE
|
|
467
|
+
|
|
468
|
+
In practice, handler class names should be unambiguous and follow
|
|
469
|
+
the convention of using a single node archetype in the name suffix
|
|
470
|
+
(e.g., "OrderEffectHandler", not "OrderEffectReducer").
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
node: The AST ClassDef node.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
The visited node.
|
|
477
|
+
"""
|
|
478
|
+
old_archetype = self.current_node_archetype
|
|
479
|
+
old_class_name = self.current_class_name
|
|
480
|
+
|
|
481
|
+
self.current_class_name = node.name
|
|
482
|
+
|
|
483
|
+
# Infer node archetype from class name.
|
|
484
|
+
# Order matters: first match wins for ambiguous names.
|
|
485
|
+
class_name = node.name.lower()
|
|
486
|
+
if "effect" in class_name:
|
|
487
|
+
self.current_node_archetype = EnumNodeArchetype.EFFECT
|
|
488
|
+
elif "compute" in class_name:
|
|
489
|
+
self.current_node_archetype = EnumNodeArchetype.COMPUTE
|
|
490
|
+
elif "reducer" in class_name:
|
|
491
|
+
self.current_node_archetype = EnumNodeArchetype.REDUCER
|
|
492
|
+
elif "orchestrator" in class_name:
|
|
493
|
+
self.current_node_archetype = EnumNodeArchetype.ORCHESTRATOR
|
|
494
|
+
|
|
495
|
+
# Visit children
|
|
496
|
+
self.generic_visit(node)
|
|
497
|
+
|
|
498
|
+
# Restore context
|
|
499
|
+
self.current_node_archetype = old_archetype
|
|
500
|
+
self.current_class_name = old_class_name
|
|
501
|
+
|
|
502
|
+
return node
|
|
503
|
+
|
|
504
|
+
def visit_Call(self, node: ast.Call) -> ast.AST:
|
|
505
|
+
"""Visit function calls to detect topic usage patterns.
|
|
506
|
+
|
|
507
|
+
Looks for patterns like:
|
|
508
|
+
- consumer.subscribe("topic_name")
|
|
509
|
+
- producer.send("topic_name", data)
|
|
510
|
+
- event_bus.publish("topic_name", message)
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
node: The AST Call node.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
The visited node.
|
|
517
|
+
"""
|
|
518
|
+
# Check for subscribe/send/publish method calls
|
|
519
|
+
if isinstance(node.func, ast.Attribute):
|
|
520
|
+
method_name = node.func.attr
|
|
521
|
+
if method_name in ("subscribe", "send", "publish", "produce"):
|
|
522
|
+
self._check_topic_call(node, method_name)
|
|
523
|
+
|
|
524
|
+
self.generic_visit(node)
|
|
525
|
+
return node
|
|
526
|
+
|
|
527
|
+
def _check_topic_call(
|
|
528
|
+
self,
|
|
529
|
+
node: ast.Call,
|
|
530
|
+
method_name: str,
|
|
531
|
+
) -> None:
|
|
532
|
+
"""Check a topic-related method call for category mismatches.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
node: The AST Call node.
|
|
536
|
+
method_name: The name of the method being called.
|
|
537
|
+
"""
|
|
538
|
+
# Extract topic name from first argument (if string literal)
|
|
539
|
+
if not node.args:
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
first_arg = node.args[0]
|
|
543
|
+
topic_name: str | None = None
|
|
544
|
+
|
|
545
|
+
if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
|
|
546
|
+
topic_name = first_arg.value
|
|
547
|
+
elif isinstance(first_arg, ast.JoinedStr):
|
|
548
|
+
# f-string handling - extract and validate static parts carefully.
|
|
549
|
+
#
|
|
550
|
+
# LIMITATION: f-strings with interpolated values (e.g., f"{domain}.events")
|
|
551
|
+
# only yield partial static content. We must avoid false positives/negatives
|
|
552
|
+
# from incomplete topic names like ".events" or "order." that could falsely
|
|
553
|
+
# match or miss patterns.
|
|
554
|
+
#
|
|
555
|
+
# Strategy:
|
|
556
|
+
# 1. Extract all static parts from the f-string
|
|
557
|
+
# 2. Check if result forms a COMPLETE valid topic pattern (domain.suffix)
|
|
558
|
+
# 3. If we only get a partial fragment (starts with "." or ends with "."),
|
|
559
|
+
# skip validation for this f-string - we can't reliably validate it
|
|
560
|
+
# 4. If no static parts exist, skip validation entirely
|
|
561
|
+
topic_name = self._extract_topic_from_fstring(first_arg)
|
|
562
|
+
elif isinstance(first_arg, ast.BinOp) and isinstance(first_arg.op, ast.Add):
|
|
563
|
+
# String concatenation handling (e.g., "order" + ".events")
|
|
564
|
+
#
|
|
565
|
+
# LIMITATION: String concatenation with variables cannot be fully resolved
|
|
566
|
+
# at static analysis time. We use the same conservative approach as f-strings.
|
|
567
|
+
topic_name = self._extract_topic_from_binop(first_arg)
|
|
568
|
+
|
|
569
|
+
if topic_name is None:
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
# Infer the category from the topic
|
|
573
|
+
inferred_category = self.validator._infer_category_from_topic(topic_name)
|
|
574
|
+
|
|
575
|
+
if inferred_category is None:
|
|
576
|
+
# Topic doesn't follow naming convention - add warning
|
|
577
|
+
# Use current_node_archetype if available (from class context), otherwise None
|
|
578
|
+
archetype_context = (
|
|
579
|
+
f" Node archetype: '{self.current_node_archetype.name}' "
|
|
580
|
+
f"(EnumNodeArchetype.{self.current_node_archetype.name})."
|
|
581
|
+
if self.current_node_archetype
|
|
582
|
+
else ""
|
|
583
|
+
)
|
|
584
|
+
self.violations.append(
|
|
585
|
+
ModelExecutionShapeViolationResult(
|
|
586
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
587
|
+
node_archetype=self.current_node_archetype, # May be None if outside handler class
|
|
588
|
+
file_path=str(self.file_path.absolute()),
|
|
589
|
+
line_number=node.lineno,
|
|
590
|
+
message=(
|
|
591
|
+
f"Topic naming convention violation at line {node.lineno}: "
|
|
592
|
+
f"Topic '{topic_name}' in {method_name}() call does not match ONEX "
|
|
593
|
+
f"naming conventions.{archetype_context} Expected topic patterns: "
|
|
594
|
+
f"'<domain>.events', '<domain>.commands', or '<domain>.intents'. "
|
|
595
|
+
f"Example: 'order.events', 'user.commands'."
|
|
596
|
+
),
|
|
597
|
+
severity=EnumValidationSeverity.WARNING,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# If we have handler context, validate the subscription makes sense
|
|
603
|
+
if self.current_node_archetype is not None:
|
|
604
|
+
expected_categories = self.validator.archetype_categories.get(
|
|
605
|
+
self.current_node_archetype, []
|
|
606
|
+
)
|
|
607
|
+
if inferred_category not in expected_categories:
|
|
608
|
+
expected_names = [c.name for c in expected_categories]
|
|
609
|
+
self.violations.append(
|
|
610
|
+
ModelExecutionShapeViolationResult(
|
|
611
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
612
|
+
node_archetype=self.current_node_archetype,
|
|
613
|
+
file_path=str(self.file_path.absolute()),
|
|
614
|
+
line_number=node.lineno,
|
|
615
|
+
message=(
|
|
616
|
+
f"Topic category mismatch at line {node.lineno}: "
|
|
617
|
+
f"Handler '{self.current_class_name or 'unknown'}' with node archetype "
|
|
618
|
+
f"'{self.current_node_archetype.name}' (EnumNodeArchetype.{self.current_node_archetype.name}) "
|
|
619
|
+
f"uses topic '{topic_name}' in {method_name}() call. Topic implies "
|
|
620
|
+
f"'{inferred_category.name}' messages. Expected categories for this "
|
|
621
|
+
f"archetype: [{', '.join(expected_names)}]. Found: {inferred_category.name}."
|
|
622
|
+
),
|
|
623
|
+
severity=EnumValidationSeverity.ERROR,
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Check for specific anti-patterns
|
|
628
|
+
self._check_send_patterns(node, method_name, topic_name, inferred_category)
|
|
629
|
+
|
|
630
|
+
def _check_send_patterns(
|
|
631
|
+
self,
|
|
632
|
+
node: ast.Call,
|
|
633
|
+
method_name: str,
|
|
634
|
+
topic_name: str,
|
|
635
|
+
topic_category: EnumMessageCategory,
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Check for anti-patterns in send/publish calls.
|
|
638
|
+
|
|
639
|
+
Looks for patterns like sending events to command topics or
|
|
640
|
+
sending commands to event topics.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
node: The AST Call node.
|
|
644
|
+
method_name: The name of the method being called.
|
|
645
|
+
topic_name: The topic name from the call.
|
|
646
|
+
topic_category: The inferred category from the topic name.
|
|
647
|
+
"""
|
|
648
|
+
if method_name not in ("send", "publish", "produce"):
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
if len(node.args) < 2:
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
# Try to infer message type from the second argument (the message/data)
|
|
655
|
+
second_arg = node.args[1]
|
|
656
|
+
message_hint = self._infer_message_category_from_expr(second_arg)
|
|
657
|
+
|
|
658
|
+
if message_hint is not None and message_hint != topic_category:
|
|
659
|
+
# Use current_node_archetype if available (from class context), otherwise None
|
|
660
|
+
expected_topic_suffix = self.validator.suffixes.get(message_hint, "unknown")
|
|
661
|
+
archetype_context = (
|
|
662
|
+
f" Node archetype: '{self.current_node_archetype.name}' "
|
|
663
|
+
f"(EnumNodeArchetype.{self.current_node_archetype.name})."
|
|
664
|
+
if self.current_node_archetype
|
|
665
|
+
else ""
|
|
666
|
+
)
|
|
667
|
+
self.violations.append(
|
|
668
|
+
ModelExecutionShapeViolationResult(
|
|
669
|
+
violation_type=EnumExecutionShapeViolation.TOPIC_CATEGORY_MISMATCH,
|
|
670
|
+
node_archetype=self.current_node_archetype, # May be None if outside handler class
|
|
671
|
+
file_path=str(self.file_path.absolute()),
|
|
672
|
+
line_number=node.lineno,
|
|
673
|
+
message=(
|
|
674
|
+
f"Message-topic category mismatch at line {node.lineno}: Message appears "
|
|
675
|
+
f"to be '{message_hint.name}' type (EnumMessageCategory.{message_hint.name}) "
|
|
676
|
+
f"but is being sent to topic '{topic_name}' which expects "
|
|
677
|
+
f"'{topic_category.name}' messages.{archetype_context} "
|
|
678
|
+
f"Expected topic pattern for {message_hint.name}: '*.{expected_topic_suffix}'."
|
|
679
|
+
),
|
|
680
|
+
severity=EnumValidationSeverity.ERROR,
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def _infer_message_category_from_expr(
|
|
685
|
+
self,
|
|
686
|
+
node: ast.expr,
|
|
687
|
+
) -> EnumMessageCategory | None:
|
|
688
|
+
"""Attempt to infer message category from an expression.
|
|
689
|
+
|
|
690
|
+
Uses naming conventions to guess the message category. Patterns are
|
|
691
|
+
checked in order of specificity to minimize false positives:
|
|
692
|
+
|
|
693
|
+
1. **Suffix patterns** (most reliable):
|
|
694
|
+
- ``*Event``, ``*Created``, ``*Updated``, ``*Deleted`` -> EVENT
|
|
695
|
+
- ``*Command`` -> COMMAND
|
|
696
|
+
- ``*Intent`` -> INTENT
|
|
697
|
+
|
|
698
|
+
2. **Prefix patterns** (for CQRS-style naming):
|
|
699
|
+
- ``Create*``, ``Update*``, ``Delete*``, ``Execute*`` -> COMMAND
|
|
700
|
+
|
|
701
|
+
Known Limitations:
|
|
702
|
+
- **False positives**: Names like ``EventEmitter`` or ``CommandLine``
|
|
703
|
+
may be incorrectly classified as message types.
|
|
704
|
+
- **Order dependence**: A name ending in both ``Created`` and
|
|
705
|
+
containing ``Command`` (e.g., ``CommandCreated``) will be
|
|
706
|
+
classified as EVENT (suffix match first).
|
|
707
|
+
- Substring matching on prefixes is less reliable than suffix matching.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
node: The AST expression node.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
The inferred message category, or None if unable to determine.
|
|
714
|
+
"""
|
|
715
|
+
name: str | None = None
|
|
716
|
+
|
|
717
|
+
if isinstance(node, ast.Name):
|
|
718
|
+
name = node.id
|
|
719
|
+
elif isinstance(node, ast.Call):
|
|
720
|
+
if isinstance(node.func, ast.Name):
|
|
721
|
+
name = node.func.id
|
|
722
|
+
elif isinstance(node.func, ast.Attribute):
|
|
723
|
+
name = node.func.attr
|
|
724
|
+
|
|
725
|
+
if name is None:
|
|
726
|
+
return None
|
|
727
|
+
|
|
728
|
+
# ==================================================================
|
|
729
|
+
# Pattern Matching Order: By Specificity (most specific first)
|
|
730
|
+
# ==================================================================
|
|
731
|
+
#
|
|
732
|
+
# Phase 1: Check suffix patterns (most reliable, fewest false positives)
|
|
733
|
+
# Suffix matching is preferred because message types conventionally
|
|
734
|
+
# END with their category: OrderCreatedEvent, CreateOrderCommand
|
|
735
|
+
#
|
|
736
|
+
# IMPORTANT: Suffix order matters! Longer/more-specific suffixes are
|
|
737
|
+
# checked first to avoid partial matches. For example:
|
|
738
|
+
# - "OrderCreatedEvent" should match "Event" suffix (not just "Created")
|
|
739
|
+
# - The suffix list is ordered by semantic specificity, not length
|
|
740
|
+
event_suffixes = ("Event", "Created", "Updated", "Deleted", "Occurred")
|
|
741
|
+
for suffix in event_suffixes:
|
|
742
|
+
if name.endswith(suffix):
|
|
743
|
+
return EnumMessageCategory.EVENT
|
|
744
|
+
|
|
745
|
+
if name.endswith("Command"):
|
|
746
|
+
return EnumMessageCategory.COMMAND
|
|
747
|
+
|
|
748
|
+
if name.endswith("Intent"):
|
|
749
|
+
return EnumMessageCategory.INTENT
|
|
750
|
+
|
|
751
|
+
# Phase 2: Check prefix patterns for CQRS-style command naming
|
|
752
|
+
# Commands often start with verbs: CreateOrder, UpdateUser, DeleteItem
|
|
753
|
+
#
|
|
754
|
+
# NOTE: Prefix matching is LESS reliable than suffix matching because
|
|
755
|
+
# many non-message types start with verbs (e.g., CreateUserService,
|
|
756
|
+
# UpdateHandler, DeleteButton). This phase runs after suffix matching
|
|
757
|
+
# to ensure names like "CreateOrderCommand" match as COMMAND via suffix.
|
|
758
|
+
command_prefixes = ("Create", "Update", "Delete", "Execute", "Do")
|
|
759
|
+
for prefix in command_prefixes:
|
|
760
|
+
if name.startswith(prefix):
|
|
761
|
+
return EnumMessageCategory.COMMAND
|
|
762
|
+
|
|
763
|
+
# Phase 3: Check for Model* prefix patterns (ONEX naming convention)
|
|
764
|
+
# ONEX models use "Model" prefix: ModelEvent, ModelCommand, etc.
|
|
765
|
+
#
|
|
766
|
+
# This phase is LAST because it's ONEX-specific and the generic suffix
|
|
767
|
+
# patterns in Phase 1 would already catch most cases (e.g., ModelOrderEvent
|
|
768
|
+
# ends with "Event" and would be caught in Phase 1).
|
|
769
|
+
if name.startswith("ModelEvent"):
|
|
770
|
+
return EnumMessageCategory.EVENT
|
|
771
|
+
if name.startswith("ModelCommand"):
|
|
772
|
+
return EnumMessageCategory.COMMAND
|
|
773
|
+
if name.startswith("ModelIntent"):
|
|
774
|
+
return EnumMessageCategory.INTENT
|
|
775
|
+
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
def _extract_topic_from_fstring(
|
|
779
|
+
self,
|
|
780
|
+
node: ast.JoinedStr,
|
|
781
|
+
) -> str | None:
|
|
782
|
+
"""Safely extract a topic name from an f-string AST node.
|
|
783
|
+
|
|
784
|
+
f-strings with interpolated values (e.g., f"{domain}.events") only yield
|
|
785
|
+
partial static content when analyzed statically. This method extracts
|
|
786
|
+
the static parts and validates that the result forms a complete, valid
|
|
787
|
+
topic pattern before returning it for validation.
|
|
788
|
+
|
|
789
|
+
IMPORTANT - INCOMPLETE TOPIC NAME HANDLING:
|
|
790
|
+
This method intentionally skips validation for f-strings that produce
|
|
791
|
+
incomplete topic fragments. The result may be an incomplete topic name
|
|
792
|
+
for f-strings with interpolated variables. For example:
|
|
793
|
+
|
|
794
|
+
- f"{domain}.events" yields only ".events" (domain is unknown)
|
|
795
|
+
- f"order.{suffix}" yields only "order." (suffix is unknown)
|
|
796
|
+
- f"{get_prefix()}.{get_suffix()}" yields "" (all dynamic)
|
|
797
|
+
|
|
798
|
+
These incomplete fragments are NOT returned for validation because:
|
|
799
|
+
- False positives: ".events" could falsely match as a valid topic
|
|
800
|
+
- False negatives: "order." would be flagged as invalid when the full
|
|
801
|
+
topic might be "order.events"
|
|
802
|
+
|
|
803
|
+
This is a deliberate design decision to prefer missing violations over
|
|
804
|
+
incorrect violations. Runtime validation should catch what static
|
|
805
|
+
analysis cannot.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
node: The AST JoinedStr node representing an f-string.
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
The extracted topic name if it forms a complete valid pattern,
|
|
812
|
+
or None if the f-string cannot be reliably validated (including
|
|
813
|
+
when only incomplete fragments are available).
|
|
814
|
+
|
|
815
|
+
Examples:
|
|
816
|
+
- f"order.events" -> "order.events" (fully static, valid)
|
|
817
|
+
- f"{domain}.events" -> None (partial: ".events" is incomplete)
|
|
818
|
+
- f"order.{suffix}" -> None (partial: "order." is incomplete)
|
|
819
|
+
- f"{prefix}.{suffix}" -> None (no static parts)
|
|
820
|
+
- f"{get_topic()}" -> None (no static parts)
|
|
821
|
+
"""
|
|
822
|
+
# Extract all static string parts from the f-string
|
|
823
|
+
static_parts: list[str] = []
|
|
824
|
+
has_interpolation = False
|
|
825
|
+
|
|
826
|
+
for value in node.values:
|
|
827
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
828
|
+
static_parts.append(value.value)
|
|
829
|
+
else:
|
|
830
|
+
# This is a FormattedValue (interpolated expression)
|
|
831
|
+
has_interpolation = True
|
|
832
|
+
|
|
833
|
+
# If no static parts, we can't validate anything
|
|
834
|
+
if not static_parts:
|
|
835
|
+
return None
|
|
836
|
+
|
|
837
|
+
# Join the static parts to see what we have
|
|
838
|
+
joined = "".join(static_parts)
|
|
839
|
+
|
|
840
|
+
# If there are interpolations, check if the result is a valid partial
|
|
841
|
+
if has_interpolation:
|
|
842
|
+
# Skip validation for incomplete fragments that could cause
|
|
843
|
+
# false positives or negatives:
|
|
844
|
+
# - Starts with "." (e.g., ".events" from f"{domain}.events")
|
|
845
|
+
# - Ends with "." (e.g., "order." from f"order.{suffix}")
|
|
846
|
+
# - Contains only a suffix without domain (e.g., ".events", ".commands")
|
|
847
|
+
if joined.startswith(".") or joined.endswith("."):
|
|
848
|
+
return None
|
|
849
|
+
|
|
850
|
+
# Check if the joined result matches a complete topic pattern.
|
|
851
|
+
# Only validate if we have what looks like a complete topic name.
|
|
852
|
+
# A complete topic should match: domain.suffix (e.g., "order.events")
|
|
853
|
+
for pattern in self.validator.patterns.values():
|
|
854
|
+
if pattern.match(joined):
|
|
855
|
+
# This partial f-string happens to form a valid complete topic
|
|
856
|
+
# This is rare but possible (e.g., f"{'order'}.events" with
|
|
857
|
+
# a constant expression that evaluates to a string literal)
|
|
858
|
+
return joined
|
|
859
|
+
|
|
860
|
+
# The static parts don't form a valid complete topic pattern.
|
|
861
|
+
# Skip validation to avoid false positives/negatives.
|
|
862
|
+
return None
|
|
863
|
+
|
|
864
|
+
# No interpolations - this is a fully static f-string (unusual but valid)
|
|
865
|
+
# For example: f"order.events" (no interpolated values)
|
|
866
|
+
return joined if joined else None
|
|
867
|
+
|
|
868
|
+
def _extract_topic_from_binop(
|
|
869
|
+
self,
|
|
870
|
+
node: ast.BinOp,
|
|
871
|
+
) -> str | None:
|
|
872
|
+
"""Safely extract a topic name from a string concatenation BinOp node.
|
|
873
|
+
|
|
874
|
+
String concatenation with variables (e.g., prefix + ".events") cannot be
|
|
875
|
+
fully resolved at static analysis time. This method extracts the static
|
|
876
|
+
string parts and applies the same conservative validation as f-strings.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
node: The AST BinOp node representing string concatenation.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
The extracted topic name if it forms a complete valid pattern,
|
|
883
|
+
or None if the concatenation cannot be reliably validated.
|
|
884
|
+
|
|
885
|
+
Examples:
|
|
886
|
+
- "order" + ".events" -> "order.events" (fully static, valid)
|
|
887
|
+
- prefix + ".events" -> None (partial: ".events" is incomplete)
|
|
888
|
+
- "order." + suffix -> None (partial: "order." is incomplete)
|
|
889
|
+
- prefix + suffix -> None (no static parts that form valid pattern)
|
|
890
|
+
"""
|
|
891
|
+
# Recursively extract static string parts from the binary operation
|
|
892
|
+
static_parts: list[str] = []
|
|
893
|
+
has_variable = False
|
|
894
|
+
|
|
895
|
+
def collect_static_parts(n: ast.expr) -> None:
|
|
896
|
+
nonlocal has_variable
|
|
897
|
+
if isinstance(n, ast.Constant) and isinstance(n.value, str):
|
|
898
|
+
static_parts.append(n.value)
|
|
899
|
+
elif isinstance(n, ast.BinOp) and isinstance(n.op, ast.Add):
|
|
900
|
+
# Recursively handle nested concatenations
|
|
901
|
+
collect_static_parts(n.left)
|
|
902
|
+
collect_static_parts(n.right)
|
|
903
|
+
else:
|
|
904
|
+
# This is a variable or other non-constant expression
|
|
905
|
+
has_variable = True
|
|
906
|
+
|
|
907
|
+
collect_static_parts(node)
|
|
908
|
+
|
|
909
|
+
# If no static parts, we can't validate anything
|
|
910
|
+
if not static_parts:
|
|
911
|
+
return None
|
|
912
|
+
|
|
913
|
+
# Join the static parts to see what we have
|
|
914
|
+
joined = "".join(static_parts)
|
|
915
|
+
|
|
916
|
+
# If there are variables, apply the same conservative approach as f-strings
|
|
917
|
+
if has_variable:
|
|
918
|
+
# Skip validation for incomplete fragments
|
|
919
|
+
if joined.startswith(".") or joined.endswith("."):
|
|
920
|
+
return None
|
|
921
|
+
|
|
922
|
+
# Check if the joined result matches a complete topic pattern
|
|
923
|
+
for pattern in self.validator.patterns.values():
|
|
924
|
+
if pattern.match(joined):
|
|
925
|
+
return joined
|
|
926
|
+
|
|
927
|
+
# Skip validation to avoid false positives/negatives
|
|
928
|
+
return None
|
|
929
|
+
|
|
930
|
+
# No variables - this is fully static concatenation
|
|
931
|
+
# (e.g., "order" + ".events")
|
|
932
|
+
return joined if joined else None
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def validate_topic_categories_in_file(
|
|
936
|
+
file_path: Path,
|
|
937
|
+
) -> list[ModelExecutionShapeViolationResult]:
|
|
938
|
+
"""Analyze a Python file for topic/category mismatches using AST.
|
|
939
|
+
|
|
940
|
+
Statically analyzes the file to detect:
|
|
941
|
+
- Topics that don't follow ONEX naming conventions
|
|
942
|
+
- Handlers subscribing to inappropriate topic categories
|
|
943
|
+
- Messages being sent to wrong topic types
|
|
944
|
+
|
|
945
|
+
This function is designed for CI integration to catch topic
|
|
946
|
+
mismatches before runtime.
|
|
947
|
+
|
|
948
|
+
Uses a cached singleton TopicCategoryValidator for performance in hot paths.
|
|
949
|
+
The TopicCategoryASTVisitor is still created per-file since it stores
|
|
950
|
+
file-specific state (violations, current handler context).
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
file_path: Path to the Python file to analyze.
|
|
954
|
+
|
|
955
|
+
Returns:
|
|
956
|
+
List of violations found in the file.
|
|
957
|
+
|
|
958
|
+
Example:
|
|
959
|
+
>>> from pathlib import Path
|
|
960
|
+
>>> violations = validate_topic_categories_in_file(
|
|
961
|
+
... Path("src/handlers/order_handler.py")
|
|
962
|
+
... )
|
|
963
|
+
>>> for v in violations:
|
|
964
|
+
... print(v.format_for_ci())
|
|
965
|
+
"""
|
|
966
|
+
if not file_path.exists():
|
|
967
|
+
# File not existing is not a topic/category violation - it's a file system issue.
|
|
968
|
+
# Log a warning and return empty list since this function analyzes code content,
|
|
969
|
+
# not file existence. Callers should validate file existence if needed.
|
|
970
|
+
logger.warning(
|
|
971
|
+
"Cannot validate topic categories: file not found: %s", file_path
|
|
972
|
+
)
|
|
973
|
+
return []
|
|
974
|
+
|
|
975
|
+
if file_path.suffix != ".py":
|
|
976
|
+
return [] # Skip non-Python files
|
|
977
|
+
|
|
978
|
+
try:
|
|
979
|
+
source = file_path.read_text(encoding="utf-8")
|
|
980
|
+
tree = ast.parse(source, filename=str(file_path))
|
|
981
|
+
except SyntaxError as e:
|
|
982
|
+
# Syntax error is a file-level issue, not a handler-specific violation.
|
|
983
|
+
# Use SYNTAX_ERROR violation type for AST parse failures.
|
|
984
|
+
# node_archetype is None because we can't analyze the code structure.
|
|
985
|
+
return [
|
|
986
|
+
ModelExecutionShapeViolationResult(
|
|
987
|
+
violation_type=EnumExecutionShapeViolation.SYNTAX_ERROR,
|
|
988
|
+
node_archetype=None, # Cannot determine archetype from unparseable file
|
|
989
|
+
file_path=str(file_path.absolute()),
|
|
990
|
+
line_number=e.lineno or 1,
|
|
991
|
+
message=(
|
|
992
|
+
f"Validation error: Cannot parse Python source file for topic category "
|
|
993
|
+
f"validation. Syntax error at line {e.lineno or 1}: {e.msg}. "
|
|
994
|
+
f"File: {file_path.name}. Fix the syntax error to enable topic "
|
|
995
|
+
f"category validation."
|
|
996
|
+
),
|
|
997
|
+
severity=EnumValidationSeverity.ERROR,
|
|
998
|
+
)
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
# Use cached singleton validator for performance
|
|
1002
|
+
# TopicCategoryASTVisitor needs fresh instance per-file (stores file state)
|
|
1003
|
+
visitor = TopicCategoryASTVisitor(file_path, _default_validator)
|
|
1004
|
+
visitor.visit(tree)
|
|
1005
|
+
|
|
1006
|
+
return visitor.violations
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def validate_message_on_topic(
|
|
1010
|
+
message: object,
|
|
1011
|
+
topic: str,
|
|
1012
|
+
message_category: MessageOutputCategory,
|
|
1013
|
+
) -> ModelExecutionShapeViolationResult | None:
|
|
1014
|
+
"""Runtime validation that message category matches topic.
|
|
1015
|
+
|
|
1016
|
+
Validates at runtime that a message's category is appropriate
|
|
1017
|
+
for the topic it's being published to or consumed from.
|
|
1018
|
+
|
|
1019
|
+
This function should be called at message processing boundaries
|
|
1020
|
+
to ensure architectural consistency.
|
|
1021
|
+
|
|
1022
|
+
Uses a cached singleton TopicCategoryValidator for performance in hot paths.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
message: The message object (used for context in error messages).
|
|
1026
|
+
topic: The Kafka topic name.
|
|
1027
|
+
message_category: The declared category of the message or node output type.
|
|
1028
|
+
Projections (EnumNodeOutputType.PROJECTION) have no topic constraint.
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
A ModelExecutionShapeViolationResult if there's a mismatch,
|
|
1032
|
+
or None if valid.
|
|
1033
|
+
|
|
1034
|
+
Example:
|
|
1035
|
+
>>> from omnibase_infra.validation import validate_message_on_topic
|
|
1036
|
+
>>> from omnibase_infra.enums import EnumMessageCategory
|
|
1037
|
+
>>>
|
|
1038
|
+
>>> result = validate_message_on_topic(
|
|
1039
|
+
... message=OrderCreatedEvent(...),
|
|
1040
|
+
... topic="order.events",
|
|
1041
|
+
... message_category=EnumMessageCategory.EVENT,
|
|
1042
|
+
... )
|
|
1043
|
+
>>> assert result is None # Valid
|
|
1044
|
+
>>>
|
|
1045
|
+
>>> result = validate_message_on_topic(
|
|
1046
|
+
... message=OrderCreatedEvent(...),
|
|
1047
|
+
... topic="order.commands", # Wrong!
|
|
1048
|
+
... message_category=EnumMessageCategory.EVENT,
|
|
1049
|
+
... )
|
|
1050
|
+
>>> assert result is not None # Violation
|
|
1051
|
+
"""
|
|
1052
|
+
# Use cached singleton validator for performance in hot paths
|
|
1053
|
+
result = _default_validator.validate_message_topic(message_category, topic)
|
|
1054
|
+
|
|
1055
|
+
if result is not None:
|
|
1056
|
+
# Enhance the message with message type info if available
|
|
1057
|
+
message_type_name = type(message).__name__
|
|
1058
|
+
category_name = (
|
|
1059
|
+
message_category.name
|
|
1060
|
+
if hasattr(message_category, "name")
|
|
1061
|
+
else str(message_category)
|
|
1062
|
+
)
|
|
1063
|
+
enhanced_message = (
|
|
1064
|
+
f"Runtime topic validation failure: Message type '{message_type_name}' with "
|
|
1065
|
+
f"category '{category_name}' (EnumMessageCategory.{category_name}) is on topic "
|
|
1066
|
+
f"'{topic}'. {result.message}"
|
|
1067
|
+
)
|
|
1068
|
+
return ModelExecutionShapeViolationResult(
|
|
1069
|
+
violation_type=result.violation_type,
|
|
1070
|
+
node_archetype=result.node_archetype,
|
|
1071
|
+
file_path=result.file_path,
|
|
1072
|
+
line_number=result.line_number,
|
|
1073
|
+
message=enhanced_message,
|
|
1074
|
+
severity=result.severity,
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
return None
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def validate_topic_categories_in_directory(
|
|
1081
|
+
directory: Path,
|
|
1082
|
+
recursive: bool = True,
|
|
1083
|
+
) -> list[ModelExecutionShapeViolationResult]:
|
|
1084
|
+
"""Validate all Python files in a directory for topic/category mismatches.
|
|
1085
|
+
|
|
1086
|
+
Convenience function for CI integration that scans a directory
|
|
1087
|
+
and validates all Python files.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
directory: Path to the directory to scan.
|
|
1091
|
+
recursive: Whether to scan subdirectories. Defaults to True.
|
|
1092
|
+
|
|
1093
|
+
Returns:
|
|
1094
|
+
List of all violations found across all files.
|
|
1095
|
+
|
|
1096
|
+
Example:
|
|
1097
|
+
>>> from pathlib import Path
|
|
1098
|
+
>>> violations = validate_topic_categories_in_directory(
|
|
1099
|
+
... Path("src/handlers/")
|
|
1100
|
+
... )
|
|
1101
|
+
>>> # CI gate: fail if any blocking violations
|
|
1102
|
+
>>> blocking = [v for v in violations if v.is_blocking()]
|
|
1103
|
+
>>> if blocking:
|
|
1104
|
+
... print(f"Found {len(blocking)} blocking violations")
|
|
1105
|
+
... exit(1)
|
|
1106
|
+
"""
|
|
1107
|
+
violations: list[ModelExecutionShapeViolationResult] = []
|
|
1108
|
+
|
|
1109
|
+
if not directory.exists():
|
|
1110
|
+
return violations
|
|
1111
|
+
|
|
1112
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
1113
|
+
for py_file in directory.glob(pattern):
|
|
1114
|
+
if py_file.is_file():
|
|
1115
|
+
violations.extend(validate_topic_categories_in_file(py_file))
|
|
1116
|
+
|
|
1117
|
+
return violations
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
# ==============================================================================
|
|
1121
|
+
# Module-Level Singleton Validator
|
|
1122
|
+
# ==============================================================================
|
|
1123
|
+
#
|
|
1124
|
+
# Performance Optimization: The TopicCategoryValidator is stateless after
|
|
1125
|
+
# initialization (only stores patterns, suffixes, and archetype_categories which
|
|
1126
|
+
# are all module-level constants). Creating new instances on every validation
|
|
1127
|
+
# call is wasteful in hot paths. Instead, we use a module-level singleton.
|
|
1128
|
+
#
|
|
1129
|
+
# Why a singleton is safe here:
|
|
1130
|
+
# - The validator only stores references to module-level immutable constants
|
|
1131
|
+
# - No per-validation state is stored in the validator instance
|
|
1132
|
+
# - All mutable state is in TopicCategoryASTVisitor (created per-file)
|
|
1133
|
+
#
|
|
1134
|
+
# Note: TopicCategoryASTVisitor still requires per-file instantiation because
|
|
1135
|
+
# it stores file-specific state (violations list, current_node_archetype, etc.).
|
|
1136
|
+
|
|
1137
|
+
_default_validator = TopicCategoryValidator()
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
__all__ = [
|
|
1141
|
+
"NODE_ARCHETYPE_EXPECTED_CATEGORIES",
|
|
1142
|
+
# Constants
|
|
1143
|
+
"TOPIC_CATEGORY_PATTERNS",
|
|
1144
|
+
"TOPIC_SUFFIXES",
|
|
1145
|
+
"TopicCategoryASTVisitor",
|
|
1146
|
+
# Classes
|
|
1147
|
+
"TopicCategoryValidator",
|
|
1148
|
+
"validate_message_on_topic",
|
|
1149
|
+
"validate_topic_categories_in_directory",
|
|
1150
|
+
# Functions
|
|
1151
|
+
"validate_topic_categories_in_file",
|
|
1152
|
+
]
|