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,26 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Testing utilities for ONEX event bus.
|
|
4
|
+
|
|
5
|
+
This module provides test adapters and helpers for event bus testing:
|
|
6
|
+
- AdapterProtocolEventPublisherInmemory: Test adapter implementing ProtocolEventPublisher
|
|
7
|
+
- decode_inmemory_event: Helper function to decode event bus messages
|
|
8
|
+
|
|
9
|
+
These utilities enable consistent test patterns without per-handler adapter duplication.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from omnibase_infra.event_bus.testing.adapter_protocol_event_publisher_inmemory import (
|
|
15
|
+
AdapterProtocolEventPublisherInmemory,
|
|
16
|
+
decode_inmemory_event,
|
|
17
|
+
)
|
|
18
|
+
from omnibase_infra.event_bus.testing.model_publisher_metrics import (
|
|
19
|
+
ModelPublisherMetrics,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__: list[str] = [
|
|
23
|
+
"AdapterProtocolEventPublisherInmemory",
|
|
24
|
+
"ModelPublisherMetrics",
|
|
25
|
+
"decode_inmemory_event",
|
|
26
|
+
]
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Test adapter implementing ProtocolEventPublisher bridged to EventBusInmemory.
|
|
4
|
+
|
|
5
|
+
This adapter provides a production-equivalent envelope format for testing,
|
|
6
|
+
eliminating the need for per-handler test adapters and ensuring test/production
|
|
7
|
+
parity for event publishing.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
- Implements full ProtocolEventPublisher interface from omnibase_spi
|
|
11
|
+
- Serializes events using canonical ModelEventEnvelope format
|
|
12
|
+
- Preserves correlation_id, causation_id, and metadata in envelope
|
|
13
|
+
- Handles topic routing (default via event_type, or explicit override)
|
|
14
|
+
- Encodes partition_key to bytes using canonical UTF-8 encoding
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
```python
|
|
18
|
+
from omnibase_infra.event_bus import EventBusInmemory
|
|
19
|
+
from omnibase_infra.event_bus.testing import (
|
|
20
|
+
AdapterProtocolEventPublisherInmemory,
|
|
21
|
+
decode_inmemory_event,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
bus = EventBusInmemory(environment="test", group="test-group")
|
|
25
|
+
await bus.start()
|
|
26
|
+
|
|
27
|
+
adapter = AdapterProtocolEventPublisherInmemory(bus)
|
|
28
|
+
|
|
29
|
+
# Publish an event
|
|
30
|
+
success = await adapter.publish(
|
|
31
|
+
event_type="omninode.user.event.created.v1",
|
|
32
|
+
payload={"user_id": "usr-123", "email": "user@example.com"},
|
|
33
|
+
correlation_id="corr-456",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Retrieve and decode for assertions
|
|
37
|
+
history = await bus.get_event_history(limit=1)
|
|
38
|
+
envelope = decode_inmemory_event(history[0].value)
|
|
39
|
+
assert envelope.correlation_id is not None
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
References:
|
|
43
|
+
- ProtocolEventPublisher: omnibase_spi.protocols.event_bus.protocol_event_publisher
|
|
44
|
+
- ModelEventEnvelope: omnibase_core.models.events.model_event_envelope
|
|
45
|
+
- Parent ticket: OMN-1611
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
from datetime import UTC, datetime
|
|
53
|
+
from typing import TYPE_CHECKING, cast
|
|
54
|
+
from uuid import UUID, uuid4
|
|
55
|
+
|
|
56
|
+
from omnibase_core.models.core.model_envelope_metadata import ModelEnvelopeMetadata
|
|
57
|
+
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
58
|
+
from omnibase_core.types import JsonType
|
|
59
|
+
from omnibase_infra.event_bus.testing.model_publisher_metrics import (
|
|
60
|
+
ModelPublisherMetrics,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
65
|
+
from omnibase_infra.types.typed_dict import TypedDictEnvelopeBuildParams
|
|
66
|
+
from omnibase_spi.protocols.types.protocol_core_types import ContextValue
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AdapterProtocolEventPublisherInmemory:
|
|
72
|
+
"""Test adapter implementing ProtocolEventPublisher bridged to EventBusInmemory.
|
|
73
|
+
|
|
74
|
+
This adapter provides production-equivalent envelope serialization for testing,
|
|
75
|
+
ensuring that test events use the exact same format as production events.
|
|
76
|
+
It eliminates the need for per-handler test adapters.
|
|
77
|
+
|
|
78
|
+
Key Design Decisions:
|
|
79
|
+
- Uses ModelEventEnvelope for canonical envelope format
|
|
80
|
+
- Preserves all correlation tracking (correlation_id, causation_id)
|
|
81
|
+
- Stores causation_id in metadata.tags since ModelEventEnvelope doesn't have a
|
|
82
|
+
dedicated field for it
|
|
83
|
+
- Topic routing: explicit topic parameter takes precedence over event_type
|
|
84
|
+
- partition_key is encoded to UTF-8 bytes as per SPI specification
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
bus: The underlying EventBusInmemory instance.
|
|
88
|
+
service_name: Service identifier included in envelope metadata.
|
|
89
|
+
instance_id: Instance identifier for envelope source tracking.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
```python
|
|
93
|
+
bus = EventBusInmemory()
|
|
94
|
+
await bus.start()
|
|
95
|
+
|
|
96
|
+
adapter = AdapterProtocolEventPublisherInmemory(
|
|
97
|
+
bus=bus,
|
|
98
|
+
service_name="test-service",
|
|
99
|
+
instance_id="instance-001",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
success = await adapter.publish(
|
|
103
|
+
event_type="user.created",
|
|
104
|
+
payload={"id": "123"},
|
|
105
|
+
correlation_id="corr-abc",
|
|
106
|
+
)
|
|
107
|
+
assert success is True
|
|
108
|
+
```
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
bus: EventBusInmemory,
|
|
114
|
+
service_name: str = "test-service",
|
|
115
|
+
instance_id: str | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Initialize the adapter with an EventBusInmemory instance.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
bus: The EventBusInmemory instance to bridge to.
|
|
121
|
+
service_name: Service name for envelope metadata. Defaults to "test-service".
|
|
122
|
+
instance_id: Optional instance identifier. Defaults to a generated UUID.
|
|
123
|
+
"""
|
|
124
|
+
self._bus = bus
|
|
125
|
+
self._service_name = service_name
|
|
126
|
+
self._instance_id = instance_id or str(uuid4())
|
|
127
|
+
self._metrics = ModelPublisherMetrics()
|
|
128
|
+
self._closed = False
|
|
129
|
+
|
|
130
|
+
async def publish(
|
|
131
|
+
self,
|
|
132
|
+
event_type: str,
|
|
133
|
+
payload: JsonType,
|
|
134
|
+
correlation_id: str | None = None,
|
|
135
|
+
causation_id: str | None = None,
|
|
136
|
+
metadata: dict[str, ContextValue] | None = None,
|
|
137
|
+
topic: str | None = None,
|
|
138
|
+
partition_key: str | None = None,
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""Publish event with canonical ModelEventEnvelope serialization.
|
|
141
|
+
|
|
142
|
+
Builds a ModelEventEnvelope from the provided parameters, serializes to JSON,
|
|
143
|
+
and publishes to the underlying EventBusInmemory.
|
|
144
|
+
|
|
145
|
+
Topic Routing:
|
|
146
|
+
1. If `topic` is provided, use it directly (explicit override).
|
|
147
|
+
2. Otherwise, derive topic from `event_type` (default routing).
|
|
148
|
+
|
|
149
|
+
Correlation Tracking:
|
|
150
|
+
- correlation_id: Stored in envelope.correlation_id
|
|
151
|
+
- causation_id: Stored in envelope.metadata.tags["causation_id"]
|
|
152
|
+
- Both IDs are preserved through serialization for full traceability
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event_type: Fully-qualified event type (e.g., "omninode.user.event.created.v1").
|
|
156
|
+
payload: Event payload data (dict, list, or primitive JSON types).
|
|
157
|
+
correlation_id: Optional correlation ID for request tracing.
|
|
158
|
+
causation_id: Optional causation ID for event sourcing chains.
|
|
159
|
+
metadata: Optional additional metadata as context values.
|
|
160
|
+
topic: Optional explicit topic override. When None, uses event_type as topic.
|
|
161
|
+
partition_key: Optional partition key for message ordering.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if published successfully, False otherwise.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
RuntimeError: If adapter has been closed.
|
|
168
|
+
"""
|
|
169
|
+
if self._closed:
|
|
170
|
+
raise RuntimeError("Publisher has been closed")
|
|
171
|
+
|
|
172
|
+
start_time = datetime.now(UTC)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Build envelope - parameters passed as dict to comply with ONEX parameter limit
|
|
176
|
+
envelope = self._build_envelope(
|
|
177
|
+
{
|
|
178
|
+
"event_type": event_type,
|
|
179
|
+
"payload": payload,
|
|
180
|
+
"correlation_id": correlation_id,
|
|
181
|
+
"causation_id": causation_id,
|
|
182
|
+
"metadata": metadata,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Determine target topic
|
|
187
|
+
target_topic = topic if topic is not None else event_type
|
|
188
|
+
|
|
189
|
+
# Encode partition key to bytes (UTF-8 canonical encoding)
|
|
190
|
+
key_bytes: bytes | None = None
|
|
191
|
+
if partition_key is not None:
|
|
192
|
+
key_bytes = partition_key.encode("utf-8")
|
|
193
|
+
|
|
194
|
+
# Serialize envelope to JSON bytes
|
|
195
|
+
envelope_dict = envelope.model_dump(mode="json")
|
|
196
|
+
value_bytes = json.dumps(envelope_dict).encode("utf-8")
|
|
197
|
+
|
|
198
|
+
# Publish to underlying bus
|
|
199
|
+
await self._bus.publish(
|
|
200
|
+
topic=target_topic,
|
|
201
|
+
key=key_bytes,
|
|
202
|
+
value=value_bytes,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Update metrics
|
|
206
|
+
elapsed_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
207
|
+
self._metrics.events_published += 1
|
|
208
|
+
self._metrics.total_publish_time_ms += elapsed_ms
|
|
209
|
+
self._metrics.avg_publish_time_ms = (
|
|
210
|
+
self._metrics.total_publish_time_ms / self._metrics.events_published
|
|
211
|
+
)
|
|
212
|
+
self._metrics.current_failures = 0
|
|
213
|
+
|
|
214
|
+
logger.debug(
|
|
215
|
+
"Event published successfully",
|
|
216
|
+
extra={
|
|
217
|
+
"event_type": event_type,
|
|
218
|
+
"topic": target_topic,
|
|
219
|
+
"correlation_id": correlation_id,
|
|
220
|
+
"elapsed_ms": elapsed_ms,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
# NOTE: Intentionally broad exception catch for test adapter.
|
|
228
|
+
# Test adapters should gracefully handle all errors and return False
|
|
229
|
+
# rather than propagate exceptions that would crash test harnesses.
|
|
230
|
+
# This allows test assertions on publish failure (e.g., assert result is False).
|
|
231
|
+
# Update failure metrics
|
|
232
|
+
self._metrics.events_failed += 1
|
|
233
|
+
self._metrics.current_failures += 1
|
|
234
|
+
|
|
235
|
+
logger.exception(
|
|
236
|
+
"Failed to publish event",
|
|
237
|
+
extra={
|
|
238
|
+
"event_type": event_type,
|
|
239
|
+
"correlation_id": correlation_id,
|
|
240
|
+
"error": str(e),
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
def _build_envelope(
|
|
247
|
+
self,
|
|
248
|
+
params: TypedDictEnvelopeBuildParams,
|
|
249
|
+
) -> ModelEventEnvelope[JsonType]:
|
|
250
|
+
"""Build a ModelEventEnvelope from publish parameters.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
params: Dictionary containing:
|
|
254
|
+
- event_type: The event type identifier
|
|
255
|
+
- payload: The event payload
|
|
256
|
+
- correlation_id: Optional correlation ID
|
|
257
|
+
- causation_id: Optional causation ID
|
|
258
|
+
- metadata: Optional additional metadata
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Configured ModelEventEnvelope ready for serialization.
|
|
262
|
+
"""
|
|
263
|
+
event_type = str(params["event_type"])
|
|
264
|
+
payload = cast("JsonType", params["payload"])
|
|
265
|
+
correlation_id = params.get("correlation_id")
|
|
266
|
+
causation_id = params.get("causation_id")
|
|
267
|
+
metadata = params.get("metadata")
|
|
268
|
+
|
|
269
|
+
# Convert correlation_id string to UUID if provided
|
|
270
|
+
corr_uuid: UUID | None = None
|
|
271
|
+
if correlation_id is not None:
|
|
272
|
+
try:
|
|
273
|
+
corr_uuid = UUID(str(correlation_id))
|
|
274
|
+
except ValueError:
|
|
275
|
+
# If not a valid UUID, generate one and log the original for debugging
|
|
276
|
+
corr_uuid = uuid4()
|
|
277
|
+
logger.warning(
|
|
278
|
+
"correlation_id is not a valid UUID, generating new UUID (original logged)",
|
|
279
|
+
extra={
|
|
280
|
+
"original_correlation_id": correlation_id,
|
|
281
|
+
"generated_uuid": str(corr_uuid),
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Build metadata tags
|
|
286
|
+
tags: dict[str, str] = {
|
|
287
|
+
"event_type": event_type,
|
|
288
|
+
"service_name": self._service_name,
|
|
289
|
+
"instance_id": self._instance_id,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Store causation_id in tags (ModelEventEnvelope doesn't have dedicated field)
|
|
293
|
+
if causation_id is not None:
|
|
294
|
+
tags["causation_id"] = str(causation_id)
|
|
295
|
+
|
|
296
|
+
# Merge additional metadata context values into tags
|
|
297
|
+
if metadata is not None and isinstance(metadata, dict):
|
|
298
|
+
for key, value in metadata.items():
|
|
299
|
+
# Context values may have a serialize_for_context method or be simple types
|
|
300
|
+
if hasattr(value, "serialize_for_context"):
|
|
301
|
+
serialized = value.serialize_for_context()
|
|
302
|
+
tags[key] = json.dumps(serialized)
|
|
303
|
+
elif hasattr(value, "value"):
|
|
304
|
+
# ProtocolContext*Value types have a value attribute
|
|
305
|
+
tags[key] = str(value.value)
|
|
306
|
+
else:
|
|
307
|
+
tags[key] = str(value)
|
|
308
|
+
|
|
309
|
+
envelope_metadata = ModelEnvelopeMetadata(tags=tags)
|
|
310
|
+
|
|
311
|
+
# Build the envelope
|
|
312
|
+
envelope: ModelEventEnvelope[JsonType] = ModelEventEnvelope(
|
|
313
|
+
payload=payload,
|
|
314
|
+
correlation_id=corr_uuid,
|
|
315
|
+
source_tool=f"{self._service_name}.{self._instance_id}",
|
|
316
|
+
metadata=envelope_metadata,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return envelope
|
|
320
|
+
|
|
321
|
+
async def get_metrics(self) -> JsonType:
|
|
322
|
+
"""Get publisher metrics.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Dictionary with metrics including:
|
|
326
|
+
- events_published: Total successful publishes
|
|
327
|
+
- events_failed: Total failed publishes
|
|
328
|
+
- events_sent_to_dlq: Always 0 for inmemory (no DLQ)
|
|
329
|
+
- total_publish_time_ms: Cumulative publish time
|
|
330
|
+
- avg_publish_time_ms: Average publish latency
|
|
331
|
+
- circuit_breaker_opens: Always 0 for inmemory
|
|
332
|
+
- retries_attempted: Always 0 for inmemory
|
|
333
|
+
- circuit_breaker_status: Always "closed" for inmemory
|
|
334
|
+
- current_failures: Current consecutive failure count
|
|
335
|
+
"""
|
|
336
|
+
return self._metrics.to_dict()
|
|
337
|
+
|
|
338
|
+
def reset_metrics(self) -> None:
|
|
339
|
+
"""Reset all publisher metrics to initial values.
|
|
340
|
+
|
|
341
|
+
Useful for test isolation when reusing an adapter across multiple
|
|
342
|
+
test cases without recreating the adapter instance.
|
|
343
|
+
|
|
344
|
+
Note:
|
|
345
|
+
This method does NOT affect the closed state of the adapter.
|
|
346
|
+
If the adapter has been closed, it remains closed after reset.
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
```python
|
|
350
|
+
adapter = AdapterProtocolEventPublisherInmemory(bus)
|
|
351
|
+
await adapter.publish(...) # metrics.events_published = 1
|
|
352
|
+
|
|
353
|
+
adapter.reset_metrics() # metrics.events_published = 0
|
|
354
|
+
await adapter.publish(...) # metrics.events_published = 1
|
|
355
|
+
```
|
|
356
|
+
"""
|
|
357
|
+
self._metrics = ModelPublisherMetrics()
|
|
358
|
+
logger.debug(
|
|
359
|
+
"Publisher metrics reset",
|
|
360
|
+
extra={
|
|
361
|
+
"service_name": self._service_name,
|
|
362
|
+
"instance_id": self._instance_id,
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def close(self, timeout_seconds: float = 30.0) -> None:
|
|
367
|
+
"""Close the publisher.
|
|
368
|
+
|
|
369
|
+
For the inmemory adapter, this simply marks the adapter as closed.
|
|
370
|
+
No actual cleanup is needed since EventBusInmemory handles its own lifecycle.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
timeout_seconds: Timeout for cleanup (unused for inmemory).
|
|
374
|
+
"""
|
|
375
|
+
self._closed = True
|
|
376
|
+
logger.info(
|
|
377
|
+
"AdapterProtocolEventPublisherInmemory closed",
|
|
378
|
+
extra={
|
|
379
|
+
"service_name": self._service_name,
|
|
380
|
+
"instance_id": self._instance_id,
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def decode_inmemory_event(value: bytes) -> ModelEventEnvelope[object]:
|
|
386
|
+
"""Decode an inmemory event bus message value to ModelEventEnvelope.
|
|
387
|
+
|
|
388
|
+
This helper function decodes the JSON bytes from EventBusInmemory message
|
|
389
|
+
values back into a ModelEventEnvelope for test assertions.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
value: The bytes value from ModelEventMessage.value
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Decoded ModelEventEnvelope with payload typed as object.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
json.JSONDecodeError: If value is not valid JSON.
|
|
399
|
+
pydantic.ValidationError: If decoded JSON doesn't match envelope schema.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
```python
|
|
403
|
+
history = await bus.get_event_history(limit=1)
|
|
404
|
+
envelope = decode_inmemory_event(history[0].value)
|
|
405
|
+
|
|
406
|
+
assert envelope.correlation_id is not None
|
|
407
|
+
assert envelope.metadata.tags.get("event_type") == "user.created"
|
|
408
|
+
assert envelope.payload["user_id"] == "usr-123"
|
|
409
|
+
```
|
|
410
|
+
"""
|
|
411
|
+
decoded_dict = json.loads(value.decode("utf-8"))
|
|
412
|
+
return ModelEventEnvelope[object].model_validate(decoded_dict)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
__all__: list[str] = [
|
|
416
|
+
"AdapterProtocolEventPublisherInmemory",
|
|
417
|
+
"decode_inmemory_event",
|
|
418
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Publisher metrics model for test adapters.
|
|
4
|
+
|
|
5
|
+
This module provides the metrics model used by AdapterProtocolEventPublisherInmemory
|
|
6
|
+
to track publishing statistics for observability and testing assertions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from omnibase_core.types import JsonType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelPublisherMetrics(BaseModel):
|
|
17
|
+
"""Metrics model for AdapterProtocolEventPublisherInmemory.
|
|
18
|
+
|
|
19
|
+
Tracks publishing statistics for observability and testing assertions.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
events_published: Total count of successfully published events.
|
|
23
|
+
events_failed: Total count of failed publish attempts.
|
|
24
|
+
events_sent_to_dlq: Count of events sent to dead letter queue (always 0 for inmemory).
|
|
25
|
+
total_publish_time_ms: Cumulative publish time in milliseconds.
|
|
26
|
+
avg_publish_time_ms: Average publish latency (computed from total/count).
|
|
27
|
+
circuit_breaker_opens: Count of circuit breaker open events (always 0 for inmemory).
|
|
28
|
+
retries_attempted: Total retry attempts (always 0 for inmemory).
|
|
29
|
+
circuit_breaker_status: Current circuit breaker status (always "closed" for inmemory).
|
|
30
|
+
current_failures: Current consecutive failure count.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
events_published: int = Field(default=0, ge=0)
|
|
34
|
+
events_failed: int = Field(default=0, ge=0)
|
|
35
|
+
events_sent_to_dlq: int = Field(default=0, ge=0)
|
|
36
|
+
total_publish_time_ms: float = Field(default=0.0, ge=0.0)
|
|
37
|
+
avg_publish_time_ms: float = Field(default=0.0, ge=0.0)
|
|
38
|
+
circuit_breaker_opens: int = Field(default=0, ge=0)
|
|
39
|
+
retries_attempted: int = Field(default=0, ge=0)
|
|
40
|
+
circuit_breaker_status: str = Field(default="closed")
|
|
41
|
+
current_failures: int = Field(default=0, ge=0)
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(frozen=False, extra="forbid")
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, JsonType]:
|
|
46
|
+
"""Convert metrics to dictionary for JSON serialization.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary representation compatible with JsonType.
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"events_published": self.events_published,
|
|
53
|
+
"events_failed": self.events_failed,
|
|
54
|
+
"events_sent_to_dlq": self.events_sent_to_dlq,
|
|
55
|
+
"total_publish_time_ms": self.total_publish_time_ms,
|
|
56
|
+
"avg_publish_time_ms": self.avg_publish_time_ms,
|
|
57
|
+
"circuit_breaker_opens": self.circuit_breaker_opens,
|
|
58
|
+
"retries_attempted": self.retries_attempted,
|
|
59
|
+
"circuit_breaker_status": self.circuit_breaker_status,
|
|
60
|
+
"current_failures": self.current_failures,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__: list[str] = ["ModelPublisherMetrics"]
|
|
@@ -52,6 +52,7 @@ from omnibase_infra.handlers.mixins import (
|
|
|
52
52
|
MixinConsulInitialization,
|
|
53
53
|
MixinConsulKV,
|
|
54
54
|
MixinConsulService,
|
|
55
|
+
MixinConsulTopicIndex,
|
|
55
56
|
)
|
|
56
57
|
from omnibase_infra.handlers.models import (
|
|
57
58
|
ModelOperationContext,
|
|
@@ -111,6 +112,7 @@ class HandlerConsul(
|
|
|
111
112
|
MixinEnvelopeExtraction,
|
|
112
113
|
MixinConsulInitialization,
|
|
113
114
|
MixinConsulKV,
|
|
115
|
+
MixinConsulTopicIndex, # Must come before MixinConsulService (MRO order)
|
|
114
116
|
MixinConsulService,
|
|
115
117
|
):
|
|
116
118
|
"""HashiCorp Consul handler using python-consul client (MVP: KV, service registration).
|
|
@@ -9,6 +9,7 @@ Consul Mixins:
|
|
|
9
9
|
- MixinConsulInitialization: Configuration parsing and client setup
|
|
10
10
|
- MixinConsulKV: Key-value store operations (get, put)
|
|
11
11
|
- MixinConsulService: Service registration operations (register, deregister)
|
|
12
|
+
- MixinConsulTopicIndex: Topic index management for event bus routing
|
|
12
13
|
|
|
13
14
|
Vault Mixins:
|
|
14
15
|
- MixinVaultInitialization: Configuration parsing and client setup
|
|
@@ -22,6 +23,9 @@ from omnibase_infra.handlers.mixins.mixin_consul_initialization import (
|
|
|
22
23
|
)
|
|
23
24
|
from omnibase_infra.handlers.mixins.mixin_consul_kv import MixinConsulKV
|
|
24
25
|
from omnibase_infra.handlers.mixins.mixin_consul_service import MixinConsulService
|
|
26
|
+
from omnibase_infra.handlers.mixins.mixin_consul_topic_index import (
|
|
27
|
+
MixinConsulTopicIndex,
|
|
28
|
+
)
|
|
25
29
|
from omnibase_infra.handlers.mixins.mixin_vault_initialization import (
|
|
26
30
|
MixinVaultInitialization,
|
|
27
31
|
)
|
|
@@ -34,6 +38,7 @@ __all__: list[str] = [
|
|
|
34
38
|
"MixinConsulInitialization",
|
|
35
39
|
"MixinConsulKV",
|
|
36
40
|
"MixinConsulService",
|
|
41
|
+
"MixinConsulTopicIndex",
|
|
37
42
|
# Vault mixins
|
|
38
43
|
"MixinVaultInitialization",
|
|
39
44
|
"MixinVaultRetry",
|