omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_handler_source_mode.py +16 -2
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
- omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
- omnibase_infra/event_bus/event_bus_kafka.py +105 -47
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
- omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/handlers/handler_consul.py +2 -0
- omnibase_infra/handlers/mixins/__init__.py +5 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/mixin_node_introspection.py +189 -19
- omnibase_infra/models/__init__.py +8 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/discovery/model_introspection_config.py +25 -17
- omnibase_infra/models/dispatch/__init__.py +8 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
- omnibase_infra/models/registration/__init__.py +9 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
- omnibase_infra/models/runtime/__init__.py +9 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
- omnibase_infra/nodes/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
- omnibase_infra/nodes/reducers/models/__init__.py +7 -2
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
- omnibase_infra/protocols/__init__.py +3 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/runtime/__init__.py +60 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_source_resolver.py +43 -2
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/models/__init__.py +13 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/service_kernel.py +76 -6
- omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
- omnibase_infra/runtime/service_runtime_host_process.py +770 -20
- omnibase_infra/runtime/transition_notification_publisher.py +3 -2
- omnibase_infra/runtime/util_wiring.py +206 -62
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
- omnibase_infra/services/session/config_consumer.py +25 -8
- omnibase_infra/services/session/config_store.py +2 -2
- omnibase_infra/services/session/consumer.py +1 -1
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/typed_dict/__init__.py +9 -1
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/utils/__init__.py +9 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/validation/infra_validators.py +18 -1
- omnibase_infra/validation/validation_exemptions.yaml +192 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 OmniNode Team
|
|
3
|
+
#
|
|
4
|
+
# ONEX Node Contract
|
|
5
|
+
# Node: NodeLedgerWriteEffect
|
|
6
|
+
#
|
|
7
|
+
# Capability-oriented effect node for event ledger write operations.
|
|
8
|
+
# Provides append-only audit ledger storage with idempotent writes.
|
|
9
|
+
#
|
|
10
|
+
# This contract defines the interface for storing events to the audit ledger
|
|
11
|
+
# and querying events by various criteria. The ledger uses PostgreSQL with
|
|
12
|
+
# a (topic, partition, kafka_offset) unique constraint for idempotency.
|
|
13
|
+
#
|
|
14
|
+
# Related Tickets:
|
|
15
|
+
# - OMN-1646: Event Ledger Schema and Models
|
|
16
|
+
# - OMN-1647: Ledger Write Effect Node
|
|
17
|
+
# Contract identifiers
|
|
18
|
+
name: "node_ledger_write_effect"
|
|
19
|
+
contract_name: "node_ledger_write_effect"
|
|
20
|
+
node_name: "node_ledger_write_effect"
|
|
21
|
+
contract_version:
|
|
22
|
+
major: 0
|
|
23
|
+
minor: 1
|
|
24
|
+
patch: 0
|
|
25
|
+
node_version:
|
|
26
|
+
major: 0
|
|
27
|
+
minor: 1
|
|
28
|
+
patch: 0
|
|
29
|
+
# Node type
|
|
30
|
+
node_type: "EFFECT_GENERIC"
|
|
31
|
+
# Description
|
|
32
|
+
description: >
|
|
33
|
+
Capability-oriented effect node for event ledger write operations. Handles appending events to the PostgreSQL audit ledger with idempotent write support via unique constraint on (topic, partition, kafka_offset). Supports querying events by correlation ID, event type, topic, or time range.
|
|
34
|
+
|
|
35
|
+
# Strongly typed I/O models
|
|
36
|
+
input_model:
|
|
37
|
+
name: "ModelPayloadLedgerAppend"
|
|
38
|
+
module: "omnibase_infra.nodes.reducers.models"
|
|
39
|
+
description: "Input model for ledger append operations (event payload with Kafka position)."
|
|
40
|
+
output_model:
|
|
41
|
+
name: "ModelLedgerAppendResult"
|
|
42
|
+
module: "omnibase_infra.nodes.node_ledger_write_effect.models"
|
|
43
|
+
description: "Output model for ledger append results (success, entry ID, duplicate flag)."
|
|
44
|
+
# Capability declaration (capability-oriented naming)
|
|
45
|
+
capabilities:
|
|
46
|
+
- name: "ledger.write"
|
|
47
|
+
description: "Append events to the audit ledger with idempotent write support"
|
|
48
|
+
version: "0.1.0"
|
|
49
|
+
- name: "ledger.write.append"
|
|
50
|
+
description: "Append a single event to the ledger"
|
|
51
|
+
- name: "ledger.query"
|
|
52
|
+
description: "Query events from the audit ledger"
|
|
53
|
+
- name: "ledger.query.by_correlation"
|
|
54
|
+
description: "Query events by correlation ID"
|
|
55
|
+
- name: "ledger.query.by_topic"
|
|
56
|
+
description: "Query events by Kafka topic"
|
|
57
|
+
- name: "ledger.query.by_time_range"
|
|
58
|
+
description: "Query events within a time range"
|
|
59
|
+
# Event-driven topic configuration
|
|
60
|
+
# Uses {env}.{namespace} placeholders for environment-aware topics
|
|
61
|
+
consumed_events:
|
|
62
|
+
- topic: "{env}.{namespace}.onex.cmd.ledger-append.v1"
|
|
63
|
+
event_type: "LedgerAppendCommand"
|
|
64
|
+
message_category: "COMMAND"
|
|
65
|
+
description: "Command to append an event to the audit ledger"
|
|
66
|
+
- topic: "{env}.{namespace}.onex.cmd.ledger-query.v1"
|
|
67
|
+
event_type: "LedgerQueryCommand"
|
|
68
|
+
message_category: "COMMAND"
|
|
69
|
+
description: "Command to query events from the audit ledger"
|
|
70
|
+
published_events:
|
|
71
|
+
- topic: "{env}.{namespace}.onex.evt.ledger-appended.v1"
|
|
72
|
+
event_type: "LedgerAppendedEvent"
|
|
73
|
+
description: "Confirmation that event was appended to the ledger"
|
|
74
|
+
- topic: "{env}.{namespace}.onex.evt.ledger-query-result.v1"
|
|
75
|
+
event_type: "LedgerQueryResultEvent"
|
|
76
|
+
description: "Result of a ledger query operation"
|
|
77
|
+
# IO operations (EFFECT node specific)
|
|
78
|
+
# Note: correlation_id is optional (UUID | None) - audit ledger must never drop events.
|
|
79
|
+
# Provide correlation_id for distributed tracing when available.
|
|
80
|
+
io_operations:
|
|
81
|
+
- operation: "ledger.append"
|
|
82
|
+
description: "Append an event to the audit ledger with idempotent write support"
|
|
83
|
+
input_fields:
|
|
84
|
+
- topic: "str"
|
|
85
|
+
- partition: "int"
|
|
86
|
+
- kafka_offset: "int"
|
|
87
|
+
- event_key: "str | None"
|
|
88
|
+
- event_value: "str"
|
|
89
|
+
- onex_headers: "dict[str, object]"
|
|
90
|
+
- correlation_id: "UUID | None"
|
|
91
|
+
- envelope_id: "UUID | None"
|
|
92
|
+
- event_type: "str | None"
|
|
93
|
+
- source: "str | None"
|
|
94
|
+
- event_timestamp: "datetime | None"
|
|
95
|
+
output_fields:
|
|
96
|
+
- success: "bool"
|
|
97
|
+
- ledger_entry_id: "UUID | None"
|
|
98
|
+
- duplicate: "bool"
|
|
99
|
+
- topic: "str"
|
|
100
|
+
- partition: "int"
|
|
101
|
+
- kafka_offset: "int"
|
|
102
|
+
# Idempotent: Uses ON CONFLICT DO NOTHING with (topic, partition, kafka_offset).
|
|
103
|
+
# Duplicate calls return duplicate=True without creating new entries.
|
|
104
|
+
idempotent: true
|
|
105
|
+
- operation: "ledger.query"
|
|
106
|
+
description: "Query events from the audit ledger by various criteria"
|
|
107
|
+
input_fields:
|
|
108
|
+
- correlation_id: "UUID | None"
|
|
109
|
+
- event_type: "str | None"
|
|
110
|
+
- topic: "str | None"
|
|
111
|
+
- start_time: "datetime | None"
|
|
112
|
+
- end_time: "datetime | None"
|
|
113
|
+
- limit: "int"
|
|
114
|
+
- offset: "int"
|
|
115
|
+
output_fields:
|
|
116
|
+
- entries: "list[ModelLedgerEntry]"
|
|
117
|
+
- total_count: "int"
|
|
118
|
+
- has_more: "bool"
|
|
119
|
+
idempotent: true
|
|
120
|
+
# Handler routing for ledger operations
|
|
121
|
+
# Handlers compose with HandlerDb for PostgreSQL operations
|
|
122
|
+
handler_routing:
|
|
123
|
+
routing_strategy: "operation_match"
|
|
124
|
+
handlers:
|
|
125
|
+
- operation: "ledger.append"
|
|
126
|
+
handler_class: "HandlerLedgerAppend"
|
|
127
|
+
handler_module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append"
|
|
128
|
+
description: "Idempotent append to event ledger with duplicate detection"
|
|
129
|
+
- operation: "ledger.query"
|
|
130
|
+
handler_class: "HandlerLedgerQuery"
|
|
131
|
+
handler_module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query"
|
|
132
|
+
description: "Query ledger by correlation_id, time_range, event_type, topic"
|
|
133
|
+
# Dependencies (protocols this node requires)
|
|
134
|
+
# Note: Ledger handlers compose with HandlerDb internally
|
|
135
|
+
dependencies:
|
|
136
|
+
- name: "handler_ledger_append"
|
|
137
|
+
type: "handler"
|
|
138
|
+
class_name: "HandlerLedgerAppend"
|
|
139
|
+
module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append"
|
|
140
|
+
description: "Handler for idempotent ledger append operations"
|
|
141
|
+
- name: "handler_ledger_query"
|
|
142
|
+
type: "handler"
|
|
143
|
+
class_name: "HandlerLedgerQuery"
|
|
144
|
+
module: "omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query"
|
|
145
|
+
description: "Handler for ledger query operations"
|
|
146
|
+
- name: "handler_db"
|
|
147
|
+
type: "handler"
|
|
148
|
+
class_name: "HandlerDb"
|
|
149
|
+
module: "omnibase_infra.handlers.handler_db"
|
|
150
|
+
description: "PostgreSQL database handler (composed by ledger handlers)"
|
|
151
|
+
# Error handling configuration
|
|
152
|
+
error_handling:
|
|
153
|
+
retry_policy:
|
|
154
|
+
max_retries: 3
|
|
155
|
+
initial_delay_ms: 100
|
|
156
|
+
max_delay_ms: 5000
|
|
157
|
+
exponential_base: 2
|
|
158
|
+
retry_on:
|
|
159
|
+
- "InfraConnectionError"
|
|
160
|
+
- "InfraTimeoutError"
|
|
161
|
+
circuit_breaker:
|
|
162
|
+
enabled: true
|
|
163
|
+
failure_threshold: 5
|
|
164
|
+
reset_timeout_ms: 60000
|
|
165
|
+
error_types:
|
|
166
|
+
- name: "DatabaseConnectionError"
|
|
167
|
+
description: "Unable to connect to PostgreSQL"
|
|
168
|
+
recoverable: true
|
|
169
|
+
retry_strategy: "exponential_backoff"
|
|
170
|
+
- name: "DatabaseTimeoutError"
|
|
171
|
+
description: "Database operation timed out"
|
|
172
|
+
recoverable: true
|
|
173
|
+
retry_strategy: "exponential_backoff"
|
|
174
|
+
- name: "ValidationError"
|
|
175
|
+
description: "Input validation failed"
|
|
176
|
+
recoverable: false
|
|
177
|
+
retry_strategy: "none"
|
|
178
|
+
# Health check configuration
|
|
179
|
+
health_check:
|
|
180
|
+
enabled: true
|
|
181
|
+
endpoint: "/health"
|
|
182
|
+
interval_seconds: 30
|
|
183
|
+
timeout_ms: 5000
|
|
184
|
+
checks:
|
|
185
|
+
- name: "database_backend"
|
|
186
|
+
type: "connection"
|
|
187
|
+
critical: true
|
|
188
|
+
# Metadata
|
|
189
|
+
metadata:
|
|
190
|
+
author: "OmniNode Team"
|
|
191
|
+
license: "MIT"
|
|
192
|
+
created: "2026-01-29"
|
|
193
|
+
tags:
|
|
194
|
+
- effect
|
|
195
|
+
- ledger
|
|
196
|
+
- audit
|
|
197
|
+
- storage
|
|
198
|
+
- postgresql
|
|
199
|
+
- capability-oriented
|
|
200
|
+
- idempotent
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 OmniNode Team
|
|
3
|
+
"""Handlers for event ledger persistence operations.
|
|
4
|
+
|
|
5
|
+
This package provides handlers for the event ledger effect node:
|
|
6
|
+
- HandlerLedgerAppend: Idempotent INSERT with duplicate detection
|
|
7
|
+
- HandlerLedgerQuery: Query by correlation_id, time_range, etc.
|
|
8
|
+
|
|
9
|
+
Both handlers compose with HandlerDb for PostgreSQL operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_append import (
|
|
13
|
+
HandlerLedgerAppend,
|
|
14
|
+
)
|
|
15
|
+
from omnibase_infra.nodes.node_ledger_write_effect.handlers.handler_ledger_query import (
|
|
16
|
+
HandlerLedgerQuery,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"HandlerLedgerAppend",
|
|
21
|
+
"HandlerLedgerQuery",
|
|
22
|
+
]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 OmniNode Team
|
|
3
|
+
"""Handler for ledger append operations with idempotent write support.
|
|
4
|
+
|
|
5
|
+
This handler composes with HandlerDb for PostgreSQL operations, providing
|
|
6
|
+
a typed interface for appending events to the audit ledger with duplicate
|
|
7
|
+
detection via ON CONFLICT DO NOTHING.
|
|
8
|
+
|
|
9
|
+
Bytes Encoding:
|
|
10
|
+
The ModelPayloadLedgerAppend contains base64-encoded event_key and event_value
|
|
11
|
+
since bytes cannot safely cross intent boundaries. This handler decodes them
|
|
12
|
+
to bytes before passing to PostgreSQL, which stores them as BYTEA.
|
|
13
|
+
|
|
14
|
+
Idempotency:
|
|
15
|
+
Uses INSERT ... ON CONFLICT (topic, partition, kafka_offset) DO NOTHING RETURNING.
|
|
16
|
+
If RETURNING returns no rows, the event was already in the ledger (duplicate).
|
|
17
|
+
Duplicates are not errors - they enable idempotent replay.
|
|
18
|
+
|
|
19
|
+
Design Decision - Composition with HandlerDb:
|
|
20
|
+
This handler delegates SQL execution to HandlerDb rather than using asyncpg
|
|
21
|
+
directly. This provides:
|
|
22
|
+
- Circuit breaker protection
|
|
23
|
+
- Error classification (transient vs permanent)
|
|
24
|
+
- Connection pool management
|
|
25
|
+
- Consistent error handling
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import base64
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
from typing import TYPE_CHECKING
|
|
34
|
+
from uuid import UUID, uuid4
|
|
35
|
+
|
|
36
|
+
from omnibase_core.models.dispatch import ModelHandlerOutput
|
|
37
|
+
from omnibase_infra.enums import (
|
|
38
|
+
EnumHandlerType,
|
|
39
|
+
EnumHandlerTypeCategory,
|
|
40
|
+
EnumInfraTransportType,
|
|
41
|
+
EnumResponseStatus,
|
|
42
|
+
)
|
|
43
|
+
from omnibase_infra.errors import ModelInfraErrorContext, RuntimeHostError
|
|
44
|
+
from omnibase_infra.nodes.node_ledger_write_effect.models import ModelLedgerAppendResult
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from omnibase_core.container import ModelONEXContainer
|
|
48
|
+
from omnibase_infra.handlers.handler_db import HandlerDb
|
|
49
|
+
from omnibase_infra.nodes.reducers.models import ModelPayloadLedgerAppend
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
# Handler ID for ModelHandlerOutput
|
|
54
|
+
HANDLER_ID_LEDGER_APPEND: str = "ledger-append-handler"
|
|
55
|
+
|
|
56
|
+
# SQL for idempotent append with duplicate detection
|
|
57
|
+
# Uses RETURNING to detect whether insert succeeded (returns row) or
|
|
58
|
+
# ON CONFLICT was triggered (returns nothing)
|
|
59
|
+
_SQL_APPEND = """
|
|
60
|
+
INSERT INTO event_ledger (
|
|
61
|
+
topic,
|
|
62
|
+
partition,
|
|
63
|
+
kafka_offset,
|
|
64
|
+
event_key,
|
|
65
|
+
event_value,
|
|
66
|
+
onex_headers,
|
|
67
|
+
envelope_id,
|
|
68
|
+
correlation_id,
|
|
69
|
+
event_type,
|
|
70
|
+
source,
|
|
71
|
+
event_timestamp
|
|
72
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
73
|
+
ON CONFLICT (topic, partition, kafka_offset) DO NOTHING
|
|
74
|
+
RETURNING ledger_entry_id
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class HandlerLedgerAppend:
|
|
79
|
+
"""Handler for appending events to the audit ledger with idempotent writes.
|
|
80
|
+
|
|
81
|
+
This handler implements the append operation for ProtocolLedgerPersistence,
|
|
82
|
+
composing with HandlerDb for PostgreSQL operations. It provides:
|
|
83
|
+
|
|
84
|
+
- Base64 decoding of event payloads to bytes
|
|
85
|
+
- Idempotent INSERT via ON CONFLICT DO NOTHING
|
|
86
|
+
- Duplicate detection via RETURNING clause
|
|
87
|
+
- Type-safe input/output with Pydantic models
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
handler_type: EnumHandlerType.INFRA_HANDLER
|
|
91
|
+
handler_category: EnumHandlerTypeCategory.EFFECT
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> handler = HandlerLedgerAppend(container, db_handler)
|
|
95
|
+
>>> await handler.initialize({})
|
|
96
|
+
>>> result = await handler.append(payload)
|
|
97
|
+
>>> if result.duplicate:
|
|
98
|
+
... logger.info("Event already in ledger")
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
container: ModelONEXContainer,
|
|
104
|
+
db_handler: HandlerDb,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Initialize the ledger append handler.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
container: ONEX dependency injection container.
|
|
110
|
+
db_handler: Initialized HandlerDb instance for PostgreSQL operations.
|
|
111
|
+
"""
|
|
112
|
+
self._container = container
|
|
113
|
+
self._db_handler = db_handler
|
|
114
|
+
self._initialized: bool = False
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def handler_type(self) -> EnumHandlerType:
|
|
118
|
+
"""Return the architectural role of this handler."""
|
|
119
|
+
return EnumHandlerType.INFRA_HANDLER
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def handler_category(self) -> EnumHandlerTypeCategory:
|
|
123
|
+
"""Return the behavioral classification of this handler."""
|
|
124
|
+
return EnumHandlerTypeCategory.EFFECT
|
|
125
|
+
|
|
126
|
+
async def initialize(self, config: dict[str, object]) -> None:
|
|
127
|
+
"""Initialize the handler.
|
|
128
|
+
|
|
129
|
+
The underlying HandlerDb must already be initialized before
|
|
130
|
+
calling this method.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: Configuration dict (currently unused).
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
RuntimeHostError: If HandlerDb is not initialized.
|
|
137
|
+
"""
|
|
138
|
+
# Verify db_handler is initialized
|
|
139
|
+
if not getattr(self._db_handler, "_initialized", False):
|
|
140
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
141
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
142
|
+
operation="initialize",
|
|
143
|
+
)
|
|
144
|
+
raise RuntimeHostError(
|
|
145
|
+
"HandlerDb must be initialized before HandlerLedgerAppend",
|
|
146
|
+
context=ctx,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self._initialized = True
|
|
150
|
+
logger.info(
|
|
151
|
+
"%s initialized successfully",
|
|
152
|
+
self.__class__.__name__,
|
|
153
|
+
extra={"handler": self.__class__.__name__},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def shutdown(self) -> None:
|
|
157
|
+
"""Shutdown the handler.
|
|
158
|
+
|
|
159
|
+
Does not shutdown the underlying HandlerDb - that is managed separately.
|
|
160
|
+
"""
|
|
161
|
+
self._initialized = False
|
|
162
|
+
logger.info("HandlerLedgerAppend shutdown complete")
|
|
163
|
+
|
|
164
|
+
async def append(
|
|
165
|
+
self,
|
|
166
|
+
payload: ModelPayloadLedgerAppend,
|
|
167
|
+
) -> ModelLedgerAppendResult:
|
|
168
|
+
"""Append an event to the audit ledger.
|
|
169
|
+
|
|
170
|
+
Decodes base64 event data, executes idempotent INSERT, and detects
|
|
171
|
+
duplicates via the RETURNING clause.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
payload: Event payload containing Kafka position and event data.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ModelLedgerAppendResult with success, ledger_entry_id, and duplicate flag.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
RuntimeHostError: If handler not initialized or validation fails.
|
|
181
|
+
InfraConnectionError: If database connection fails.
|
|
182
|
+
InfraTimeoutError: If operation times out.
|
|
183
|
+
"""
|
|
184
|
+
correlation_id = payload.correlation_id or uuid4()
|
|
185
|
+
|
|
186
|
+
if not self._initialized:
|
|
187
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
188
|
+
correlation_id=correlation_id,
|
|
189
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
190
|
+
operation="ledger.append",
|
|
191
|
+
)
|
|
192
|
+
raise RuntimeHostError(
|
|
193
|
+
"HandlerLedgerAppend not initialized. Call initialize() first.",
|
|
194
|
+
context=ctx,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Decode base64 event data to bytes
|
|
198
|
+
event_key_bytes = (
|
|
199
|
+
self._decode_base64(payload.event_key) if payload.event_key else None
|
|
200
|
+
)
|
|
201
|
+
event_value_bytes = self._decode_base64(payload.event_value)
|
|
202
|
+
|
|
203
|
+
# Serialize onex_headers to JSON string for JSONB column
|
|
204
|
+
onex_headers_json = json.dumps(payload.onex_headers)
|
|
205
|
+
|
|
206
|
+
# Build parameters for INSERT
|
|
207
|
+
# Order must match $1..$11 in _SQL_APPEND
|
|
208
|
+
parameters: list[object] = [
|
|
209
|
+
payload.topic, # $1
|
|
210
|
+
payload.partition, # $2
|
|
211
|
+
payload.kafka_offset, # $3
|
|
212
|
+
event_key_bytes, # $4 (BYTEA, nullable)
|
|
213
|
+
event_value_bytes, # $5 (BYTEA)
|
|
214
|
+
onex_headers_json, # $6 (JSONB)
|
|
215
|
+
str(payload.envelope_id)
|
|
216
|
+
if payload.envelope_id
|
|
217
|
+
else None, # $7 (UUID, nullable)
|
|
218
|
+
str(payload.correlation_id)
|
|
219
|
+
if payload.correlation_id
|
|
220
|
+
else None, # $8 (UUID, nullable)
|
|
221
|
+
payload.event_type, # $9 (TEXT, nullable)
|
|
222
|
+
payload.source, # $10 (TEXT, nullable)
|
|
223
|
+
payload.event_timestamp, # $11 (TIMESTAMPTZ, nullable)
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# Build envelope for HandlerDb
|
|
227
|
+
envelope: dict[str, object] = {
|
|
228
|
+
"operation": "db.query", # Use query because RETURNING produces rows
|
|
229
|
+
"payload": {
|
|
230
|
+
"sql": _SQL_APPEND,
|
|
231
|
+
"parameters": parameters,
|
|
232
|
+
},
|
|
233
|
+
"correlation_id": str(correlation_id),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
logger.debug(
|
|
237
|
+
"Appending event to ledger",
|
|
238
|
+
extra={
|
|
239
|
+
"topic": payload.topic,
|
|
240
|
+
"partition": payload.partition,
|
|
241
|
+
"offset": payload.kafka_offset,
|
|
242
|
+
"correlation_id": str(correlation_id),
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Execute via HandlerDb
|
|
247
|
+
db_result = await self._db_handler.execute(envelope)
|
|
248
|
+
|
|
249
|
+
# Check if RETURNING produced a row (insert succeeded) or not (duplicate)
|
|
250
|
+
# db_result.result is guaranteed non-None for successful db operations
|
|
251
|
+
if db_result.result is None:
|
|
252
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
253
|
+
correlation_id=correlation_id,
|
|
254
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
255
|
+
operation="ledger.append",
|
|
256
|
+
)
|
|
257
|
+
raise RuntimeHostError("Database operation returned no result", context=ctx)
|
|
258
|
+
|
|
259
|
+
rows = db_result.result.payload.rows
|
|
260
|
+
if rows and len(rows) > 0:
|
|
261
|
+
# Insert succeeded - extract ledger_entry_id from RETURNING
|
|
262
|
+
ledger_entry_id = UUID(str(rows[0]["ledger_entry_id"]))
|
|
263
|
+
duplicate = False
|
|
264
|
+
logger.debug(
|
|
265
|
+
"Event appended to ledger",
|
|
266
|
+
extra={
|
|
267
|
+
"ledger_entry_id": str(ledger_entry_id),
|
|
268
|
+
"topic": payload.topic,
|
|
269
|
+
"partition": payload.partition,
|
|
270
|
+
"offset": payload.kafka_offset,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
# ON CONFLICT DO NOTHING triggered - duplicate
|
|
275
|
+
ledger_entry_id = None
|
|
276
|
+
duplicate = True
|
|
277
|
+
logger.debug(
|
|
278
|
+
"Duplicate event detected (already in ledger)",
|
|
279
|
+
extra={
|
|
280
|
+
"topic": payload.topic,
|
|
281
|
+
"partition": payload.partition,
|
|
282
|
+
"offset": payload.kafka_offset,
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return ModelLedgerAppendResult(
|
|
287
|
+
success=True,
|
|
288
|
+
ledger_entry_id=ledger_entry_id,
|
|
289
|
+
duplicate=duplicate,
|
|
290
|
+
topic=payload.topic,
|
|
291
|
+
partition=payload.partition,
|
|
292
|
+
kafka_offset=payload.kafka_offset,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _decode_base64(self, encoded: str) -> bytes:
|
|
296
|
+
"""Decode base64 string to bytes.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
encoded: Base64-encoded string.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Decoded bytes.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
RuntimeHostError: If decoding fails.
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
return base64.b64decode(encoded)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
311
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
312
|
+
operation="ledger.append",
|
|
313
|
+
)
|
|
314
|
+
raise RuntimeHostError(
|
|
315
|
+
f"Failed to decode base64 event data: {type(e).__name__}",
|
|
316
|
+
context=ctx,
|
|
317
|
+
) from e
|
|
318
|
+
|
|
319
|
+
async def execute(
|
|
320
|
+
self,
|
|
321
|
+
envelope: dict[str, object],
|
|
322
|
+
) -> ModelHandlerOutput[ModelLedgerAppendResult]:
|
|
323
|
+
"""Execute ledger append from envelope (ProtocolHandler interface).
|
|
324
|
+
|
|
325
|
+
This method provides the standard handler interface for contract-driven
|
|
326
|
+
invocation. It extracts the payload from the envelope and delegates to
|
|
327
|
+
the append() method.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
envelope: Request envelope containing:
|
|
331
|
+
- operation: "ledger.append"
|
|
332
|
+
- payload: ModelPayloadLedgerAppend as dict
|
|
333
|
+
- correlation_id: Optional correlation ID
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
ModelHandlerOutput wrapping ModelLedgerAppendResult.
|
|
337
|
+
"""
|
|
338
|
+
from omnibase_infra.nodes.reducers.models import ModelPayloadLedgerAppend
|
|
339
|
+
|
|
340
|
+
correlation_id_raw = envelope.get("correlation_id")
|
|
341
|
+
correlation_id = (
|
|
342
|
+
UUID(str(correlation_id_raw)) if correlation_id_raw else uuid4()
|
|
343
|
+
)
|
|
344
|
+
input_envelope_id = uuid4()
|
|
345
|
+
|
|
346
|
+
payload_raw = envelope.get("payload")
|
|
347
|
+
if not isinstance(payload_raw, dict):
|
|
348
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
349
|
+
correlation_id=correlation_id,
|
|
350
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
351
|
+
operation="ledger.append",
|
|
352
|
+
)
|
|
353
|
+
raise RuntimeHostError(
|
|
354
|
+
"Missing or invalid 'payload' in envelope",
|
|
355
|
+
context=ctx,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Parse payload into typed model
|
|
359
|
+
payload = ModelPayloadLedgerAppend.model_validate(payload_raw)
|
|
360
|
+
|
|
361
|
+
# Execute append
|
|
362
|
+
result = await self.append(payload)
|
|
363
|
+
|
|
364
|
+
return ModelHandlerOutput.for_compute(
|
|
365
|
+
input_envelope_id=input_envelope_id,
|
|
366
|
+
correlation_id=correlation_id,
|
|
367
|
+
handler_id=HANDLER_ID_LEDGER_APPEND,
|
|
368
|
+
result=result,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = ["HandlerLedgerAppend"]
|