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,1329 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Snapshot Publisher for Registration Projections.
4
+
5
+ Publishes compacted snapshots to Kafka for read optimization. Snapshots are
6
+ derived from projections and NEVER replace the event log. The event log
7
+ remains the absolute source of truth.
8
+
9
+ Architecture Overview:
10
+ This service implements F2 (Snapshot Publishing) of the ONEX registration
11
+ projection pipeline:
12
+
13
+ 1. Projectors (F1) persist projections to PostgreSQL via ProjectorRegistration
14
+ 2. Snapshot Publisher (F2) reads projections and publishes compacted snapshots
15
+ 3. Consumers read snapshots for fast O(1) state queries
16
+
17
+ ```
18
+ Events -> Projector -> PostgreSQL -> Snapshot Publisher -> Kafka (compacted)
19
+ |
20
+ v
21
+ Orchestrators/Readers
22
+ ```
23
+
24
+ Design Principles:
25
+ - **Read Optimization Only**: Snapshots are for fast reads, not data integrity
26
+ - **Kafka Compaction**: Only latest snapshot per entity_id retained
27
+ - **Tombstone Support**: Null values delete snapshots during compaction
28
+ - **Version Tracking**: Monotonic versions for conflict resolution
29
+ - **Circuit Breaker**: Resilience against Kafka failures
30
+ - **Lazy Consumer**: Consumer for reads is created on-demand
31
+
32
+ Concurrency Safety:
33
+ This implementation is coroutine-safe for concurrent async publishing.
34
+ Uses asyncio locks for circuit breaker state management and
35
+ version tracker synchronization. Note: This is coroutine-safe, not
36
+ thread-safe. For multi-threaded access, additional synchronization
37
+ would be required.
38
+
39
+ Error Handling:
40
+ All methods raise ONEX error types:
41
+ - InfraConnectionError: Kafka unavailable or connection failed
42
+ - InfraTimeoutError: Publish operation timed out
43
+ - InfraUnavailableError: Circuit breaker open
44
+
45
+ Example Usage:
46
+ ```python
47
+ from aiokafka import AIOKafkaProducer
48
+ from omnibase_infra.projectors import SnapshotPublisherRegistration
49
+ from omnibase_infra.models.projection import ModelSnapshotTopicConfig
50
+
51
+ # Create producer and config
52
+ producer = AIOKafkaProducer(bootstrap_servers="localhost:9092")
53
+ config = ModelSnapshotTopicConfig.default()
54
+
55
+ # Initialize publisher
56
+ publisher = SnapshotPublisherRegistration(producer, config)
57
+ await publisher.start()
58
+
59
+ try:
60
+ # Publish snapshot from projection
61
+ snapshot = await publisher.publish_from_projection(projection)
62
+ print(f"Published snapshot version {snapshot.snapshot_version}")
63
+
64
+ # Or publish pre-built snapshot
65
+ await publisher.publish_snapshot(snapshot)
66
+
67
+ # Batch publish
68
+ count = await publisher.publish_batch(snapshots)
69
+ print(f"Published {count} snapshots")
70
+
71
+ # Read snapshot (uses lazy consumer and in-memory cache)
72
+ snapshot = await publisher.get_latest_snapshot("entity-123", "registration")
73
+ if snapshot:
74
+ print(f"Entity state: {snapshot.current_state}")
75
+
76
+ # Delete snapshot (tombstone)
77
+ await publisher.delete_snapshot("entity-123", "registration")
78
+ finally:
79
+ await publisher.stop()
80
+ ```
81
+
82
+ Performance Considerations:
83
+ - Use publish_batch for bulk operations (e.g., periodic snapshot jobs)
84
+ - Consider publish_from_projection for single updates (handles versioning)
85
+ - Tombstones are cheap - use delete_snapshot for permanent removals
86
+ - Monitor circuit breaker state for Kafka health
87
+ - First read triggers cache loading (may take a few seconds for large topics)
88
+ - Subsequent reads are O(1) from in-memory cache
89
+
90
+ Related Tickets:
91
+ - OMN-947 (F2): Snapshot Publishing
92
+ - OMN-944 (F1): Implement Registration Projection Schema
93
+ - OMN-940 (F0): Define Projector Execution Model
94
+ - OMN-1059: Implement snapshot read functionality
95
+
96
+ See Also:
97
+ - ProtocolSnapshotPublisher: Protocol definition for snapshot publishers
98
+ - ModelRegistrationSnapshot: Snapshot model definition
99
+ - ModelSnapshotTopicConfig: Topic configuration for compacted topics
100
+ - ProjectorRegistration: Projection persistence (source for snapshots)
101
+ """
102
+
103
+ from __future__ import annotations
104
+
105
+ import asyncio
106
+ import logging
107
+ from datetime import UTC, datetime
108
+ from typing import TYPE_CHECKING
109
+ from uuid import UUID, uuid4
110
+
111
+ from omnibase_infra.enums import EnumInfraTransportType
112
+ from omnibase_infra.errors import (
113
+ InfraConnectionError,
114
+ InfraTimeoutError,
115
+ InfraUnavailableError,
116
+ ModelInfraErrorContext,
117
+ ModelTimeoutErrorContext,
118
+ )
119
+ from omnibase_infra.mixins import MixinAsyncCircuitBreaker
120
+ from omnibase_infra.models.projection import (
121
+ ModelRegistrationProjection,
122
+ ModelRegistrationSnapshot,
123
+ ModelSnapshotTopicConfig,
124
+ )
125
+ from omnibase_infra.models.resilience import ModelCircuitBreakerConfig
126
+
127
+ if TYPE_CHECKING:
128
+ from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
129
+
130
+ logger = logging.getLogger(__name__)
131
+
132
+
133
+ class SnapshotPublisherRegistration(MixinAsyncCircuitBreaker):
134
+ """Publishes registration snapshots to a compacted Kafka topic.
135
+
136
+ This service reads registration projections and publishes them as
137
+ optimized snapshots to a Kafka compacted topic. Kafka compaction
138
+ ensures only the latest snapshot per entity is retained, enabling
139
+ fast state reconstruction without replaying events.
140
+
141
+ The publisher implements ProtocolSnapshotPublisher for structural
142
+ typing compatibility, allowing it to be used wherever the protocol
143
+ is expected.
144
+
145
+ Compaction Semantics:
146
+ - Key: "{domain}:{entity_id}" (e.g., "registration:uuid-here")
147
+ - Value: JSON-serialized ModelRegistrationSnapshot
148
+ - Tombstone: null value deletes the key during compaction
149
+ - After compaction: only latest snapshot per key survives
150
+
151
+ Circuit Breaker:
152
+ Uses MixinAsyncCircuitBreaker for resilience:
153
+ - Opens after 5 consecutive failures
154
+ - Resets after 60 seconds
155
+ - Raises InfraUnavailableError when open
156
+
157
+ Version Tracking:
158
+ The publisher maintains a version tracker per entity to ensure
159
+ monotonically increasing snapshot versions. This enables conflict
160
+ resolution and ordering guarantees during compaction.
161
+
162
+ Version Tracker Semantics:
163
+ - Versions start at 1 for each new entity
164
+ - Versions increment monotonically per entity within publisher lifetime
165
+ - Version tracker resets when publisher is recreated (new instance)
166
+ - delete_snapshot clears the version tracker entry for that entity
167
+ - For persistent version tracking across restarts, inject a shared
168
+ snapshot_version_tracker dict in __init__
169
+ - Coroutine-safe: Uses asyncio.Lock for concurrent access
170
+
171
+ NOTE: Snapshots are for READ OPTIMIZATION only. The immutable event
172
+ log remains the authoritative source of truth. Snapshots can be
173
+ regenerated from the event log at any time.
174
+
175
+ Attributes:
176
+ _producer: Kafka producer for publishing snapshots
177
+ _config: Snapshot topic configuration
178
+ _version_tracker: Dict tracking versions per entity
179
+ _started: Whether the publisher has been started
180
+
181
+ Example:
182
+ >>> config = ModelSnapshotTopicConfig.default()
183
+ >>> publisher = SnapshotPublisherRegistration(producer, config)
184
+ >>> await publisher.start()
185
+ >>>
186
+ >>> # Publish snapshot from projection
187
+ >>> snapshot = await publisher.publish_from_projection(projection)
188
+ >>>
189
+ >>> # Or publish existing snapshot
190
+ >>> await publisher.publish_snapshot(snapshot)
191
+ >>>
192
+ >>> await publisher.stop()
193
+ """
194
+
195
+ def __init__(
196
+ self,
197
+ producer: AIOKafkaProducer,
198
+ config: ModelSnapshotTopicConfig,
199
+ *,
200
+ snapshot_version_tracker: dict[str, int] | None = None,
201
+ bootstrap_servers: str | None = None,
202
+ consumer_timeout_ms: int = 5000,
203
+ ) -> None:
204
+ """Initialize snapshot publisher.
205
+
206
+ Args:
207
+ producer: AIOKafka producer for publishing snapshots. The producer
208
+ should be configured for the target Kafka cluster but NOT
209
+ started - the publisher will manage its lifecycle.
210
+ config: Snapshot topic configuration defining the target topic
211
+ and compaction settings.
212
+ snapshot_version_tracker: Optional dict to track versions per entity.
213
+ If not provided, a new dict is created internally. Useful for
214
+ sharing version state across multiple publishers or for testing.
215
+ bootstrap_servers: Kafka bootstrap servers for the consumer (for reads).
216
+ Required if you intend to use get_latest_snapshot(). If not provided,
217
+ reads will attempt to extract from the producer configuration.
218
+ consumer_timeout_ms: Timeout in milliseconds for consumer poll operations.
219
+ Default is 5000ms (5 seconds). Used when loading the snapshot cache.
220
+
221
+ Example:
222
+ >>> producer = AIOKafkaProducer(
223
+ ... bootstrap_servers="localhost:9092",
224
+ ... value_serializer=lambda v: v, # Publisher handles serialization
225
+ ... )
226
+ >>> config = ModelSnapshotTopicConfig.default()
227
+ >>> publisher = SnapshotPublisherRegistration(
228
+ ... producer,
229
+ ... config,
230
+ ... bootstrap_servers="localhost:9092",
231
+ ... )
232
+ """
233
+ self._producer = producer
234
+ self._config = config
235
+ self._version_tracker = snapshot_version_tracker or {}
236
+ self._version_tracker_lock = asyncio.Lock()
237
+ self._started = False
238
+
239
+ # Consumer configuration for read operations
240
+ self._bootstrap_servers = bootstrap_servers
241
+ self._consumer_timeout_ms = consumer_timeout_ms
242
+ self._consumer: AIOKafkaConsumer | None = None
243
+ self._consumer_started = False
244
+
245
+ # In-memory cache for O(1) snapshot lookups
246
+ # Key: "{domain}:{entity_id}", Value: ModelRegistrationSnapshot
247
+ #
248
+ # Cache Size Expectations:
249
+ # - Typical deployment: 100-1000 registered nodes
250
+ # - Large deployment: 5000-10000 nodes
251
+ # - Maximum practical: ~50000 nodes (memory ~100MB with full snapshots)
252
+ # - Each snapshot is approximately 2KB serialized
253
+ #
254
+ # Memory Footprint Estimation:
255
+ # - 1000 nodes * 2KB = ~2MB
256
+ # - 10000 nodes * 2KB = ~20MB
257
+ # - 50000 nodes * 2KB = ~100MB
258
+ self._snapshot_cache: dict[str, ModelRegistrationSnapshot] = {}
259
+ self._cache_lock = asyncio.Lock()
260
+ self._cache_loaded = False
261
+ self._cache_warming_in_progress = False
262
+
263
+ # Initialize circuit breaker with Kafka-appropriate settings
264
+ cb_config = ModelCircuitBreakerConfig.from_env(
265
+ service_name=f"snapshot-publisher.{config.topic}",
266
+ transport_type=EnumInfraTransportType.KAFKA,
267
+ )
268
+ self._init_circuit_breaker_from_config(cb_config)
269
+
270
+ @property
271
+ def topic(self) -> str:
272
+ """Get the configured topic."""
273
+ return self._config.topic
274
+
275
+ @property
276
+ def is_started(self) -> bool:
277
+ """Check if the publisher has been started."""
278
+ return self._started
279
+
280
+ async def start(self, *, warm_cache: bool = False) -> None:
281
+ """Start the snapshot publisher.
282
+
283
+ Starts the underlying Kafka producer. Must be called before
284
+ publishing any snapshots.
285
+
286
+ Args:
287
+ warm_cache: If True, pre-load the snapshot cache from Kafka
288
+ during startup. This is useful for read-heavy workloads
289
+ where you want the first read to be fast. The warming
290
+ is performed asynchronously and does not block start().
291
+ Default is False for backward compatibility.
292
+
293
+ Raises:
294
+ InfraConnectionError: If Kafka connection fails
295
+
296
+ Example:
297
+ >>> publisher = SnapshotPublisherRegistration(producer, config)
298
+ >>> await publisher.start()
299
+ >>> # Now ready to publish
300
+ >>>
301
+ >>> # With cache warming for read-heavy workloads
302
+ >>> await publisher.start(warm_cache=True)
303
+ """
304
+ if self._started:
305
+ logger.debug("Snapshot publisher already started")
306
+ return
307
+
308
+ correlation_id = uuid4()
309
+ ctx = ModelInfraErrorContext(
310
+ transport_type=EnumInfraTransportType.KAFKA,
311
+ operation="start",
312
+ target_name=self._config.topic,
313
+ correlation_id=correlation_id,
314
+ )
315
+
316
+ try:
317
+ await self._producer.start()
318
+ self._started = True
319
+ logger.info(
320
+ "Snapshot publisher started for topic %s",
321
+ self._config.topic,
322
+ extra={"correlation_id": str(correlation_id)},
323
+ )
324
+
325
+ # Optionally warm the cache in the background
326
+ if warm_cache and self._bootstrap_servers:
327
+ await self._warm_cache_async(correlation_id)
328
+
329
+ except Exception as e:
330
+ raise InfraConnectionError(
331
+ f"Failed to start Kafka producer for topic {self._config.topic}",
332
+ context=ctx,
333
+ ) from e
334
+
335
+ async def _warm_cache_async(self, correlation_id: UUID) -> None:
336
+ """Warm the snapshot cache asynchronously.
337
+
338
+ Pre-loads all snapshots from the Kafka topic into the in-memory
339
+ cache. This is called during start() when warm_cache=True.
340
+
341
+ Cache warming is performed inline (not in background task) to ensure
342
+ the cache is populated before start() returns. This provides
343
+ predictable behavior for read-heavy workloads.
344
+
345
+ Args:
346
+ correlation_id: Correlation ID for tracing
347
+
348
+ Note:
349
+ Errors during cache warming are logged but do not fail startup.
350
+ The cache will be loaded lazily on the first read if warming fails.
351
+ """
352
+ if self._cache_warming_in_progress:
353
+ logger.debug("Cache warming already in progress, skipping")
354
+ return
355
+
356
+ self._cache_warming_in_progress = True
357
+
358
+ try:
359
+ logger.info(
360
+ "Warming snapshot cache for topic %s",
361
+ self._config.topic,
362
+ extra={"correlation_id": str(correlation_id)},
363
+ )
364
+
365
+ await self._load_cache_from_topic(correlation_id)
366
+
367
+ async with self._cache_lock:
368
+ cache_size = len(self._snapshot_cache)
369
+
370
+ logger.info(
371
+ "Cache warming completed: %d snapshots loaded for topic %s",
372
+ cache_size,
373
+ self._config.topic,
374
+ extra={
375
+ "correlation_id": str(correlation_id),
376
+ "cache_size": cache_size,
377
+ "topic": self._config.topic,
378
+ },
379
+ )
380
+ except Exception as e:
381
+ # Log but don't fail startup - cache can be loaded lazily
382
+ logger.warning(
383
+ "Cache warming failed for topic %s: %s. "
384
+ "Cache will be loaded lazily on first read.",
385
+ self._config.topic,
386
+ str(e),
387
+ extra={
388
+ "correlation_id": str(correlation_id),
389
+ "error_type": type(e).__name__,
390
+ },
391
+ )
392
+ finally:
393
+ self._cache_warming_in_progress = False
394
+
395
+ async def stop(self) -> None:
396
+ """Stop the snapshot publisher.
397
+
398
+ Stops the underlying Kafka producer, consumer (if started), and
399
+ cleans up resources. Safe to call multiple times.
400
+
401
+ Example:
402
+ >>> await publisher.stop()
403
+ >>> # Publisher is now stopped
404
+ """
405
+ # Stop consumer if it was started
406
+ if self._consumer_started and self._consumer is not None:
407
+ try:
408
+ await self._consumer.stop()
409
+ self._consumer_started = False
410
+ self._consumer = None
411
+ logger.debug(
412
+ "Snapshot consumer stopped for topic %s", self._config.topic
413
+ )
414
+ except Exception as e:
415
+ # Log but don't raise - stop should be best-effort
416
+ logger.warning(
417
+ "Error stopping Kafka consumer: %s",
418
+ str(e),
419
+ extra={"topic": self._config.topic},
420
+ )
421
+ self._consumer_started = False
422
+ self._consumer = None
423
+
424
+ if not self._started:
425
+ logger.debug("Snapshot publisher already stopped")
426
+ return
427
+
428
+ try:
429
+ await self._producer.stop()
430
+ self._started = False
431
+ logger.info("Snapshot publisher stopped for topic %s", self._config.topic)
432
+ except Exception as e:
433
+ # Log but don't raise - stop should be best-effort
434
+ logger.warning(
435
+ "Error stopping Kafka producer: %s",
436
+ str(e),
437
+ extra={"topic": self._config.topic},
438
+ )
439
+ self._started = False
440
+
441
+ # Clear the cache on stop
442
+ async with self._cache_lock:
443
+ self._snapshot_cache.clear()
444
+ self._cache_loaded = False
445
+
446
+ async def _get_next_version(self, entity_id: str, domain: str) -> int:
447
+ """Get the next snapshot version for an entity.
448
+
449
+ Increments and returns the version counter for the given entity.
450
+ Versions are monotonically increasing within the lifetime of
451
+ this publisher instance.
452
+
453
+ Concurrency Safety:
454
+ Uses _version_tracker_lock (asyncio.Lock) to ensure atomic
455
+ read-modify-write operations in concurrent coroutine contexts.
456
+
457
+ Args:
458
+ entity_id: The entity identifier
459
+ domain: The domain namespace
460
+
461
+ Returns:
462
+ Next version number (starting from 1)
463
+ """
464
+ key = f"{domain}:{entity_id}"
465
+ async with self._version_tracker_lock:
466
+ current = self._version_tracker.get(key, 0)
467
+ next_version = current + 1
468
+ self._version_tracker[key] = next_version
469
+ return next_version
470
+
471
+ async def _cleanup_consumer(self) -> None:
472
+ """Clean up Kafka consumer after cache load operations.
473
+
474
+ Stops the consumer, resets the started flag, and clears the reference.
475
+ This method is idempotent and safe to call even if no consumer exists.
476
+ Used for cleanup after both successful and failed cache load operations.
477
+ """
478
+ if self._consumer_started:
479
+ try:
480
+ if self._consumer is not None:
481
+ await self._consumer.stop()
482
+ except Exception:
483
+ pass
484
+ self._consumer_started = False
485
+ self._consumer = None
486
+
487
+ async def publish_snapshot(
488
+ self,
489
+ snapshot: ModelRegistrationProjection,
490
+ ) -> None:
491
+ """Publish a single snapshot to the snapshot topic.
492
+
493
+ Publishes the projection as a snapshot to the compacted Kafka topic.
494
+ The key is derived from (entity_id, domain) for proper compaction.
495
+
496
+ NOTE: This is a READ OPTIMIZATION. The event log remains source of truth.
497
+
498
+ This method implements ProtocolSnapshotPublisher.publish_snapshot using
499
+ ModelRegistrationProjection as the input type. For publishing pre-built
500
+ ModelRegistrationSnapshot objects, use _publish_snapshot_model.
501
+
502
+ Args:
503
+ snapshot: The projection to publish as a snapshot. Must contain
504
+ valid entity_id and domain for key construction.
505
+
506
+ Raises:
507
+ InfraConnectionError: If Kafka connection fails
508
+ InfraTimeoutError: If publish times out
509
+ InfraUnavailableError: If circuit breaker is open
510
+
511
+ Example:
512
+ >>> projection = await reader.get_entity_state(entity_id)
513
+ >>> await publisher.publish_snapshot(projection)
514
+ """
515
+ # Delegate to publish_from_projection for versioning and publishing
516
+ await self.publish_from_projection(
517
+ projection=snapshot,
518
+ node_name=None,
519
+ )
520
+
521
+ async def _publish_snapshot_model(
522
+ self,
523
+ snapshot: ModelRegistrationSnapshot,
524
+ ) -> None:
525
+ """Publish a pre-built snapshot model to Kafka.
526
+
527
+ Internal method for publishing ModelRegistrationSnapshot objects.
528
+ Use publish_snapshot for protocol compliance or publish_from_projection
529
+ for automatic version tracking.
530
+
531
+ Args:
532
+ snapshot: The snapshot model to publish
533
+
534
+ Raises:
535
+ InfraConnectionError: If Kafka connection fails
536
+ InfraTimeoutError: If publish times out
537
+ InfraUnavailableError: If circuit breaker is open
538
+ """
539
+ correlation_id = uuid4()
540
+
541
+ # Check circuit breaker before operation
542
+ async with self._circuit_breaker_lock:
543
+ await self._check_circuit_breaker("publish_snapshot", correlation_id)
544
+
545
+ ctx = ModelInfraErrorContext(
546
+ transport_type=EnumInfraTransportType.KAFKA,
547
+ operation="publish_snapshot",
548
+ target_name=self._config.topic,
549
+ correlation_id=correlation_id,
550
+ )
551
+
552
+ try:
553
+ # Build key and value for Kafka
554
+ key = snapshot.to_kafka_key().encode("utf-8")
555
+ value = snapshot.model_dump_json().encode("utf-8")
556
+
557
+ # Send and wait for acknowledgment
558
+ await self._producer.send_and_wait(
559
+ self._config.topic,
560
+ key=key,
561
+ value=value,
562
+ )
563
+
564
+ # Record success
565
+ async with self._circuit_breaker_lock:
566
+ await self._reset_circuit_breaker()
567
+
568
+ # Update cache if loaded (for read-after-write consistency)
569
+ if self._cache_loaded:
570
+ cache_key = snapshot.to_kafka_key()
571
+ async with self._cache_lock:
572
+ self._snapshot_cache[cache_key] = snapshot
573
+
574
+ logger.debug(
575
+ "Published snapshot for %s version %d",
576
+ snapshot.to_kafka_key(),
577
+ snapshot.snapshot_version,
578
+ extra={"correlation_id": str(correlation_id)},
579
+ )
580
+
581
+ except TimeoutError as e:
582
+ async with self._circuit_breaker_lock:
583
+ await self._record_circuit_failure("publish_snapshot", correlation_id)
584
+ raise InfraTimeoutError(
585
+ f"Timeout publishing snapshot: {snapshot.to_kafka_key()}",
586
+ context=ModelTimeoutErrorContext(
587
+ transport_type=ctx.transport_type,
588
+ operation=ctx.operation,
589
+ target_name=ctx.target_name,
590
+ correlation_id=ctx.correlation_id,
591
+ # timeout_seconds omitted - value not available in this context (defaults to None)
592
+ ),
593
+ ) from e
594
+
595
+ except Exception as e:
596
+ async with self._circuit_breaker_lock:
597
+ await self._record_circuit_failure("publish_snapshot", correlation_id)
598
+ raise InfraConnectionError(
599
+ f"Failed to publish snapshot: {snapshot.to_kafka_key()}",
600
+ context=ctx,
601
+ ) from e
602
+
603
+ async def publish_batch(
604
+ self,
605
+ snapshots: list[ModelRegistrationProjection],
606
+ *,
607
+ parallel: bool = True,
608
+ ) -> int:
609
+ """Publish multiple snapshots in a batch operation.
610
+
611
+ Publishes each projection as a snapshot, continuing on individual
612
+ failures. This is the recommended method for bulk snapshot jobs.
613
+
614
+ NOTE: This is a READ OPTIMIZATION. The event log remains source of truth.
615
+
616
+ Args:
617
+ snapshots: List of projections to publish as snapshots
618
+ parallel: If True (default), publish concurrently using asyncio.gather.
619
+ Set to False for sequential publishing (useful for debugging
620
+ or rate-limited scenarios).
621
+
622
+ Returns:
623
+ Count of successfully published snapshots.
624
+ May be less than len(snapshots) if some fail.
625
+
626
+ Raises:
627
+ InfraConnectionError: Only if connection fails before any publishing
628
+
629
+ Example:
630
+ >>> projections = await reader.get_all()
631
+ >>> count = await publisher.publish_batch(projections)
632
+ >>> print(f"Published {count}/{len(projections)} snapshots")
633
+ >>>
634
+ >>> # Sequential publishing for debugging
635
+ >>> count = await publisher.publish_batch(projections, parallel=False)
636
+ """
637
+ if not snapshots:
638
+ return 0
639
+
640
+ if parallel:
641
+ # Parallel publishing using asyncio.gather with return_exceptions=True
642
+ results = await asyncio.gather(
643
+ *[self.publish_snapshot(projection) for projection in snapshots],
644
+ return_exceptions=True,
645
+ )
646
+
647
+ success_count = 0
648
+ for i, result in enumerate(results):
649
+ if isinstance(result, Exception):
650
+ projection = snapshots[i]
651
+ logger.warning(
652
+ "Failed to publish snapshot %s:%s: %s",
653
+ projection.domain,
654
+ str(projection.entity_id),
655
+ str(result),
656
+ extra={
657
+ "entity_id": str(projection.entity_id),
658
+ "domain": projection.domain,
659
+ },
660
+ )
661
+ else:
662
+ success_count += 1
663
+ else:
664
+ # Sequential publishing (original behavior)
665
+ success_count = 0
666
+ for projection in snapshots:
667
+ try:
668
+ await self.publish_snapshot(projection)
669
+ success_count += 1
670
+ except (
671
+ InfraConnectionError,
672
+ InfraTimeoutError,
673
+ InfraUnavailableError,
674
+ ) as e:
675
+ logger.warning(
676
+ "Failed to publish snapshot %s:%s: %s",
677
+ projection.domain,
678
+ str(projection.entity_id),
679
+ str(e),
680
+ extra={
681
+ "entity_id": str(projection.entity_id),
682
+ "domain": projection.domain,
683
+ },
684
+ )
685
+ # Continue with remaining snapshots (best-effort)
686
+
687
+ logger.info(
688
+ "Batch publish completed: %d/%d snapshots published (parallel=%s)",
689
+ success_count,
690
+ len(snapshots),
691
+ parallel,
692
+ extra={"topic": self._config.topic},
693
+ )
694
+ return success_count
695
+
696
+ async def get_latest_snapshot(
697
+ self,
698
+ entity_id: str,
699
+ domain: str,
700
+ ) -> ModelRegistrationSnapshot | None:
701
+ """Retrieve the latest snapshot for an entity.
702
+
703
+ Reads the snapshot from an in-memory cache that is built from the
704
+ compacted Kafka topic. The cache is loaded lazily on first read.
705
+
706
+ IMPORTANT: Snapshot may be slightly stale. For guaranteed freshness,
707
+ combine with event log events since snapshot.updated_at. Call
708
+ refresh_cache() to reload the cache from Kafka.
709
+
710
+ Args:
711
+ entity_id: The entity identifier (UUID as string)
712
+ domain: The domain namespace (e.g., "registration")
713
+
714
+ Returns:
715
+ The latest snapshot if found, None otherwise.
716
+
717
+ Raises:
718
+ InfraConnectionError: If Kafka connection fails during cache load
719
+ InfraTimeoutError: If cache loading times out
720
+ InfraUnavailableError: If circuit breaker is open
721
+
722
+ Example:
723
+ >>> snapshot = await publisher.get_latest_snapshot("uuid", "registration")
724
+ >>> if snapshot is not None:
725
+ ... print(f"Entity state: {snapshot.current_state}")
726
+ ... else:
727
+ ... print("Entity not found")
728
+ """
729
+ correlation_id = uuid4()
730
+
731
+ # Load cache if not already loaded
732
+ # Circuit breaker check is now inside _load_cache_from_topic()
733
+ if not self._cache_loaded:
734
+ await self._load_cache_from_topic(correlation_id)
735
+
736
+ # Lookup in cache (O(1))
737
+ key = f"{domain}:{entity_id}"
738
+ async with self._cache_lock:
739
+ snapshot = self._snapshot_cache.get(key)
740
+
741
+ if snapshot is None:
742
+ logger.debug(
743
+ "Snapshot not found in cache for %s:%s",
744
+ domain,
745
+ entity_id,
746
+ extra={
747
+ "entity_id": entity_id,
748
+ "domain": domain,
749
+ "topic": self._config.topic,
750
+ "correlation_id": str(correlation_id),
751
+ },
752
+ )
753
+ else:
754
+ logger.debug(
755
+ "Snapshot retrieved from cache for %s:%s version %d",
756
+ domain,
757
+ entity_id,
758
+ snapshot.snapshot_version,
759
+ extra={
760
+ "entity_id": entity_id,
761
+ "domain": domain,
762
+ "snapshot_version": snapshot.snapshot_version,
763
+ "correlation_id": str(correlation_id),
764
+ },
765
+ )
766
+
767
+ return snapshot
768
+
769
+ async def _load_cache_from_topic(self, correlation_id: UUID) -> None:
770
+ """Load the snapshot cache from the compacted Kafka topic.
771
+
772
+ Reads all snapshots from the topic and populates the in-memory cache.
773
+ Uses getmany() with timeout to avoid blocking indefinitely.
774
+
775
+ This method is called lazily on the first read operation. It includes
776
+ circuit breaker protection to ensure consistent protection regardless
777
+ of the call site.
778
+
779
+ Performance Notes:
780
+ - Uses model_validate_json() for ~30% faster JSON parsing vs
781
+ json.loads() + model_validate()
782
+ - Logs progress every 1000 messages for observability during
783
+ large topic scans (5000+ messages)
784
+
785
+ Args:
786
+ correlation_id: Correlation ID for tracing
787
+
788
+ Raises:
789
+ InfraConnectionError: If Kafka connection fails
790
+ InfraTimeoutError: If consumer startup times out
791
+ InfraUnavailableError: If circuit breaker is open
792
+ """
793
+ # Progress logging interval (log every N messages)
794
+ progress_log_interval = 1000
795
+
796
+ # Check circuit breaker before operation - moved inside this method
797
+ # to ensure consistent protection regardless of call site
798
+ async with self._circuit_breaker_lock:
799
+ await self._check_circuit_breaker("load_cache", correlation_id)
800
+
801
+ async with self._cache_lock:
802
+ # Double-check after acquiring lock
803
+ if self._cache_loaded:
804
+ return
805
+
806
+ ctx = ModelInfraErrorContext(
807
+ transport_type=EnumInfraTransportType.KAFKA,
808
+ operation="load_cache",
809
+ target_name=self._config.topic,
810
+ correlation_id=correlation_id,
811
+ )
812
+
813
+ # Get bootstrap servers - must be explicitly configured
814
+ # We don't try to extract from producer because:
815
+ # 1. The producer may use internal/private attributes that vary by version
816
+ # 2. Mock producers don't have real bootstrap_servers
817
+ # 3. It's cleaner to require explicit configuration for reads
818
+ bootstrap_servers = self._bootstrap_servers
819
+
820
+ # Validate bootstrap_servers is non-empty string with proper format
821
+ if not bootstrap_servers or not bootstrap_servers.strip():
822
+ raise InfraConnectionError(
823
+ "bootstrap_servers not configured or empty. Provide bootstrap_servers "
824
+ "in constructor to enable snapshot reads "
825
+ "(e.g., 'localhost:9092' or 'kafka1:9092,kafka2:9092').",
826
+ context=ctx,
827
+ )
828
+
829
+ # Validate host:port format for each server
830
+ stripped_servers = bootstrap_servers.strip()
831
+ for server in stripped_servers.split(","):
832
+ server = server.strip()
833
+ if not server:
834
+ raise InfraConnectionError(
835
+ f"bootstrap_servers contains empty entries: '{bootstrap_servers}'. "
836
+ "Each entry must be in 'host:port' format.",
837
+ context=ctx,
838
+ )
839
+ if ":" not in server:
840
+ raise InfraConnectionError(
841
+ f"Invalid bootstrap server format '{server}'. "
842
+ "Expected 'host:port' (e.g., 'localhost:9092').",
843
+ context=ctx,
844
+ )
845
+ host, port_str = server.rsplit(":", 1)
846
+ if not host:
847
+ raise InfraConnectionError(
848
+ f"Invalid bootstrap server format '{server}'. "
849
+ "Host cannot be empty.",
850
+ context=ctx,
851
+ )
852
+ try:
853
+ port = int(port_str)
854
+ if port < 1 or port > 65535:
855
+ raise InfraConnectionError(
856
+ f"Invalid port {port} in '{server}'. "
857
+ "Port must be between 1 and 65535.",
858
+ context=ctx,
859
+ )
860
+ except ValueError:
861
+ raise InfraConnectionError(
862
+ f"Invalid port '{port_str}' in '{server}'. "
863
+ "Port must be a valid integer.",
864
+ context=ctx,
865
+ ) from None
866
+
867
+ # Use the stripped and validated version
868
+ bootstrap_servers = stripped_servers
869
+
870
+ # Import consumer here to avoid circular imports
871
+ from aiokafka import AIOKafkaConsumer
872
+ from pydantic import ValidationError
873
+
874
+ # Create consumer with unique group ID for this publisher instance
875
+ # Using a unique group ensures we get our own offset tracking
876
+ consumer_group = f"snapshot-reader-{self._config.topic}-{uuid4()!s}"
877
+ consumer = AIOKafkaConsumer(
878
+ self._config.topic,
879
+ bootstrap_servers=bootstrap_servers,
880
+ group_id=consumer_group,
881
+ auto_offset_reset="earliest",
882
+ enable_auto_commit=False,
883
+ )
884
+
885
+ try:
886
+ await consumer.start()
887
+ self._consumer = consumer
888
+ self._consumer_started = True
889
+
890
+ # Seek to beginning to read all snapshots
891
+ await consumer.seek_to_beginning()
892
+
893
+ # Read all messages from the topic until no more messages
894
+ messages_read = 0
895
+ tombstones_applied = 0
896
+ parse_errors = 0
897
+ last_progress_log = 0
898
+
899
+ while True:
900
+ # Poll with timeout - returns empty dict when no more messages
901
+ messages = await consumer.getmany(
902
+ timeout_ms=self._consumer_timeout_ms
903
+ )
904
+ if not messages:
905
+ break # No more messages within timeout
906
+
907
+ for _tp, msgs in messages.items():
908
+ for message in msgs:
909
+ key = message.key.decode("utf-8") if message.key else None
910
+
911
+ if key is None:
912
+ # Skip messages without keys
913
+ continue
914
+
915
+ if message.value is None:
916
+ # Tombstone - remove from cache
917
+ self._snapshot_cache.pop(key, None)
918
+ tombstones_applied += 1
919
+ else:
920
+ # Parse snapshot using model_validate_json for
921
+ # ~30% faster parsing (Pydantic v2 optimization)
922
+ try:
923
+ snapshot = (
924
+ ModelRegistrationSnapshot.model_validate_json(
925
+ message.value
926
+ )
927
+ )
928
+ self._snapshot_cache[key] = snapshot
929
+ messages_read += 1
930
+ except (ValidationError, ValueError) as e:
931
+ parse_errors += 1
932
+ logger.warning(
933
+ "Failed to parse snapshot for key %s: %s",
934
+ key,
935
+ str(e),
936
+ extra={
937
+ "key": key,
938
+ "correlation_id": str(correlation_id),
939
+ },
940
+ )
941
+
942
+ # Log progress for large topic scans
943
+ total_processed = messages_read + tombstones_applied
944
+ if (
945
+ total_processed - last_progress_log
946
+ >= progress_log_interval
947
+ ):
948
+ logger.info(
949
+ "Cache loading progress: %d messages processed "
950
+ "(%d snapshots, %d tombstones, %d errors)",
951
+ total_processed,
952
+ messages_read,
953
+ tombstones_applied,
954
+ parse_errors,
955
+ extra={
956
+ "topic": self._config.topic,
957
+ "messages_processed": total_processed,
958
+ "snapshots": messages_read,
959
+ "tombstones": tombstones_applied,
960
+ "parse_errors": parse_errors,
961
+ "correlation_id": str(correlation_id),
962
+ },
963
+ )
964
+ last_progress_log = total_processed
965
+
966
+ self._cache_loaded = True
967
+
968
+ # Reset circuit breaker on success
969
+ async with self._circuit_breaker_lock:
970
+ await self._reset_circuit_breaker()
971
+
972
+ # Calculate cache memory estimate (approx 2KB per snapshot)
973
+ cache_size = len(self._snapshot_cache)
974
+ estimated_memory_kb = cache_size * 2
975
+
976
+ logger.info(
977
+ "Snapshot cache loaded: %d snapshots, %d tombstones applied, "
978
+ "%d parse errors, cache size: %d entries (~%dKB)",
979
+ messages_read,
980
+ tombstones_applied,
981
+ parse_errors,
982
+ cache_size,
983
+ estimated_memory_kb,
984
+ extra={
985
+ "topic": self._config.topic,
986
+ "snapshots_loaded": messages_read,
987
+ "tombstones_applied": tombstones_applied,
988
+ "parse_errors": parse_errors,
989
+ "cache_size": cache_size,
990
+ "estimated_memory_kb": estimated_memory_kb,
991
+ "correlation_id": str(correlation_id),
992
+ },
993
+ )
994
+
995
+ # Stop consumer after successful cache load - consumer is only
996
+ # needed during the cache loading phase, not for ongoing reads
997
+ await self._cleanup_consumer()
998
+
999
+ except TimeoutError as e:
1000
+ async with self._circuit_breaker_lock:
1001
+ await self._record_circuit_failure("load_cache", correlation_id)
1002
+ await self._cleanup_consumer()
1003
+ raise InfraTimeoutError(
1004
+ f"Timeout loading snapshot cache from topic {self._config.topic}",
1005
+ context=ModelTimeoutErrorContext(
1006
+ transport_type=ctx.transport_type,
1007
+ operation=ctx.operation,
1008
+ target_name=ctx.target_name,
1009
+ correlation_id=ctx.correlation_id,
1010
+ timeout_seconds=float(self._consumer_timeout_ms) / 1000.0,
1011
+ ),
1012
+ ) from e
1013
+
1014
+ except Exception as e:
1015
+ async with self._circuit_breaker_lock:
1016
+ await self._record_circuit_failure("load_cache", correlation_id)
1017
+ await self._cleanup_consumer()
1018
+ raise InfraConnectionError(
1019
+ f"Failed to load snapshot cache from topic {self._config.topic}: {e}",
1020
+ context=ctx,
1021
+ ) from e
1022
+
1023
+ async def refresh_cache(self) -> int:
1024
+ """Refresh the snapshot cache by reloading from the Kafka topic.
1025
+
1026
+ Reloads all snapshots from the compacted topic. Use this to ensure
1027
+ the cache reflects the latest published state.
1028
+
1029
+ Error Recovery:
1030
+ If cache loading fails, the existing cache is preserved to avoid
1031
+ leaving the system in a broken state. This follows the principle
1032
+ of graceful degradation - stale data is better than no data.
1033
+
1034
+ Returns:
1035
+ Number of snapshots loaded into the cache.
1036
+
1037
+ Raises:
1038
+ InfraConnectionError: If Kafka connection fails
1039
+ InfraTimeoutError: If cache loading times out
1040
+ InfraUnavailableError: If circuit breaker is open
1041
+
1042
+ Example:
1043
+ >>> count = await publisher.refresh_cache()
1044
+ >>> print(f"Loaded {count} snapshots")
1045
+ """
1046
+ correlation_id = uuid4()
1047
+
1048
+ # Check circuit breaker before operation
1049
+ async with self._circuit_breaker_lock:
1050
+ await self._check_circuit_breaker("refresh_cache", correlation_id)
1051
+
1052
+ # Stop existing consumer if running
1053
+ if self._consumer_started and self._consumer is not None:
1054
+ try:
1055
+ await self._consumer.stop()
1056
+ except Exception:
1057
+ pass
1058
+ self._consumer_started = False
1059
+ self._consumer = None
1060
+
1061
+ # Preserve existing cache state before attempting reload
1062
+ # This allows rollback on failure (graceful degradation)
1063
+ async with self._cache_lock:
1064
+ old_cache = self._snapshot_cache.copy()
1065
+ old_cache_loaded = self._cache_loaded
1066
+ self._snapshot_cache.clear()
1067
+ self._cache_loaded = False
1068
+
1069
+ try:
1070
+ await self._load_cache_from_topic(correlation_id)
1071
+
1072
+ async with self._cache_lock:
1073
+ count = len(self._snapshot_cache)
1074
+
1075
+ logger.info(
1076
+ "Snapshot cache refreshed with %d snapshots",
1077
+ count,
1078
+ extra={
1079
+ "topic": self._config.topic,
1080
+ "snapshot_count": count,
1081
+ "correlation_id": str(correlation_id),
1082
+ },
1083
+ )
1084
+
1085
+ return count
1086
+
1087
+ except Exception as e:
1088
+ # Restore previous cache on failure (graceful degradation)
1089
+ async with self._cache_lock:
1090
+ self._snapshot_cache = old_cache
1091
+ self._cache_loaded = old_cache_loaded
1092
+
1093
+ logger.warning(
1094
+ "Cache refresh failed, preserving existing cache with %d snapshots",
1095
+ len(old_cache),
1096
+ extra={
1097
+ "topic": self._config.topic,
1098
+ "preserved_count": len(old_cache),
1099
+ "error_type": type(e).__name__,
1100
+ "correlation_id": str(correlation_id),
1101
+ },
1102
+ )
1103
+ raise
1104
+
1105
+ @property
1106
+ def cache_size(self) -> int:
1107
+ """Get the number of snapshots in the cache.
1108
+
1109
+ Returns:
1110
+ Number of snapshots currently in the cache.
1111
+
1112
+ Note:
1113
+ This is a synchronous property that does not trigger cache loading.
1114
+ Call get_latest_snapshot() or refresh_cache() to load the cache first.
1115
+ """
1116
+ return len(self._snapshot_cache)
1117
+
1118
+ @property
1119
+ def is_cache_loaded(self) -> bool:
1120
+ """Check if the cache has been loaded.
1121
+
1122
+ Returns:
1123
+ True if the cache has been loaded from Kafka, False otherwise.
1124
+ """
1125
+ return self._cache_loaded
1126
+
1127
+ async def delete_snapshot(
1128
+ self,
1129
+ entity_id: str,
1130
+ domain: str,
1131
+ ) -> bool:
1132
+ """Publish a tombstone to remove a snapshot.
1133
+
1134
+ In Kafka compaction, a message with null value acts as a tombstone,
1135
+ causing the key to be removed during compaction. This effectively
1136
+ deletes the snapshot for the given entity.
1137
+
1138
+ NOTE: This does NOT delete events from the event log. The event log
1139
+ is immutable and retains full history. Tombstones only affect the
1140
+ snapshot read path.
1141
+
1142
+ Use Cases:
1143
+ - Node deregistration (permanent removal)
1144
+ - Entity lifecycle completion
1145
+ - Data retention cleanup
1146
+
1147
+ Args:
1148
+ entity_id: The entity identifier (UUID as string)
1149
+ domain: The domain namespace (e.g., "registration")
1150
+
1151
+ Returns:
1152
+ True if tombstone was published successfully.
1153
+ False if publish failed (caller should retry or handle).
1154
+
1155
+ Raises:
1156
+ InfraUnavailableError: If circuit breaker is open (fail-fast).
1157
+
1158
+ Example:
1159
+ >>> # Handle node deregistration
1160
+ >>> deleted = await publisher.delete_snapshot(str(node_id), "registration")
1161
+ >>> if not deleted:
1162
+ ... logger.warning(f"Failed to delete snapshot for {node_id}")
1163
+ """
1164
+ correlation_id = uuid4()
1165
+
1166
+ # Check circuit breaker before operation - let InfraUnavailableError propagate
1167
+ # per ONEX fail-fast principles (callers need to know service is unavailable)
1168
+ async with self._circuit_breaker_lock:
1169
+ await self._check_circuit_breaker("delete_snapshot", correlation_id)
1170
+
1171
+ try:
1172
+ # Build key for tombstone
1173
+ key = f"{domain}:{entity_id}".encode()
1174
+
1175
+ # Publish tombstone (null value)
1176
+ await self._producer.send_and_wait(
1177
+ self._config.topic,
1178
+ key=key,
1179
+ value=None, # Tombstone - null value triggers deletion on compaction
1180
+ )
1181
+
1182
+ # Record success
1183
+ async with self._circuit_breaker_lock:
1184
+ await self._reset_circuit_breaker()
1185
+
1186
+ # Clear version tracker for this entity (thread-safe)
1187
+ tracker_key = f"{domain}:{entity_id}"
1188
+ async with self._version_tracker_lock:
1189
+ self._version_tracker.pop(tracker_key, None)
1190
+
1191
+ # Also remove from cache if loaded (for consistency)
1192
+ if self._cache_loaded:
1193
+ async with self._cache_lock:
1194
+ self._snapshot_cache.pop(tracker_key, None)
1195
+
1196
+ logger.info(
1197
+ "Published tombstone for %s:%s",
1198
+ domain,
1199
+ entity_id,
1200
+ extra={"correlation_id": str(correlation_id)},
1201
+ )
1202
+ return True
1203
+
1204
+ except Exception:
1205
+ async with self._circuit_breaker_lock:
1206
+ await self._record_circuit_failure("delete_snapshot", correlation_id)
1207
+
1208
+ logger.exception(
1209
+ "Failed to publish tombstone for %s:%s",
1210
+ domain,
1211
+ entity_id,
1212
+ extra={"correlation_id": str(correlation_id)},
1213
+ )
1214
+ return False
1215
+
1216
+ async def publish_from_projection(
1217
+ self,
1218
+ projection: ModelRegistrationProjection,
1219
+ *,
1220
+ node_name: str | None = None,
1221
+ ) -> ModelRegistrationSnapshot:
1222
+ """Create and publish a snapshot from a projection.
1223
+
1224
+ Convenience method that handles version tracking automatically.
1225
+ Converts the projection to a snapshot model, assigns the next
1226
+ version number, and publishes to Kafka.
1227
+
1228
+ This is the recommended method for publishing snapshots when you
1229
+ have a projection and want automatic version management.
1230
+
1231
+ Args:
1232
+ projection: The projection to convert and publish
1233
+ node_name: Optional node name to include in snapshot.
1234
+ Not stored in projection, must be provided externally
1235
+ (e.g., from introspection data).
1236
+
1237
+ Returns:
1238
+ The published snapshot model with assigned version
1239
+
1240
+ Raises:
1241
+ InfraConnectionError: If Kafka connection fails
1242
+ InfraTimeoutError: If publish times out
1243
+ InfraUnavailableError: If circuit breaker is open
1244
+
1245
+ Example:
1246
+ >>> # Automatic versioning
1247
+ >>> snapshot1 = await publisher.publish_from_projection(proj)
1248
+ >>> print(snapshot1.snapshot_version) # 1
1249
+ >>>
1250
+ >>> # Next snapshot for same entity increments version
1251
+ >>> snapshot2 = await publisher.publish_from_projection(proj)
1252
+ >>> print(snapshot2.snapshot_version) # 2
1253
+ >>>
1254
+ >>> # Include node name for service discovery
1255
+ >>> snapshot = await publisher.publish_from_projection(
1256
+ ... projection,
1257
+ ... node_name="PostgresAdapter",
1258
+ ... )
1259
+ """
1260
+ entity_id_str = str(projection.entity_id)
1261
+ version = await self._get_next_version(entity_id_str, projection.domain)
1262
+
1263
+ # Create snapshot from projection
1264
+ snapshot = ModelRegistrationSnapshot.from_projection(
1265
+ projection=projection,
1266
+ snapshot_version=version,
1267
+ snapshot_created_at=datetime.now(UTC),
1268
+ node_name=node_name,
1269
+ )
1270
+
1271
+ # Publish the snapshot model
1272
+ await self._publish_snapshot_model(snapshot)
1273
+
1274
+ return snapshot
1275
+
1276
+ async def publish_snapshot_batch(
1277
+ self,
1278
+ snapshots: list[ModelRegistrationSnapshot],
1279
+ ) -> int:
1280
+ """Publish multiple pre-built snapshots in a batch.
1281
+
1282
+ Similar to publish_batch but for pre-built ModelRegistrationSnapshot
1283
+ objects instead of projections. Use this when you have already
1284
+ constructed snapshot models (e.g., from a different source).
1285
+
1286
+ Args:
1287
+ snapshots: List of snapshot models to publish
1288
+
1289
+ Returns:
1290
+ Count of successfully published snapshots
1291
+
1292
+ Example:
1293
+ >>> snapshots = [
1294
+ ... ModelRegistrationSnapshot.from_projection(p, version=1, ...)
1295
+ ... for p in projections
1296
+ ... ]
1297
+ >>> count = await publisher.publish_snapshot_batch(snapshots)
1298
+ """
1299
+ if not snapshots:
1300
+ return 0
1301
+
1302
+ success_count = 0
1303
+ for snapshot in snapshots:
1304
+ try:
1305
+ await self._publish_snapshot_model(snapshot)
1306
+ success_count += 1
1307
+ except (
1308
+ InfraConnectionError,
1309
+ InfraTimeoutError,
1310
+ InfraUnavailableError,
1311
+ ) as e:
1312
+ logger.warning(
1313
+ "Failed to publish snapshot %s version %d: %s",
1314
+ snapshot.to_kafka_key(),
1315
+ snapshot.snapshot_version,
1316
+ str(e),
1317
+ )
1318
+ # Continue with remaining snapshots (best-effort)
1319
+
1320
+ logger.info(
1321
+ "Batch publish completed: %d/%d snapshots published",
1322
+ success_count,
1323
+ len(snapshots),
1324
+ extra={"topic": self._config.topic},
1325
+ )
1326
+ return success_count
1327
+
1328
+
1329
+ __all__: list[str] = ["SnapshotPublisherRegistration"]