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.
Files changed (675) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/cli/__init__.py +1 -0
  3. omnibase_infra/cli/commands.py +216 -0
  4. omnibase_infra/clients/__init__.py +0 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
  7. omnibase_infra/decorators/__init__.py +29 -0
  8. omnibase_infra/decorators/allow_any.py +109 -0
  9. omnibase_infra/dlq/__init__.py +90 -0
  10. omnibase_infra/dlq/constants_dlq.py +57 -0
  11. omnibase_infra/dlq/models/__init__.py +26 -0
  12. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  13. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  14. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  15. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  16. omnibase_infra/enums/__init__.py +123 -0
  17. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  18. omnibase_infra/enums/enum_backend_type.py +27 -0
  19. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  20. omnibase_infra/enums/enum_capture_state.py +88 -0
  21. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  22. omnibase_infra/enums/enum_circuit_state.py +51 -0
  23. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  24. omnibase_infra/enums/enum_contract_type.py +84 -0
  25. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  26. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  27. omnibase_infra/enums/enum_environment.py +46 -0
  28. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  29. omnibase_infra/enums/enum_handler_error_type.py +101 -0
  30. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  31. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  32. omnibase_infra/enums/enum_handler_type.py +77 -0
  33. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  34. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  35. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  36. omnibase_infra/enums/enum_message_category.py +213 -0
  37. omnibase_infra/enums/enum_node_archetype.py +74 -0
  38. omnibase_infra/enums/enum_node_output_type.py +185 -0
  39. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  40. omnibase_infra/enums/enum_policy_type.py +32 -0
  41. omnibase_infra/enums/enum_registration_state.py +261 -0
  42. omnibase_infra/enums/enum_registration_status.py +33 -0
  43. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  44. omnibase_infra/enums/enum_response_status.py +26 -0
  45. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  46. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  47. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  48. omnibase_infra/enums/enum_topic_standard.py +42 -0
  49. omnibase_infra/enums/enum_validation_severity.py +78 -0
  50. omnibase_infra/errors/__init__.py +156 -0
  51. omnibase_infra/errors/error_architecture_violation.py +152 -0
  52. omnibase_infra/errors/error_chain_propagation.py +188 -0
  53. omnibase_infra/errors/error_compute_registry.py +92 -0
  54. omnibase_infra/errors/error_consul.py +132 -0
  55. omnibase_infra/errors/error_container_wiring.py +243 -0
  56. omnibase_infra/errors/error_event_bus_registry.py +102 -0
  57. omnibase_infra/errors/error_infra.py +608 -0
  58. omnibase_infra/errors/error_message_type_registry.py +101 -0
  59. omnibase_infra/errors/error_policy_registry.py +112 -0
  60. omnibase_infra/errors/error_vault.py +123 -0
  61. omnibase_infra/event_bus/__init__.py +72 -0
  62. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
  63. omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
  64. omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
  65. omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
  66. omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
  67. omnibase_infra/event_bus/models/__init__.py +29 -0
  68. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  69. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
  70. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  71. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  72. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  73. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  74. omnibase_infra/event_bus/topic_constants.py +376 -0
  75. omnibase_infra/handlers/__init__.py +75 -0
  76. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  77. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  78. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  79. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  80. omnibase_infra/handlers/handler_consul.py +787 -0
  81. omnibase_infra/handlers/handler_db.py +1039 -0
  82. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  83. omnibase_infra/handlers/handler_graph.py +1154 -0
  84. omnibase_infra/handlers/handler_http.py +920 -0
  85. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  86. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  87. omnibase_infra/handlers/handler_mcp.py +748 -0
  88. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  89. omnibase_infra/handlers/handler_vault.py +422 -0
  90. omnibase_infra/handlers/mcp/__init__.py +19 -0
  91. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  92. omnibase_infra/handlers/mcp/protocols.py +178 -0
  93. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  94. omnibase_infra/handlers/mixins/__init__.py +42 -0
  95. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  96. omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
  97. omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
  98. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  99. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  100. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  101. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  102. omnibase_infra/handlers/models/__init__.py +286 -0
  103. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  104. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  105. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  106. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  107. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  108. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  109. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  110. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  111. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  112. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  113. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  114. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  115. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  116. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  117. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  118. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  119. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  120. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  121. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  122. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  123. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  124. omnibase_infra/handlers/models/http/__init__.py +50 -0
  125. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  126. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  127. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  128. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  129. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  130. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  131. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  132. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  133. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  134. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  135. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  136. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  137. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  138. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  139. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  140. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  141. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  142. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  143. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  144. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  145. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  146. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  147. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  148. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  149. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  150. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  151. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  152. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  153. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  154. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  155. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  156. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  157. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  158. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  159. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  160. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  161. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  162. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  163. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  164. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  165. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  166. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  167. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  168. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  169. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  170. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  171. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  172. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  173. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  174. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  175. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  176. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  177. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  178. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  179. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  180. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  181. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  182. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  183. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  184. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  185. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  186. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  187. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  188. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  189. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  190. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  191. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
  192. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  193. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  194. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  195. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  196. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  197. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
  198. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  199. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  200. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  201. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  202. omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
  203. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  204. omnibase_infra/idempotency/__init__.py +94 -0
  205. omnibase_infra/idempotency/models/__init__.py +43 -0
  206. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  207. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  208. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  209. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  210. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  211. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  212. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  213. omnibase_infra/idempotency/store_inmemory.py +265 -0
  214. omnibase_infra/idempotency/store_postgres.py +923 -0
  215. omnibase_infra/infrastructure/__init__.py +0 -0
  216. omnibase_infra/mixins/__init__.py +71 -0
  217. omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
  218. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  219. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  220. omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
  221. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  222. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  223. omnibase_infra/models/__init__.py +136 -0
  224. omnibase_infra/models/corpus/__init__.py +17 -0
  225. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  226. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  227. omnibase_infra/models/discovery/__init__.py +42 -0
  228. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  229. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  230. omnibase_infra/models/discovery/model_introspection_config.py +311 -0
  231. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  232. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  233. omnibase_infra/models/dispatch/__init__.py +147 -0
  234. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  235. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  236. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  237. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  238. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  239. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  240. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  241. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  242. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  243. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  244. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  245. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  246. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  247. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  248. omnibase_infra/models/errors/__init__.py +45 -0
  249. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  250. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  251. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  252. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  253. omnibase_infra/models/handlers/__init__.py +37 -0
  254. omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
  255. omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
  256. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  257. omnibase_infra/models/health/__init__.py +9 -0
  258. omnibase_infra/models/health/model_health_check_result.py +40 -0
  259. omnibase_infra/models/lifecycle/__init__.py +39 -0
  260. omnibase_infra/models/logging/__init__.py +51 -0
  261. omnibase_infra/models/logging/model_log_context.py +756 -0
  262. omnibase_infra/models/model_retry_error_classification.py +78 -0
  263. omnibase_infra/models/projection/__init__.py +43 -0
  264. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  265. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  266. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  267. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  268. omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
  269. omnibase_infra/models/projectors/__init__.py +41 -0
  270. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  271. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  272. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  273. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  274. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  275. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  276. omnibase_infra/models/registration/__init__.py +59 -0
  277. omnibase_infra/models/registration/commands/__init__.py +15 -0
  278. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  279. omnibase_infra/models/registration/events/__init__.py +56 -0
  280. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  281. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  282. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  283. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  284. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  285. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  286. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  287. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  288. omnibase_infra/models/registration/model_node_capabilities.py +179 -0
  289. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  290. omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
  291. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  292. omnibase_infra/models/registration/model_node_registration.py +162 -0
  293. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  294. omnibase_infra/models/registry/__init__.py +29 -0
  295. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  296. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  297. omnibase_infra/models/resilience/__init__.py +9 -0
  298. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  299. omnibase_infra/models/routing/__init__.py +25 -0
  300. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  301. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  302. omnibase_infra/models/runtime/__init__.py +40 -0
  303. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  304. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  305. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  306. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  307. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  308. omnibase_infra/models/runtime/model_handler_contract.py +280 -0
  309. omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
  310. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  311. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  312. omnibase_infra/models/security/__init__.py +50 -0
  313. omnibase_infra/models/security/classification_levels.py +99 -0
  314. omnibase_infra/models/security/model_environment_policy.py +145 -0
  315. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  316. omnibase_infra/models/security/model_security_error.py +81 -0
  317. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  318. omnibase_infra/models/security/model_security_warning.py +67 -0
  319. omnibase_infra/models/snapshot/__init__.py +27 -0
  320. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  321. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  322. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  323. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  324. omnibase_infra/models/types/__init__.py +71 -0
  325. omnibase_infra/models/validation/__init__.py +89 -0
  326. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  327. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  328. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  329. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  330. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  331. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  332. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  333. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  334. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  335. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  336. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  337. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  338. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  339. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  340. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  341. omnibase_infra/nodes/__init__.py +48 -0
  342. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  343. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  344. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
  345. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  346. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  347. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  348. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  349. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  350. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  351. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  352. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  353. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  354. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  355. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  356. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  357. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  358. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  359. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
  360. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  361. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  362. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  363. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  364. omnibase_infra/nodes/effects/README.md +358 -0
  365. omnibase_infra/nodes/effects/__init__.py +26 -0
  366. omnibase_infra/nodes/effects/contract.yaml +172 -0
  367. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  368. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  369. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  370. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  371. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  372. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  373. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  374. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  375. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  376. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  377. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  378. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  379. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
  380. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  381. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  382. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  383. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  384. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  385. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  386. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
  387. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  388. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  389. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  390. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  391. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  392. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  393. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  394. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  395. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  396. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  397. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  398. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  399. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  400. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  401. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  402. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  403. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  404. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  405. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  406. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  407. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  408. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
  409. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
  410. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
  411. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  412. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  413. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  414. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  415. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  416. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  417. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  418. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  419. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
  420. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  421. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  422. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  423. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  424. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  425. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  426. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  427. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  428. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  429. omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
  430. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  431. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  432. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  433. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
  434. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  435. omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
  436. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  437. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  438. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  439. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
  440. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  441. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  442. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  443. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  444. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  445. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  446. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  447. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  448. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  449. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  450. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  451. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  452. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  453. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  454. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  455. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  456. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  457. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  458. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  459. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  460. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  461. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  462. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  463. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  464. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  465. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
  466. omnibase_infra/nodes/reducers/__init__.py +30 -0
  467. omnibase_infra/nodes/reducers/models/__init__.py +32 -0
  468. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
  469. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  470. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  471. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  472. omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
  473. omnibase_infra/observability/__init__.py +143 -0
  474. omnibase_infra/observability/constants_metrics.py +91 -0
  475. omnibase_infra/observability/factory_observability_sink.py +525 -0
  476. omnibase_infra/observability/handlers/__init__.py +118 -0
  477. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  478. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  479. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  480. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  481. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  482. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  483. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  484. omnibase_infra/observability/hooks/__init__.py +74 -0
  485. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  486. omnibase_infra/observability/models/__init__.py +30 -0
  487. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  488. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  489. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  490. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  491. omnibase_infra/observability/sinks/__init__.py +69 -0
  492. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  493. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  494. omnibase_infra/plugins/__init__.py +27 -0
  495. omnibase_infra/plugins/examples/__init__.py +28 -0
  496. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  497. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  498. omnibase_infra/plugins/models/__init__.py +21 -0
  499. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  500. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  501. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  502. omnibase_infra/plugins/plugin_compute_base.py +435 -0
  503. omnibase_infra/projectors/__init__.py +30 -0
  504. omnibase_infra/projectors/contracts/__init__.py +63 -0
  505. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  506. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  507. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  508. omnibase_infra/protocols/__init__.py +99 -0
  509. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  510. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  511. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  512. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  513. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  514. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  515. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  516. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  517. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  518. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  519. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  520. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  521. omnibase_infra/runtime/__init__.py +296 -0
  522. omnibase_infra/runtime/binding_config_resolver.py +2706 -0
  523. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  524. omnibase_infra/runtime/contract_handler_discovery.py +582 -0
  525. omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
  526. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  527. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  528. omnibase_infra/runtime/enums/__init__.py +18 -0
  529. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  530. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  531. omnibase_infra/runtime/envelope_validator.py +179 -0
  532. omnibase_infra/runtime/handler_contract_source.py +669 -0
  533. omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
  534. omnibase_infra/runtime/handler_registry.py +321 -0
  535. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  536. omnibase_infra/runtime/kernel.py +40 -0
  537. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  538. omnibase_infra/runtime/mixin_semver_cache.py +378 -0
  539. omnibase_infra/runtime/mixins/__init__.py +17 -0
  540. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
  541. omnibase_infra/runtime/models/__init__.py +192 -0
  542. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  543. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  544. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  545. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  546. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  547. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  548. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  549. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  550. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  551. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  552. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  553. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  554. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  555. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  556. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  557. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  558. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  559. omnibase_infra/runtime/models/model_health_check_result.py +228 -0
  560. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  561. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  562. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  563. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  564. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  565. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  566. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  567. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  568. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  569. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  570. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  571. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  572. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  573. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  574. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
  575. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  576. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  577. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  578. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  579. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  580. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  581. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  582. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  583. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  584. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  585. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  586. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  587. omnibase_infra/runtime/projector_shell.py +1102 -0
  588. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  589. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  590. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  591. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  592. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  593. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  594. omnibase_infra/runtime/protocol_policy.py +366 -0
  595. omnibase_infra/runtime/protocols/__init__.py +27 -0
  596. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  597. omnibase_infra/runtime/registry/__init__.py +93 -0
  598. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  599. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  600. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  601. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  602. omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
  603. omnibase_infra/runtime/registry_compute.py +1143 -0
  604. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  605. omnibase_infra/runtime/registry_policy.py +1502 -0
  606. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  607. omnibase_infra/runtime/secret_resolver.py +2110 -0
  608. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  609. omnibase_infra/runtime/service_kernel.py +1573 -0
  610. omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
  611. omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
  612. omnibase_infra/runtime/util_container_wiring.py +1123 -0
  613. omnibase_infra/runtime/util_validation.py +314 -0
  614. omnibase_infra/runtime/util_version.py +98 -0
  615. omnibase_infra/runtime/util_wiring.py +566 -0
  616. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  617. omnibase_infra/services/__init__.py +68 -0
  618. omnibase_infra/services/corpus_capture.py +678 -0
  619. omnibase_infra/services/service_capability_query.py +945 -0
  620. omnibase_infra/services/service_health.py +897 -0
  621. omnibase_infra/services/service_node_selector.py +530 -0
  622. omnibase_infra/services/service_timeout_emitter.py +682 -0
  623. omnibase_infra/services/service_timeout_scanner.py +390 -0
  624. omnibase_infra/services/snapshot/__init__.py +31 -0
  625. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  626. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  627. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  628. omnibase_infra/shared/__init__.py +8 -0
  629. omnibase_infra/testing/__init__.py +10 -0
  630. omnibase_infra/testing/utils.py +23 -0
  631. omnibase_infra/types/__init__.py +48 -0
  632. omnibase_infra/types/type_cache_info.py +49 -0
  633. omnibase_infra/types/type_dsn.py +173 -0
  634. omnibase_infra/types/type_infra_aliases.py +60 -0
  635. omnibase_infra/types/typed_dict/__init__.py +21 -0
  636. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  637. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  638. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  639. omnibase_infra/utils/__init__.py +89 -0
  640. omnibase_infra/utils/correlation.py +208 -0
  641. omnibase_infra/utils/util_datetime.py +372 -0
  642. omnibase_infra/utils/util_dsn_validation.py +333 -0
  643. omnibase_infra/utils/util_env_parsing.py +264 -0
  644. omnibase_infra/utils/util_error_sanitization.py +457 -0
  645. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  646. omnibase_infra/utils/util_semver.py +233 -0
  647. omnibase_infra/validation/__init__.py +307 -0
  648. omnibase_infra/validation/enums/__init__.py +11 -0
  649. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  650. omnibase_infra/validation/infra_validators.py +1486 -0
  651. omnibase_infra/validation/linter_contract.py +907 -0
  652. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  653. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  654. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  655. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  656. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  657. omnibase_infra/validation/models/__init__.py +15 -0
  658. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  659. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  660. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  661. omnibase_infra/validation/validation_exemptions.yaml +1710 -0
  662. omnibase_infra/validation/validator_any_type.py +715 -0
  663. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  664. omnibase_infra/validation/validator_execution_shape.py +465 -0
  665. omnibase_infra/validation/validator_localhandler.py +261 -0
  666. omnibase_infra/validation/validator_registration_security.py +410 -0
  667. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  668. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  669. omnibase_infra/validation/validator_security.py +410 -0
  670. omnibase_infra/validation/validator_topic_category.py +1152 -0
  671. omnibase_infra-0.2.1.dist-info/METADATA +197 -0
  672. omnibase_infra-0.2.1.dist-info/RECORD +675 -0
  673. omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
  674. omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
  675. omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1658 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Kafka Event Bus implementation for production message streaming.
4
+
5
+ Implements ProtocolEventBus interface using Apache Kafka (via aiokafka) for
6
+ production-grade message delivery with resilience patterns including circuit
7
+ breaker, retry with exponential backoff, and dead letter queue support.
8
+
9
+ Features:
10
+ - Topic-based message routing with Kafka partitioning
11
+ - Async publish/subscribe with callback handlers
12
+ - Circuit breaker for connection failure protection
13
+ - Retry with exponential backoff on publish failures
14
+ - Dead letter queue (DLQ) for failed message processing
15
+ - Graceful degradation when Kafka is unavailable
16
+ - Support for environment/group-based routing
17
+ - Proper producer/consumer lifecycle management
18
+
19
+ Environment Variables:
20
+ Configuration can be overridden using environment variables. All variables
21
+ are optional and fall back to defaults if not set.
22
+
23
+ Connection Settings:
24
+ KAFKA_BOOTSTRAP_SERVERS: Kafka broker addresses (comma-separated)
25
+ Default: "localhost:9092"
26
+ Example: "kafka1:9092,kafka2:9092,kafka3:9092"
27
+
28
+ KAFKA_ENVIRONMENT: Environment identifier for message routing
29
+ Default: "local"
30
+ Example: "dev", "staging", "prod"
31
+
32
+ KAFKA_GROUP: Consumer group identifier
33
+ Default: "default"
34
+ Example: "my-service-group"
35
+
36
+ Timeout and Retry Settings:
37
+ KAFKA_TIMEOUT_SECONDS: Timeout for Kafka operations (integer seconds)
38
+ Default: 30
39
+ Range: 1-300
40
+ Example: "60"
41
+
42
+ KAFKA_MAX_RETRY_ATTEMPTS: Maximum publish retry attempts
43
+ Default: 3
44
+ Range: 0-10
45
+ Example: "5"
46
+
47
+ NOTE: This is the BUS-LEVEL retry for Kafka connection/publish failures.
48
+ This is distinct from MESSAGE-LEVEL retry tracked in ModelEventHeaders
49
+ (retry_count/max_retries), which is for application-level message
50
+ delivery tracking across services. See "Dual Retry Configuration" below.
51
+
52
+ KAFKA_RETRY_BACKOFF_BASE: Base delay for exponential backoff (float seconds)
53
+ Default: 1.0
54
+ Range: 0.1-60.0
55
+ Example: "2.0"
56
+
57
+ Circuit Breaker Settings:
58
+ KAFKA_CIRCUIT_BREAKER_THRESHOLD: Failures before circuit opens
59
+ Default: 5
60
+ Range: 1-100
61
+ Example: "10"
62
+
63
+ KAFKA_CIRCUIT_BREAKER_RESET_TIMEOUT: Seconds before circuit resets
64
+ Default: 30.0
65
+ Range: 1.0-3600.0
66
+ Example: "60.0"
67
+
68
+ Consumer Settings:
69
+ KAFKA_CONSUMER_SLEEP_INTERVAL: Sleep between poll iterations (float seconds)
70
+ Default: 0.1
71
+ Range: 0.01-10.0
72
+ Example: "0.2"
73
+
74
+ KAFKA_AUTO_OFFSET_RESET: Offset reset policy
75
+ Default: "latest"
76
+ Options: "earliest", "latest"
77
+
78
+ KAFKA_ENABLE_AUTO_COMMIT: Auto-commit consumer offsets
79
+ Default: true
80
+ Options: "true", "1", "yes", "on" (case-insensitive) = True
81
+ All other values = False
82
+ Example: "false"
83
+
84
+ Producer Settings:
85
+ KAFKA_ACKS: Producer acknowledgment policy
86
+ Default: "all"
87
+ Options: "all" (all replicas), "1" (leader only), "0" (no ack)
88
+
89
+ KAFKA_ENABLE_IDEMPOTENCE: Enable idempotent producer
90
+ Default: true
91
+ Options: "true", "1", "yes", "on" (case-insensitive) = True
92
+ All other values = False
93
+ Example: "true"
94
+
95
+ Dead Letter Queue Settings:
96
+ KAFKA_DEAD_LETTER_TOPIC: Topic name for failed messages
97
+ Default: None (DLQ disabled)
98
+ Example: "dlq-events"
99
+
100
+ When configured, messages that fail processing will be published
101
+ to this topic with comprehensive failure metadata including:
102
+ - Original topic and message
103
+ - Failure reason and timestamp
104
+ - Correlation ID for tracking
105
+ - Retry count and error type
106
+
107
+ Dual Retry Configuration:
108
+ ONEX uses TWO distinct retry mechanisms that serve different purposes:
109
+
110
+ 1. **Bus-Level Retry** (EventBusKafka internal):
111
+ - Configured via: max_retry_attempts, retry_backoff_base
112
+ - Purpose: Handle transient Kafka connection/publish failures
113
+ - Scope: Single publish operation within the event bus
114
+ - Applies to: Producer.send() failures, timeouts, connection errors
115
+ - Example: If Kafka broker is temporarily unreachable, retry 3 times
116
+ with exponential backoff before failing
117
+
118
+ 2. **Message-Level Retry** (ModelEventHeaders):
119
+ - Configured via: retry_count, max_retries in message headers
120
+ - Purpose: Track application-level message delivery attempts
121
+ - Scope: End-to-end message delivery across services
122
+ - Applies to: Business logic failures, handler exceptions
123
+ - Example: If order processing fails, increment retry_count and
124
+ republish; stop after max_retries reached
125
+
126
+ These mechanisms are INDEPENDENT and work together:
127
+ - Bus-level retry handles infrastructure failures (network, broker)
128
+ - Message-level retry handles application failures (handler errors)
129
+
130
+ A single message publish may trigger multiple bus-level retries,
131
+ while still counting as a single message-level delivery attempt.
132
+
133
+ Usage:
134
+ ```python
135
+ from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
136
+ from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
137
+
138
+ # Option 1: Use defaults with environment variable overrides
139
+ bus = EventBusKafka.default()
140
+ await bus.start()
141
+
142
+ # Option 2: Explicit configuration via config model
143
+ config = ModelKafkaEventBusConfig(
144
+ bootstrap_servers="kafka:9092",
145
+ environment="dev",
146
+ )
147
+ bus = EventBusKafka(config=config)
148
+ await bus.start()
149
+
150
+ # Subscribe to a topic
151
+ async def handler(msg):
152
+ print(f"Received: {msg.value}")
153
+ unsubscribe = await bus.subscribe("events", "group1", handler)
154
+
155
+ # Publish a message
156
+ await bus.publish("events", b"key", b"value")
157
+
158
+ # Cleanup
159
+ await unsubscribe()
160
+ await bus.close()
161
+ ```
162
+
163
+ Protocol Compatibility:
164
+ This class implements ProtocolEventBus from omnibase_core using duck typing
165
+ (no explicit inheritance required per ONEX patterns).
166
+ """
167
+
168
+ from __future__ import annotations
169
+
170
+ import asyncio
171
+ import logging
172
+ import random
173
+ import re
174
+ from collections import defaultdict
175
+ from collections.abc import Awaitable, Callable
176
+ from datetime import UTC, datetime
177
+ from pathlib import Path
178
+ from uuid import UUID, uuid4
179
+
180
+ from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
181
+ from aiokafka.errors import KafkaError
182
+
183
+ from omnibase_infra.enums import EnumInfraTransportType
184
+ from omnibase_infra.errors import (
185
+ InfraConnectionError,
186
+ InfraTimeoutError,
187
+ InfraUnavailableError,
188
+ ModelInfraErrorContext,
189
+ ModelTimeoutErrorContext,
190
+ ProtocolConfigurationError,
191
+ )
192
+ from omnibase_infra.event_bus.mixin_kafka_broadcast import MixinKafkaBroadcast
193
+ from omnibase_infra.event_bus.mixin_kafka_dlq import MixinKafkaDlq
194
+ from omnibase_infra.event_bus.models import (
195
+ ModelEventHeaders,
196
+ ModelEventMessage,
197
+ )
198
+ from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
199
+ from omnibase_infra.mixins import MixinAsyncCircuitBreaker
200
+
201
+ logger = logging.getLogger(__name__)
202
+
203
+
204
+ class EventBusKafka(MixinKafkaBroadcast, MixinKafkaDlq, MixinAsyncCircuitBreaker):
205
+ """Kafka-backed event bus for production message streaming.
206
+
207
+ Implements ProtocolEventBus interface using Apache Kafka (via aiokafka)
208
+ with resilience patterns including circuit breaker, retry with exponential
209
+ backoff, dead letter queue support, and graceful degradation when Kafka
210
+ is unavailable.
211
+
212
+ Features:
213
+ - Topic-based message routing with Kafka partitioning
214
+ - Multiple subscribers per topic with callback-based delivery
215
+ - Circuit breaker for connection failure protection
216
+ - Retry with exponential backoff on publish failures
217
+ - Dead letter queue (DLQ) for failed message processing
218
+ - Environment and group-based message routing
219
+ - Proper async producer/consumer lifecycle management
220
+
221
+ Attributes:
222
+ environment: Environment identifier (e.g., "local", "dev", "prod")
223
+ group: Consumer group identifier
224
+ adapter: Returns self (for protocol compatibility)
225
+
226
+ Architecture:
227
+ This class uses mixin composition to organize functionality:
228
+ - MixinKafkaBroadcast: Environment/group broadcast messaging, envelope publishing
229
+ - MixinKafkaDlq: Dead letter queue handling and metrics
230
+ - MixinAsyncCircuitBreaker: Circuit breaker resilience pattern
231
+
232
+ The core class provides:
233
+ - Factory methods (3): from_config, from_yaml, default
234
+ - Properties (4): config, adapter, environment, group
235
+ - Lifecycle methods (4): start, initialize, shutdown, close
236
+ - Pub/Sub methods (3): publish, subscribe, start_consuming
237
+ - Health check (1): health_check
238
+
239
+ Example:
240
+ ```python
241
+ config = ModelKafkaEventBusConfig(
242
+ bootstrap_servers="kafka:9092",
243
+ environment="dev",
244
+ )
245
+ bus = EventBusKafka(config=config)
246
+ await bus.start()
247
+
248
+ # Subscribe
249
+ async def handler(msg):
250
+ print(f"Received: {msg.value}")
251
+ unsubscribe = await bus.subscribe("events", "group1", handler)
252
+
253
+ # Publish
254
+ await bus.publish("events", b"key", b"value")
255
+
256
+ # Cleanup
257
+ await unsubscribe()
258
+ await bus.close()
259
+ ```
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ config: ModelKafkaEventBusConfig | None = None,
265
+ ) -> None:
266
+ """Initialize the Kafka event bus.
267
+
268
+ Args:
269
+ config: Configuration model containing all settings. If not provided,
270
+ defaults are used with environment variable overrides.
271
+
272
+ Raises:
273
+ ProtocolConfigurationError: If circuit_breaker_threshold is not a positive integer
274
+
275
+ Example:
276
+ ```python
277
+ # Using config model (recommended)
278
+ config = ModelKafkaEventBusConfig(
279
+ bootstrap_servers="kafka:9092",
280
+ environment="prod",
281
+ )
282
+ bus = EventBusKafka(config=config)
283
+
284
+ # Using factory methods
285
+ bus = EventBusKafka.default()
286
+ bus = EventBusKafka.from_yaml(Path("kafka.yaml"))
287
+ ```
288
+ """
289
+ # Use provided config or create default with environment overrides
290
+ if config is None:
291
+ config = ModelKafkaEventBusConfig.default()
292
+
293
+ # Store config reference
294
+ self._config = config
295
+
296
+ # Apply config values
297
+ self._bootstrap_servers = config.bootstrap_servers
298
+ self._environment = config.environment
299
+ self._group = config.group
300
+ self._timeout_seconds = config.timeout_seconds
301
+ self._max_retry_attempts = config.max_retry_attempts
302
+ self._retry_backoff_base = config.retry_backoff_base
303
+
304
+ # Circuit breaker configuration
305
+ if config.circuit_breaker_threshold < 1:
306
+ context = ModelInfraErrorContext(
307
+ transport_type=EnumInfraTransportType.KAFKA,
308
+ operation="init",
309
+ target_name="kafka_event_bus",
310
+ correlation_id=uuid4(),
311
+ )
312
+ raise ProtocolConfigurationError(
313
+ f"circuit_breaker_threshold must be a positive integer, got {config.circuit_breaker_threshold}",
314
+ context=context,
315
+ parameter="circuit_breaker_threshold",
316
+ value=config.circuit_breaker_threshold,
317
+ )
318
+
319
+ # Initialize circuit breaker mixin
320
+ self._init_circuit_breaker(
321
+ threshold=config.circuit_breaker_threshold,
322
+ reset_timeout=config.circuit_breaker_reset_timeout,
323
+ service_name=f"kafka.{self._environment}",
324
+ transport_type=EnumInfraTransportType.KAFKA,
325
+ )
326
+
327
+ # Kafka producer and consumer
328
+ self._producer: AIOKafkaProducer | None = None
329
+ self._consumers: dict[str, AIOKafkaConsumer] = {}
330
+
331
+ # Subscriber registry: topic -> list of (group_id, subscription_id, callback) tuples
332
+ self._subscribers: dict[
333
+ str, list[tuple[str, str, Callable[[ModelEventMessage], Awaitable[None]]]]
334
+ ] = defaultdict(list)
335
+
336
+ # Lock for coroutine safety (protects all shared state)
337
+ self._lock = asyncio.Lock()
338
+
339
+ # State flags
340
+ self._started = False
341
+ self._shutdown = False
342
+
343
+ # Background consumer tasks
344
+ self._consumer_tasks: dict[str, asyncio.Task[None]] = {}
345
+
346
+ # Producer lock for independent producer access (avoids deadlock with main lock)
347
+ self._producer_lock = asyncio.Lock()
348
+
349
+ # Initialize DLQ mixin (metrics tracking, callback hooks)
350
+ self._init_dlq()
351
+
352
+ # =========================================================================
353
+ # Factory Methods
354
+ # =========================================================================
355
+
356
+ @classmethod
357
+ def from_config(cls, config: ModelKafkaEventBusConfig) -> EventBusKafka:
358
+ """Create EventBusKafka from a configuration model.
359
+
360
+ Args:
361
+ config: Configuration model containing all settings
362
+
363
+ Returns:
364
+ EventBusKafka instance configured with the provided settings
365
+
366
+ Example:
367
+ ```python
368
+ config = ModelKafkaEventBusConfig(
369
+ bootstrap_servers="kafka:9092",
370
+ environment="prod",
371
+ timeout_seconds=60,
372
+ )
373
+ bus = EventBusKafka.from_config(config)
374
+ ```
375
+ """
376
+ return cls(config=config)
377
+
378
+ @classmethod
379
+ def from_yaml(cls, path: Path) -> EventBusKafka:
380
+ """Create EventBusKafka from a YAML configuration file.
381
+
382
+ Loads configuration from a YAML file with environment variable
383
+ overrides applied automatically.
384
+
385
+ Args:
386
+ path: Path to YAML configuration file
387
+
388
+ Returns:
389
+ EventBusKafka instance configured from the YAML file
390
+
391
+ Raises:
392
+ FileNotFoundError: If the YAML file does not exist
393
+ ValueError: If the YAML content is invalid
394
+
395
+ Example:
396
+ ```python
397
+ bus = EventBusKafka.from_yaml(Path("/etc/kafka/config.yaml"))
398
+ ```
399
+ """
400
+ config = ModelKafkaEventBusConfig.from_yaml(path)
401
+ return cls(config=config)
402
+
403
+ @classmethod
404
+ def default(cls) -> EventBusKafka:
405
+ """Create EventBusKafka with default configuration.
406
+
407
+ Creates an instance with default settings and environment variable
408
+ overrides applied automatically. This is the recommended way to
409
+ create a EventBusKafka for most use cases.
410
+
411
+ Returns:
412
+ EventBusKafka instance with default configuration
413
+
414
+ Example:
415
+ ```python
416
+ bus = EventBusKafka.default()
417
+ await bus.start()
418
+ ```
419
+ """
420
+ return cls(config=ModelKafkaEventBusConfig.default())
421
+
422
+ # =========================================================================
423
+ # Properties
424
+ # =========================================================================
425
+
426
+ @property
427
+ def config(self) -> ModelKafkaEventBusConfig:
428
+ """Get the configuration model.
429
+
430
+ Returns:
431
+ Configuration model instance used by this event bus
432
+ """
433
+ return self._config
434
+
435
+ @property
436
+ def adapter(self) -> EventBusKafka:
437
+ """Return self for protocol compatibility.
438
+
439
+ Returns:
440
+ Self reference (Kafka bus is its own adapter)
441
+ """
442
+ return self
443
+
444
+ @property
445
+ def environment(self) -> str:
446
+ """Get the environment identifier.
447
+
448
+ Returns:
449
+ Environment string (e.g., "local", "dev", "prod")
450
+ """
451
+ return self._environment
452
+
453
+ @property
454
+ def group(self) -> str:
455
+ """Get the consumer group identifier.
456
+
457
+ Returns:
458
+ Consumer group string
459
+ """
460
+ return self._group
461
+
462
+ async def start(self) -> None:
463
+ """Start the event bus and connect to Kafka.
464
+
465
+ Initializes the Kafka producer with connection retry and circuit
466
+ breaker protection. If connection fails, the bus operates in
467
+ degraded mode where publishes will fail gracefully.
468
+
469
+ Raises:
470
+ InfraConnectionError: If connection fails after all retries and
471
+ circuit breaker is open
472
+ """
473
+ if self._started:
474
+ logger.debug("EventBusKafka already started")
475
+ return
476
+
477
+ correlation_id = uuid4()
478
+
479
+ async with self._lock:
480
+ if self._started:
481
+ return
482
+
483
+ # Check circuit breaker before attempting connection
484
+ # Note: Circuit breaker requires its own lock to be held
485
+ async with self._circuit_breaker_lock:
486
+ await self._check_circuit_breaker(
487
+ operation="start", correlation_id=correlation_id
488
+ )
489
+
490
+ try:
491
+ # Apply producer configuration from config model
492
+ self._producer = AIOKafkaProducer(
493
+ bootstrap_servers=self._bootstrap_servers,
494
+ acks=self._config.acks,
495
+ enable_idempotence=self._config.enable_idempotence,
496
+ )
497
+
498
+ await asyncio.wait_for(
499
+ self._producer.start(),
500
+ timeout=self._timeout_seconds,
501
+ )
502
+
503
+ self._started = True
504
+ self._shutdown = False
505
+
506
+ # Reset circuit breaker on success
507
+ async with self._circuit_breaker_lock:
508
+ await self._reset_circuit_breaker()
509
+
510
+ logger.info(
511
+ "EventBusKafka started",
512
+ extra={
513
+ "environment": self._environment,
514
+ "group": self._group,
515
+ "bootstrap_servers": self._sanitize_bootstrap_servers(
516
+ self._bootstrap_servers
517
+ ),
518
+ },
519
+ )
520
+
521
+ except TimeoutError as e:
522
+ # Clean up producer on failure to prevent resource leak (thread-safe)
523
+ async with self._producer_lock:
524
+ if self._producer is not None:
525
+ try:
526
+ await self._producer.stop()
527
+ except Exception as cleanup_err:
528
+ logger.warning(
529
+ "Cleanup failed for Kafka producer stop: %s",
530
+ cleanup_err,
531
+ exc_info=True,
532
+ )
533
+ self._producer = None
534
+ # Record failure (circuit breaker lock required)
535
+ async with self._circuit_breaker_lock:
536
+ await self._record_circuit_failure(
537
+ operation="start", correlation_id=correlation_id
538
+ )
539
+ # Sanitize servers for safe logging (remove credentials)
540
+ sanitized_servers = self._sanitize_bootstrap_servers(
541
+ self._bootstrap_servers
542
+ )
543
+ timeout_ctx = ModelTimeoutErrorContext(
544
+ transport_type=EnumInfraTransportType.KAFKA,
545
+ operation="start",
546
+ target_name=f"kafka.{self._environment}",
547
+ correlation_id=correlation_id,
548
+ timeout_seconds=self._timeout_seconds,
549
+ )
550
+ logger.warning(
551
+ f"Timeout connecting to Kafka after {self._timeout_seconds}s",
552
+ extra={
553
+ "environment": self._environment,
554
+ "correlation_id": str(correlation_id),
555
+ },
556
+ )
557
+ raise InfraTimeoutError(
558
+ f"Timeout connecting to Kafka after {self._timeout_seconds}s",
559
+ context=timeout_ctx,
560
+ servers=sanitized_servers,
561
+ ) from e
562
+
563
+ except Exception as e:
564
+ # Clean up producer on failure to prevent resource leak (thread-safe)
565
+ async with self._producer_lock:
566
+ if self._producer is not None:
567
+ try:
568
+ await self._producer.stop()
569
+ except Exception as cleanup_err:
570
+ logger.warning(
571
+ "Cleanup failed for Kafka producer stop: %s",
572
+ cleanup_err,
573
+ exc_info=True,
574
+ )
575
+ self._producer = None
576
+ # Record failure (circuit breaker lock required)
577
+ async with self._circuit_breaker_lock:
578
+ await self._record_circuit_failure(
579
+ operation="start", correlation_id=correlation_id
580
+ )
581
+ # Sanitize servers for safe logging (remove credentials)
582
+ sanitized_servers = self._sanitize_bootstrap_servers(
583
+ self._bootstrap_servers
584
+ )
585
+ context = ModelInfraErrorContext(
586
+ transport_type=EnumInfraTransportType.KAFKA,
587
+ operation="start",
588
+ target_name=f"kafka.{self._environment}",
589
+ correlation_id=correlation_id,
590
+ )
591
+ logger.warning(
592
+ f"Failed to connect to Kafka: {e}",
593
+ extra={
594
+ "environment": self._environment,
595
+ "error": str(e),
596
+ "correlation_id": str(correlation_id),
597
+ },
598
+ )
599
+ raise InfraConnectionError(
600
+ f"Failed to connect to Kafka: {e}",
601
+ context=context,
602
+ servers=sanitized_servers,
603
+ ) from e
604
+
605
+ async def initialize(self, config: dict[str, object]) -> None:
606
+ """Initialize the event bus with configuration.
607
+
608
+ Protocol method for compatibility with ProtocolEventBus.
609
+ Extracts configuration and delegates to start(). Config updates
610
+ are applied atomically with lock protection to prevent races.
611
+
612
+ Args:
613
+ config: Configuration dictionary with optional keys:
614
+ - environment: Override environment setting
615
+ - group: Override group setting
616
+ - bootstrap_servers: Override bootstrap servers
617
+ - timeout_seconds: Override timeout setting
618
+ """
619
+ # Apply config updates atomically under lock to prevent races
620
+ async with self._lock:
621
+ if "environment" in config:
622
+ self._environment = str(config["environment"])
623
+ if "group" in config:
624
+ self._group = str(config["group"])
625
+ if "bootstrap_servers" in config:
626
+ self._bootstrap_servers = str(config["bootstrap_servers"])
627
+ if "timeout_seconds" in config:
628
+ self._timeout_seconds = int(str(config["timeout_seconds"]))
629
+
630
+ # Start after config updates are complete
631
+ await self.start()
632
+
633
+ async def shutdown(self) -> None:
634
+ """Gracefully shutdown the event bus.
635
+
636
+ Protocol method that stops consuming and closes connections.
637
+ """
638
+ await self.close()
639
+
640
+ async def close(self) -> None:
641
+ """Close the event bus and release all resources.
642
+
643
+ Stops all background consumer tasks, closes all consumers, and
644
+ stops the producer. Safe to call multiple times. Uses proper
645
+ synchronization to prevent races during shutdown.
646
+ """
647
+ # First, signal shutdown to all background tasks
648
+ async with self._lock:
649
+ if self._shutdown:
650
+ # Already shutting down or shutdown
651
+ return
652
+ self._shutdown = True
653
+ self._started = False
654
+
655
+ # Cancel all consumer tasks (outside main lock to avoid deadlock)
656
+ tasks_to_cancel = []
657
+ async with self._lock:
658
+ tasks_to_cancel = list(self._consumer_tasks.values())
659
+
660
+ for task in tasks_to_cancel:
661
+ if not task.done():
662
+ task.cancel()
663
+ try:
664
+ await task
665
+ except asyncio.CancelledError:
666
+ pass
667
+
668
+ # Clear task registry
669
+ async with self._lock:
670
+ self._consumer_tasks.clear()
671
+
672
+ # Close all consumers
673
+ consumers_to_close = []
674
+ async with self._lock:
675
+ consumers_to_close = list(self._consumers.values())
676
+ self._consumers.clear()
677
+
678
+ for consumer in consumers_to_close:
679
+ try:
680
+ await consumer.stop()
681
+ except Exception as e:
682
+ logger.warning(f"Error stopping consumer: {e}")
683
+
684
+ # Close producer with proper locking
685
+ async with self._producer_lock:
686
+ if self._producer is not None:
687
+ try:
688
+ await self._producer.stop()
689
+ except Exception as e:
690
+ logger.warning(f"Error stopping producer: {e}")
691
+ self._producer = None
692
+
693
+ # Clear subscribers
694
+ async with self._lock:
695
+ self._subscribers.clear()
696
+
697
+ logger.info(
698
+ "EventBusKafka closed",
699
+ extra={"environment": self._environment, "group": self._group},
700
+ )
701
+
702
+ async def publish(
703
+ self,
704
+ topic: str,
705
+ key: bytes | None,
706
+ value: bytes,
707
+ headers: ModelEventHeaders | None = None,
708
+ ) -> None:
709
+ """Publish message to topic.
710
+
711
+ Publishes a message to the specified Kafka topic with retry and
712
+ circuit breaker protection.
713
+
714
+ Args:
715
+ topic: Target topic name
716
+ key: Optional message key (for partitioning)
717
+ value: Message payload as bytes
718
+ headers: Optional event headers with metadata
719
+
720
+ Raises:
721
+ InfraUnavailableError: If the bus has not been started
722
+ InfraConnectionError: If publish fails after all retries
723
+ """
724
+ if not self._started:
725
+ context = ModelInfraErrorContext(
726
+ transport_type=EnumInfraTransportType.KAFKA,
727
+ operation="publish",
728
+ target_name=f"kafka.{self._environment}",
729
+ correlation_id=(
730
+ headers.correlation_id if headers is not None else uuid4()
731
+ ),
732
+ )
733
+ raise InfraUnavailableError(
734
+ "Event bus not started. Call start() first.",
735
+ context=context,
736
+ topic=topic,
737
+ )
738
+
739
+ # Create headers if not provided
740
+ if headers is None:
741
+ headers = ModelEventHeaders(
742
+ source=f"{self._environment}.{self._group}",
743
+ event_type=topic,
744
+ timestamp=datetime.now(UTC),
745
+ )
746
+
747
+ # Validate topic name
748
+ self._validate_topic_name(topic, headers.correlation_id)
749
+
750
+ # Check circuit breaker - propagate correlation_id from headers (thread-safe)
751
+ async with self._circuit_breaker_lock:
752
+ await self._check_circuit_breaker(
753
+ operation="publish", correlation_id=headers.correlation_id
754
+ )
755
+
756
+ # Convert headers to Kafka format
757
+ kafka_headers = self._model_headers_to_kafka(headers)
758
+
759
+ # Publish with retry
760
+ await self._publish_with_retry(topic, key, value, kafka_headers, headers)
761
+
762
+ async def _publish_with_retry(
763
+ self,
764
+ topic: str,
765
+ key: bytes | None,
766
+ value: bytes,
767
+ kafka_headers: list[tuple[str, bytes]],
768
+ headers: ModelEventHeaders,
769
+ ) -> None:
770
+ """Publish message with exponential backoff retry.
771
+
772
+ Args:
773
+ topic: Target topic name
774
+ key: Optional message key
775
+ value: Message payload
776
+ kafka_headers: Kafka-formatted headers
777
+ headers: Original headers model
778
+
779
+ Raises:
780
+ InfraConnectionError: If publish fails after all retries
781
+ """
782
+ last_exception: Exception | None = None
783
+
784
+ for attempt in range(self._max_retry_attempts + 1):
785
+ try:
786
+ # Thread-safe producer access - acquire lock to check and use producer
787
+ async with self._producer_lock:
788
+ if self._producer is None:
789
+ raise InfraConnectionError(
790
+ "Kafka producer not initialized",
791
+ context=ModelInfraErrorContext(
792
+ transport_type=EnumInfraTransportType.KAFKA,
793
+ operation="publish",
794
+ target_name=f"kafka.{topic}",
795
+ correlation_id=headers.correlation_id,
796
+ ),
797
+ )
798
+
799
+ future = await self._producer.send(
800
+ topic,
801
+ value=value,
802
+ key=key,
803
+ headers=kafka_headers,
804
+ )
805
+
806
+ # Wait for completion outside lock to allow other operations
807
+ record_metadata = await asyncio.wait_for(
808
+ future,
809
+ timeout=self._timeout_seconds,
810
+ )
811
+
812
+ # Success - reset circuit breaker (thread-safe)
813
+ async with self._circuit_breaker_lock:
814
+ await self._reset_circuit_breaker()
815
+
816
+ logger.debug(
817
+ f"Published to topic {topic}",
818
+ extra={
819
+ "partition": record_metadata.partition,
820
+ "offset": record_metadata.offset,
821
+ "correlation_id": str(headers.correlation_id),
822
+ },
823
+ )
824
+ return
825
+
826
+ except TimeoutError as e:
827
+ # Clean up producer on timeout to prevent resource leak (thread-safe)
828
+ async with self._producer_lock:
829
+ if self._producer is not None:
830
+ try:
831
+ await self._producer.stop()
832
+ except Exception as cleanup_err:
833
+ logger.warning(
834
+ "Cleanup failed for Kafka producer stop during publish: %s",
835
+ cleanup_err,
836
+ exc_info=True,
837
+ )
838
+ self._producer = None
839
+ last_exception = e
840
+ async with self._circuit_breaker_lock:
841
+ await self._record_circuit_failure(
842
+ operation="publish", correlation_id=headers.correlation_id
843
+ )
844
+ logger.warning(
845
+ f"Publish timeout (attempt {attempt + 1}/{self._max_retry_attempts + 1})",
846
+ extra={
847
+ "topic": topic,
848
+ "correlation_id": str(headers.correlation_id),
849
+ },
850
+ )
851
+
852
+ except KafkaError as e:
853
+ last_exception = e
854
+ async with self._circuit_breaker_lock:
855
+ await self._record_circuit_failure(
856
+ operation="publish", correlation_id=headers.correlation_id
857
+ )
858
+ logger.warning(
859
+ f"Kafka error on publish (attempt {attempt + 1}/{self._max_retry_attempts + 1}): {e}",
860
+ extra={
861
+ "topic": topic,
862
+ "correlation_id": str(headers.correlation_id),
863
+ },
864
+ )
865
+
866
+ except Exception as e:
867
+ last_exception = e
868
+ async with self._circuit_breaker_lock:
869
+ await self._record_circuit_failure(
870
+ operation="publish", correlation_id=headers.correlation_id
871
+ )
872
+ logger.warning(
873
+ f"Publish error (attempt {attempt + 1}/{self._max_retry_attempts + 1}): {e}",
874
+ extra={
875
+ "topic": topic,
876
+ "correlation_id": str(headers.correlation_id),
877
+ },
878
+ )
879
+
880
+ # Calculate backoff with jitter
881
+ if attempt < self._max_retry_attempts:
882
+ delay = self._retry_backoff_base * (2**attempt)
883
+ jitter = random.uniform(0.5, 1.5)
884
+ delay *= jitter
885
+ await asyncio.sleep(delay)
886
+
887
+ # All retries exhausted - differentiate timeout vs connection errors
888
+ context = ModelInfraErrorContext(
889
+ transport_type=EnumInfraTransportType.KAFKA,
890
+ operation="publish",
891
+ target_name=f"kafka.{topic}",
892
+ correlation_id=headers.correlation_id,
893
+ )
894
+ if isinstance(last_exception, TimeoutError):
895
+ timeout_ctx = ModelTimeoutErrorContext(
896
+ transport_type=EnumInfraTransportType.KAFKA,
897
+ operation="publish",
898
+ target_name=f"kafka.{topic}",
899
+ correlation_id=headers.correlation_id,
900
+ timeout_seconds=self._timeout_seconds,
901
+ )
902
+ raise InfraTimeoutError(
903
+ f"Timeout publishing to topic {topic} after {self._max_retry_attempts + 1} attempts",
904
+ context=timeout_ctx,
905
+ topic=topic,
906
+ retry_count=self._max_retry_attempts + 1,
907
+ ) from last_exception
908
+ raise InfraConnectionError(
909
+ f"Failed to publish to topic {topic} after {self._max_retry_attempts + 1} attempts",
910
+ context=context,
911
+ topic=topic,
912
+ retry_count=self._max_retry_attempts + 1,
913
+ ) from last_exception
914
+
915
+ async def subscribe(
916
+ self,
917
+ topic: str,
918
+ group_id: str,
919
+ on_message: Callable[[ModelEventMessage], Awaitable[None]],
920
+ ) -> Callable[[], Awaitable[None]]:
921
+ """Subscribe to topic with callback handler.
922
+
923
+ Registers a callback to be invoked for each message received on the topic.
924
+ Returns an unsubscribe function to remove the subscription.
925
+
926
+ Note: Unlike typical Kafka consumer groups, this implementation maintains
927
+ a subscriber registry and fans out messages to all registered callbacks,
928
+ matching the EventBusInmemory interface.
929
+
930
+ Args:
931
+ topic: Topic to subscribe to
932
+ group_id: Consumer group identifier for this subscription
933
+ on_message: Async callback invoked for each message
934
+
935
+ Returns:
936
+ Async unsubscribe function to remove this subscription
937
+
938
+ Example:
939
+ ```python
940
+ async def handler(msg):
941
+ print(f"Received: {msg.value}")
942
+
943
+ unsubscribe = await bus.subscribe("events", "group1", handler)
944
+ # ... later ...
945
+ await unsubscribe()
946
+ ```
947
+ """
948
+ subscription_id = str(uuid4())
949
+ correlation_id = uuid4()
950
+
951
+ # Validate topic name
952
+ self._validate_topic_name(topic, correlation_id)
953
+
954
+ async with self._lock:
955
+ # Add to subscriber registry
956
+ self._subscribers[topic].append((group_id, subscription_id, on_message))
957
+
958
+ # Start consumer for this topic if not already running
959
+ if topic not in self._consumers and self._started:
960
+ await self._start_consumer_for_topic(topic, group_id)
961
+
962
+ logger.debug(
963
+ "Subscriber added",
964
+ extra={
965
+ "topic": topic,
966
+ "group_id": group_id,
967
+ "subscription_id": subscription_id,
968
+ },
969
+ )
970
+
971
+ async def unsubscribe() -> None:
972
+ """Remove this subscription from the topic."""
973
+ async with self._lock:
974
+ try:
975
+ # Find and remove the subscription
976
+ subs = self._subscribers.get(topic, [])
977
+ for i, (_gid, sid, _) in enumerate(subs):
978
+ if sid == subscription_id:
979
+ subs.pop(i)
980
+ break
981
+
982
+ logger.debug(
983
+ "Subscriber removed",
984
+ extra={
985
+ "topic": topic,
986
+ "group_id": group_id,
987
+ "subscription_id": subscription_id,
988
+ },
989
+ )
990
+
991
+ # Stop consumer if no more subscribers for this topic
992
+ if not self._subscribers.get(topic):
993
+ await self._stop_consumer_for_topic(topic)
994
+
995
+ except Exception as e:
996
+ logger.warning(f"Error during unsubscribe: {e}")
997
+
998
+ return unsubscribe
999
+
1000
+ async def _start_consumer_for_topic(self, topic: str, group_id: str) -> None:
1001
+ """Start a Kafka consumer for a specific topic.
1002
+
1003
+ This method creates and starts a Kafka consumer for the specified topic,
1004
+ then launches a background task to consume messages. All startup failures
1005
+ are logged and propagated to the caller.
1006
+
1007
+ Args:
1008
+ topic: Topic to consume from
1009
+ group_id: Consumer group ID
1010
+
1011
+ Raises:
1012
+ InfraTimeoutError: If consumer startup times out after timeout_seconds
1013
+ InfraConnectionError: If consumer fails to connect to Kafka brokers
1014
+ """
1015
+ if topic in self._consumers:
1016
+ return
1017
+
1018
+ correlation_id = uuid4()
1019
+ sanitized_servers = self._sanitize_bootstrap_servers(self._bootstrap_servers)
1020
+
1021
+ # Normalize empty string to default group (treats "" same as None)
1022
+ # This ensures consistent behavior when group_id is unset or empty
1023
+ effective_group_id = group_id.strip() if group_id else self._group
1024
+
1025
+ # Apply consumer configuration from config model
1026
+ consumer = AIOKafkaConsumer(
1027
+ topic,
1028
+ bootstrap_servers=self._bootstrap_servers,
1029
+ group_id=f"{self._environment}.{effective_group_id}",
1030
+ auto_offset_reset=self._config.auto_offset_reset,
1031
+ enable_auto_commit=self._config.enable_auto_commit,
1032
+ )
1033
+
1034
+ try:
1035
+ await asyncio.wait_for(
1036
+ consumer.start(),
1037
+ timeout=self._timeout_seconds,
1038
+ )
1039
+
1040
+ self._consumers[topic] = consumer
1041
+
1042
+ # Start background task to consume messages with correlation tracking
1043
+ task = asyncio.create_task(self._consume_loop(topic, correlation_id))
1044
+ self._consumer_tasks[topic] = task
1045
+
1046
+ logger.info(
1047
+ f"Started consumer for topic {topic}",
1048
+ extra={
1049
+ "topic": topic,
1050
+ "group_id": effective_group_id,
1051
+ "correlation_id": str(correlation_id),
1052
+ "servers": sanitized_servers,
1053
+ },
1054
+ )
1055
+
1056
+ except TimeoutError as e:
1057
+ # Clean up consumer on failure to prevent resource leak
1058
+ try:
1059
+ await consumer.stop()
1060
+ except Exception as cleanup_err:
1061
+ logger.warning(
1062
+ "Cleanup failed for Kafka consumer stop (topic=%s): %s",
1063
+ topic,
1064
+ cleanup_err,
1065
+ exc_info=True,
1066
+ )
1067
+
1068
+ # Propagate timeout error to surface startup failures (differentiate from connection errors)
1069
+ timeout_ctx = ModelTimeoutErrorContext(
1070
+ transport_type=EnumInfraTransportType.KAFKA,
1071
+ operation="start_consumer",
1072
+ target_name=f"kafka.{topic}",
1073
+ correlation_id=correlation_id,
1074
+ timeout_seconds=self._timeout_seconds,
1075
+ )
1076
+ logger.exception(
1077
+ f"Timeout starting consumer for topic {topic} after {self._timeout_seconds}s",
1078
+ extra={
1079
+ "topic": topic,
1080
+ "group_id": group_id,
1081
+ "correlation_id": str(correlation_id),
1082
+ "timeout_seconds": self._timeout_seconds,
1083
+ "servers": sanitized_servers,
1084
+ "error_type": "timeout",
1085
+ },
1086
+ )
1087
+ raise InfraTimeoutError(
1088
+ f"Timeout starting consumer for topic {topic} after {self._timeout_seconds}s",
1089
+ context=timeout_ctx,
1090
+ topic=topic,
1091
+ servers=sanitized_servers,
1092
+ ) from e
1093
+
1094
+ except Exception as e:
1095
+ # Clean up consumer on failure to prevent resource leak
1096
+ try:
1097
+ await consumer.stop()
1098
+ except Exception as cleanup_err:
1099
+ logger.warning(
1100
+ "Cleanup failed for Kafka consumer stop (topic=%s): %s",
1101
+ topic,
1102
+ cleanup_err,
1103
+ exc_info=True,
1104
+ )
1105
+
1106
+ # Propagate connection error to surface startup failures (differentiate from timeout)
1107
+ context = ModelInfraErrorContext(
1108
+ transport_type=EnumInfraTransportType.KAFKA,
1109
+ operation="start_consumer",
1110
+ target_name=f"kafka.{topic}",
1111
+ correlation_id=correlation_id,
1112
+ )
1113
+ logger.exception(
1114
+ f"Failed to start consumer for topic {topic}: {e}",
1115
+ extra={
1116
+ "topic": topic,
1117
+ "group_id": group_id,
1118
+ "correlation_id": str(correlation_id),
1119
+ "error": str(e),
1120
+ "error_type": type(e).__name__,
1121
+ "servers": sanitized_servers,
1122
+ },
1123
+ )
1124
+ raise InfraConnectionError(
1125
+ f"Failed to start consumer for topic {topic}: {e}",
1126
+ context=context,
1127
+ topic=topic,
1128
+ servers=sanitized_servers,
1129
+ ) from e
1130
+
1131
+ async def _stop_consumer_for_topic(self, topic: str) -> None:
1132
+ """Stop the consumer for a specific topic.
1133
+
1134
+ Args:
1135
+ topic: Topic to stop consuming from
1136
+ """
1137
+ # Cancel consumer task
1138
+ if topic in self._consumer_tasks:
1139
+ task = self._consumer_tasks.pop(topic)
1140
+ if not task.done():
1141
+ task.cancel()
1142
+ try:
1143
+ await task
1144
+ except asyncio.CancelledError:
1145
+ pass
1146
+
1147
+ # Stop consumer
1148
+ if topic in self._consumers:
1149
+ consumer = self._consumers.pop(topic)
1150
+ try:
1151
+ await consumer.stop()
1152
+ except Exception as e:
1153
+ logger.warning(f"Error stopping consumer for topic {topic}: {e}")
1154
+
1155
+ async def _consume_loop(self, topic: str, correlation_id: UUID) -> None:
1156
+ """Background loop to consume messages and dispatch to subscribers.
1157
+
1158
+ This method runs in a background task and continuously polls the Kafka consumer
1159
+ for new messages. It handles graceful cancellation, dispatches messages to all
1160
+ registered subscribers, and logs all errors without terminating the loop.
1161
+
1162
+ Args:
1163
+ topic: Topic being consumed
1164
+ correlation_id: Correlation ID for tracking this consumer task
1165
+ """
1166
+ consumer = self._consumers.get(topic)
1167
+ if consumer is None:
1168
+ logger.warning(
1169
+ f"Consumer not found for topic {topic} in consume loop",
1170
+ extra={
1171
+ "topic": topic,
1172
+ "correlation_id": str(correlation_id),
1173
+ },
1174
+ )
1175
+ return
1176
+
1177
+ logger.debug(
1178
+ f"Consumer loop started for topic {topic}",
1179
+ extra={
1180
+ "topic": topic,
1181
+ "correlation_id": str(correlation_id),
1182
+ },
1183
+ )
1184
+
1185
+ try:
1186
+ async for msg in consumer:
1187
+ if self._shutdown:
1188
+ logger.debug(
1189
+ f"Consumer loop shutdown signal received for topic {topic}",
1190
+ extra={
1191
+ "topic": topic,
1192
+ "correlation_id": str(correlation_id),
1193
+ },
1194
+ )
1195
+ break
1196
+
1197
+ # Convert Kafka message to ModelEventMessage - handle conversion errors
1198
+ try:
1199
+ event_message = self._kafka_msg_to_model(msg, topic)
1200
+ except Exception as e:
1201
+ logger.exception(
1202
+ f"Failed to convert Kafka message to event model for topic {topic}",
1203
+ extra={
1204
+ "topic": topic,
1205
+ "correlation_id": str(correlation_id),
1206
+ "error": str(e),
1207
+ "error_type": type(e).__name__,
1208
+ },
1209
+ )
1210
+ # Deserialization errors are permanent failures - route to DLQ
1211
+ # Create minimal message from raw Kafka data for DLQ context
1212
+ await self._publish_raw_to_dlq(
1213
+ original_topic=topic,
1214
+ raw_msg=msg,
1215
+ error=e,
1216
+ correlation_id=correlation_id,
1217
+ failure_type="deserialization_error",
1218
+ )
1219
+ continue # Skip this message but continue consuming
1220
+
1221
+ # Get subscribers snapshot
1222
+ async with self._lock:
1223
+ subscribers = list(self._subscribers.get(topic, []))
1224
+
1225
+ # Dispatch to all subscribers
1226
+ for group_id, subscription_id, callback in subscribers:
1227
+ try:
1228
+ await callback(event_message)
1229
+ except Exception as e:
1230
+ # Check if message-level retries are exhausted
1231
+ retry_count = event_message.headers.retry_count
1232
+ max_retries = event_message.headers.max_retries
1233
+ retries_exhausted = retry_count >= max_retries
1234
+
1235
+ logger.exception(
1236
+ "Subscriber callback failed",
1237
+ extra={
1238
+ "topic": topic,
1239
+ "group_id": group_id,
1240
+ "subscription_id": subscription_id,
1241
+ "correlation_id": str(correlation_id),
1242
+ "error": str(e),
1243
+ "error_type": type(e).__name__,
1244
+ "retry_count": retry_count,
1245
+ "max_retries": max_retries,
1246
+ "retries_exhausted": retries_exhausted,
1247
+ },
1248
+ )
1249
+
1250
+ # Route to DLQ when retries exhausted (permanent failure)
1251
+ # Per ModelEventHeaders: "When retry_count >= max_retries, message should go to DLQ"
1252
+ if retries_exhausted:
1253
+ await self._publish_to_dlq(
1254
+ original_topic=topic,
1255
+ failed_message=event_message,
1256
+ error=e,
1257
+ correlation_id=correlation_id,
1258
+ )
1259
+ else:
1260
+ # Message still has retries available - log for potential republish
1261
+ # Note: Republishing logic is the responsibility of the caller/handler
1262
+ logger.warning(
1263
+ f"Handler failed but retries available ({retry_count}/{max_retries})",
1264
+ extra={
1265
+ "topic": topic,
1266
+ "correlation_id": str(correlation_id),
1267
+ "retry_count": retry_count,
1268
+ "max_retries": max_retries,
1269
+ },
1270
+ )
1271
+ # Continue dispatching to other subscribers even if one fails
1272
+
1273
+ except asyncio.CancelledError:
1274
+ # Graceful cancellation - this is expected during shutdown
1275
+ logger.info(
1276
+ f"Consumer loop cancelled for topic {topic}",
1277
+ extra={
1278
+ "topic": topic,
1279
+ "correlation_id": str(correlation_id),
1280
+ },
1281
+ )
1282
+ raise # Re-raise to properly handle task cancellation
1283
+
1284
+ except Exception as e:
1285
+ # Unexpected error in consumer loop - log with full context
1286
+ logger.exception(
1287
+ f"Consumer loop error for topic {topic}: {e}",
1288
+ extra={
1289
+ "topic": topic,
1290
+ "correlation_id": str(correlation_id),
1291
+ "error": str(e),
1292
+ "error_type": type(e).__name__,
1293
+ },
1294
+ )
1295
+ # Don't raise - allow task to complete and cleanup to proceed
1296
+
1297
+ finally:
1298
+ logger.info(
1299
+ f"Consumer loop exiting for topic {topic}",
1300
+ extra={
1301
+ "topic": topic,
1302
+ "correlation_id": str(correlation_id),
1303
+ },
1304
+ )
1305
+
1306
+ async def start_consuming(self) -> None:
1307
+ """Start the consumer loop.
1308
+
1309
+ Protocol method for ProtocolEventBus compatibility.
1310
+ Blocks until shutdown() is called.
1311
+ """
1312
+ if not self._started:
1313
+ await self.start()
1314
+
1315
+ # Collect topics that need consumers while holding lock briefly
1316
+ topics_to_start: list[tuple[str, str]] = []
1317
+ async with self._lock:
1318
+ for topic in self._subscribers:
1319
+ if topic not in self._consumers:
1320
+ subs = self._subscribers[topic]
1321
+ if subs:
1322
+ group_id = subs[0][0]
1323
+ topics_to_start.append((topic, group_id))
1324
+
1325
+ # Start consumers outside the lock to avoid blocking
1326
+ for topic, group_id in topics_to_start:
1327
+ await self._start_consumer_for_topic(topic, group_id)
1328
+
1329
+ # Block until shutdown
1330
+ while not self._shutdown:
1331
+ await asyncio.sleep(self._config.consumer_sleep_interval)
1332
+
1333
+ async def health_check(self) -> dict[str, object]:
1334
+ """Check event bus health.
1335
+
1336
+ Protocol method for ProtocolEventBus compatibility.
1337
+
1338
+ Returns:
1339
+ Dictionary with health status information:
1340
+ - healthy: Whether the bus is operational
1341
+ - started: Whether start() has been called
1342
+ - environment: Current environment
1343
+ - group: Current consumer group
1344
+ - bootstrap_servers: Kafka bootstrap servers
1345
+ - circuit_state: Current circuit breaker state
1346
+ - subscriber_count: Total number of active subscriptions
1347
+ - topic_count: Number of topics with subscribers
1348
+ - consumer_count: Number of active consumers
1349
+ """
1350
+ async with self._lock:
1351
+ subscriber_count = sum(len(subs) for subs in self._subscribers.values())
1352
+ topic_count = len(self._subscribers)
1353
+ consumer_count = len(self._consumers)
1354
+ started = self._started
1355
+
1356
+ # Get circuit breaker state (thread-safe access)
1357
+ async with self._circuit_breaker_lock:
1358
+ circuit_state = "open" if self._circuit_breaker_open else "closed"
1359
+
1360
+ # Check if producer is healthy (thread-safe access)
1361
+ producer_healthy = False
1362
+ async with self._producer_lock:
1363
+ if self._producer is not None:
1364
+ try:
1365
+ # Check if producer client is not closed
1366
+ producer_healthy = not getattr(self._producer, "_closed", True)
1367
+ except Exception:
1368
+ producer_healthy = False
1369
+
1370
+ return {
1371
+ "healthy": started and producer_healthy,
1372
+ "started": started,
1373
+ "environment": self._environment,
1374
+ "group": self._group,
1375
+ "bootstrap_servers": self._sanitize_bootstrap_servers(
1376
+ self._bootstrap_servers
1377
+ ),
1378
+ "circuit_state": circuit_state,
1379
+ "subscriber_count": subscriber_count,
1380
+ "topic_count": topic_count,
1381
+ "consumer_count": consumer_count,
1382
+ }
1383
+
1384
+ # =========================================================================
1385
+ # Helper Methods
1386
+ # =========================================================================
1387
+
1388
+ def _sanitize_bootstrap_servers(self, servers: str) -> str:
1389
+ """Sanitize bootstrap servers string to remove potential credentials.
1390
+
1391
+ Removes any authentication tokens, passwords, or sensitive data from
1392
+ the bootstrap servers string before logging or including in errors.
1393
+
1394
+ Args:
1395
+ servers: Raw bootstrap servers string (may contain credentials)
1396
+
1397
+ Returns:
1398
+ Sanitized servers string safe for logging and error messages
1399
+
1400
+ Example:
1401
+ "user:pass@kafka:9092" -> "kafka:9092"
1402
+ "kafka:9092,kafka2:9092" -> "kafka:9092,kafka2:9092"
1403
+ """
1404
+ if not servers:
1405
+ return "unknown"
1406
+
1407
+ # Split by comma for multiple servers
1408
+ server_list = [s.strip() for s in servers.split(",")]
1409
+ sanitized = []
1410
+
1411
+ for server in server_list:
1412
+ # Remove any user:pass@ prefix (credentials)
1413
+ if "@" in server:
1414
+ # Keep only the part after @
1415
+ server = server.split("@", 1)[1]
1416
+ sanitized.append(server)
1417
+
1418
+ return ",".join(sanitized)
1419
+
1420
+ def _validate_topic_name(self, topic: str, correlation_id: UUID) -> None:
1421
+ """Validate Kafka topic name according to Kafka naming rules.
1422
+
1423
+ Kafka topic names must:
1424
+ - Not be empty
1425
+ - Be 255 characters or less
1426
+ - Contain only: a-z, A-Z, 0-9, period (.), underscore (_), hyphen (-)
1427
+ - Not be "." or ".." (reserved)
1428
+
1429
+ Args:
1430
+ topic: Topic name to validate
1431
+ correlation_id: Correlation ID for error context
1432
+
1433
+ Raises:
1434
+ ProtocolConfigurationError: If topic name is invalid
1435
+
1436
+ Reference:
1437
+ https://kafka.apache.org/documentation/#topicconfigs
1438
+ """
1439
+ context = ModelInfraErrorContext(
1440
+ transport_type=EnumInfraTransportType.KAFKA,
1441
+ operation="validate_topic",
1442
+ target_name=f"kafka.{self._environment}",
1443
+ correlation_id=correlation_id,
1444
+ )
1445
+
1446
+ if not topic:
1447
+ raise ProtocolConfigurationError(
1448
+ "Topic name cannot be empty",
1449
+ context=context,
1450
+ parameter="topic",
1451
+ value=topic,
1452
+ )
1453
+
1454
+ if len(topic) > 255:
1455
+ raise ProtocolConfigurationError(
1456
+ f"Topic name '{topic}' exceeds maximum length of 255 characters",
1457
+ context=context,
1458
+ parameter="topic",
1459
+ value=topic,
1460
+ )
1461
+
1462
+ if topic in (".", ".."):
1463
+ raise ProtocolConfigurationError(
1464
+ f"Topic name '{topic}' is reserved and cannot be used",
1465
+ context=context,
1466
+ parameter="topic",
1467
+ value=topic,
1468
+ )
1469
+
1470
+ # Validate characters (a-z, A-Z, 0-9, '.', '_', '-')
1471
+ if not re.match(r"^[a-zA-Z0-9._-]+$", topic):
1472
+ raise ProtocolConfigurationError(
1473
+ f"Topic name '{topic}' contains invalid characters. "
1474
+ "Only alphanumeric characters, periods (.), underscores (_), "
1475
+ "and hyphens (-) are allowed",
1476
+ context=context,
1477
+ parameter="topic",
1478
+ value=topic,
1479
+ )
1480
+
1481
+ def _model_headers_to_kafka(
1482
+ self, headers: ModelEventHeaders
1483
+ ) -> list[tuple[str, bytes]]:
1484
+ """Convert ModelEventHeaders to Kafka header format.
1485
+
1486
+ Args:
1487
+ headers: Model headers
1488
+
1489
+ Returns:
1490
+ List of (key, value) tuples with bytes values
1491
+ """
1492
+ kafka_headers: list[tuple[str, bytes]] = [
1493
+ ("content_type", headers.content_type.encode("utf-8")),
1494
+ ("correlation_id", str(headers.correlation_id).encode("utf-8")),
1495
+ ("message_id", str(headers.message_id).encode("utf-8")),
1496
+ ("timestamp", headers.timestamp.isoformat().encode("utf-8")),
1497
+ ("source", headers.source.encode("utf-8")),
1498
+ ("event_type", headers.event_type.encode("utf-8")),
1499
+ ("schema_version", headers.schema_version.encode("utf-8")),
1500
+ ("priority", headers.priority.encode("utf-8")),
1501
+ ("retry_count", str(headers.retry_count).encode("utf-8")),
1502
+ ("max_retries", str(headers.max_retries).encode("utf-8")),
1503
+ ]
1504
+
1505
+ # Add optional headers if present
1506
+ if headers.destination:
1507
+ kafka_headers.append(("destination", headers.destination.encode("utf-8")))
1508
+ if headers.trace_id:
1509
+ kafka_headers.append(("trace_id", headers.trace_id.encode("utf-8")))
1510
+ if headers.span_id:
1511
+ kafka_headers.append(("span_id", headers.span_id.encode("utf-8")))
1512
+ if headers.parent_span_id:
1513
+ kafka_headers.append(
1514
+ ("parent_span_id", headers.parent_span_id.encode("utf-8"))
1515
+ )
1516
+ if headers.operation_name:
1517
+ kafka_headers.append(
1518
+ ("operation_name", headers.operation_name.encode("utf-8"))
1519
+ )
1520
+ if headers.routing_key:
1521
+ kafka_headers.append(("routing_key", headers.routing_key.encode("utf-8")))
1522
+ if headers.partition_key:
1523
+ kafka_headers.append(
1524
+ ("partition_key", headers.partition_key.encode("utf-8"))
1525
+ )
1526
+ if headers.ttl_seconds is not None:
1527
+ kafka_headers.append(
1528
+ ("ttl_seconds", str(headers.ttl_seconds).encode("utf-8"))
1529
+ )
1530
+
1531
+ return kafka_headers
1532
+
1533
+ def _kafka_headers_to_model(
1534
+ self, kafka_headers: list[tuple[str, bytes]] | None
1535
+ ) -> ModelEventHeaders:
1536
+ """Convert Kafka headers to ModelEventHeaders.
1537
+
1538
+ Args:
1539
+ kafka_headers: Kafka header list
1540
+
1541
+ Returns:
1542
+ ModelEventHeaders instance
1543
+ """
1544
+ if not kafka_headers:
1545
+ return ModelEventHeaders(
1546
+ source="unknown",
1547
+ event_type="unknown",
1548
+ timestamp=datetime.now(UTC),
1549
+ )
1550
+
1551
+ headers_dict: dict[str, str] = {}
1552
+ for key, value in kafka_headers:
1553
+ if value is not None:
1554
+ headers_dict[key] = value.decode("utf-8")
1555
+
1556
+ # Parse correlation_id from string to UUID (with fallback to new UUID)
1557
+ correlation_id_str = headers_dict.get("correlation_id")
1558
+ if correlation_id_str:
1559
+ try:
1560
+ correlation_id = UUID(correlation_id_str)
1561
+ except (ValueError, AttributeError):
1562
+ # Invalid UUID format - generate new one
1563
+ correlation_id = uuid4()
1564
+ else:
1565
+ correlation_id = uuid4()
1566
+
1567
+ # Parse message_id from string to UUID (with fallback to new UUID)
1568
+ message_id_str = headers_dict.get("message_id")
1569
+ if message_id_str:
1570
+ try:
1571
+ message_id = UUID(message_id_str)
1572
+ except (ValueError, AttributeError):
1573
+ # Invalid UUID format - generate new one
1574
+ message_id = uuid4()
1575
+ else:
1576
+ message_id = uuid4()
1577
+
1578
+ # Parse timestamp from ISO format string to datetime (with fallback to now)
1579
+ timestamp_str = headers_dict.get("timestamp")
1580
+ if timestamp_str:
1581
+ timestamp = datetime.fromisoformat(timestamp_str)
1582
+ else:
1583
+ timestamp = datetime.now(UTC)
1584
+
1585
+ # Parse priority with validation (default to "normal" if invalid)
1586
+ priority_str = headers_dict.get("priority", "normal")
1587
+ valid_priorities = ("low", "normal", "high", "critical")
1588
+ priority = priority_str if priority_str in valid_priorities else "normal"
1589
+
1590
+ # Parse integer fields with fallback defaults
1591
+ retry_count_str = headers_dict.get("retry_count")
1592
+ retry_count = int(retry_count_str) if retry_count_str else 0
1593
+
1594
+ max_retries_str = headers_dict.get("max_retries")
1595
+ max_retries = int(max_retries_str) if max_retries_str else 3
1596
+
1597
+ ttl_seconds_str = headers_dict.get("ttl_seconds")
1598
+ ttl_seconds = int(ttl_seconds_str) if ttl_seconds_str else None
1599
+
1600
+ return ModelEventHeaders(
1601
+ content_type=headers_dict.get("content_type", "application/json"),
1602
+ correlation_id=correlation_id,
1603
+ message_id=message_id,
1604
+ timestamp=timestamp,
1605
+ source=headers_dict.get("source", "unknown"),
1606
+ event_type=headers_dict.get("event_type", "unknown"),
1607
+ schema_version=headers_dict.get("schema_version", "1.0.0"),
1608
+ destination=headers_dict.get("destination"),
1609
+ trace_id=headers_dict.get("trace_id"),
1610
+ span_id=headers_dict.get("span_id"),
1611
+ parent_span_id=headers_dict.get("parent_span_id"),
1612
+ operation_name=headers_dict.get("operation_name"),
1613
+ priority=priority,
1614
+ routing_key=headers_dict.get("routing_key"),
1615
+ partition_key=headers_dict.get("partition_key"),
1616
+ retry_count=retry_count,
1617
+ max_retries=max_retries,
1618
+ ttl_seconds=ttl_seconds,
1619
+ )
1620
+
1621
+ def _kafka_msg_to_model(self, msg: object, topic: str) -> ModelEventMessage:
1622
+ """Convert Kafka ConsumerRecord to ModelEventMessage.
1623
+
1624
+ Args:
1625
+ msg: Kafka ConsumerRecord
1626
+ topic: Topic name
1627
+
1628
+ Returns:
1629
+ ModelEventMessage instance
1630
+ """
1631
+ # Extract fields from Kafka message
1632
+ key = getattr(msg, "key", None)
1633
+ value = getattr(msg, "value", b"")
1634
+ offset = getattr(msg, "offset", None)
1635
+ partition = getattr(msg, "partition", None)
1636
+ kafka_headers = getattr(msg, "headers", None)
1637
+
1638
+ # Convert key to bytes if it's a string
1639
+ if isinstance(key, str):
1640
+ key = key.encode("utf-8")
1641
+
1642
+ # Ensure value is bytes
1643
+ if isinstance(value, str):
1644
+ value = value.encode("utf-8")
1645
+
1646
+ headers = self._kafka_headers_to_model(kafka_headers)
1647
+
1648
+ return ModelEventMessage(
1649
+ topic=topic,
1650
+ key=key,
1651
+ value=value,
1652
+ headers=headers,
1653
+ offset=str(offset) if offset is not None else None,
1654
+ partition=partition,
1655
+ )
1656
+
1657
+
1658
+ __all__: list[str] = ["EventBusKafka"]