omnibase_infra 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
- omnibase_infra/enums/enum_postgres_error_code.py +188 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
- omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
- omnibase_infra/models/__init__.py +3 -0
- omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
- omnibase_infra/models/projection/__init__.py +11 -0
- omnibase_infra/models/projection/model_contract_projection.py +170 -0
- omnibase_infra/models/projection/model_topic_projection.py +148 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/effects/__init__.py +1 -1
- omnibase_infra/nodes/effects/models/__init__.py +6 -4
- omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
- omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
- omnibase_infra/nodes/effects/registry_effect.py +1 -1
- omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
- omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +5 -0
- omnibase_infra/runtime/contract_registration_event_router.py +500 -0
- omnibase_infra/runtime/db/__init__.py +4 -0
- omnibase_infra/runtime/db/models/__init__.py +15 -10
- omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
- omnibase_infra/runtime/db/models/model_db_param.py +24 -0
- omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
- omnibase_infra/runtime/db/models/model_db_return.py +26 -0
- omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
- omnibase_infra/runtime/intent_execution_router.py +430 -0
- omnibase_infra/runtime/models/__init__.py +6 -0
- omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
- omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
- omnibase_infra/runtime/models/model_runtime_config.py +8 -0
- omnibase_infra/runtime/protocols/__init__.py +16 -0
- omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
- omnibase_infra/runtime/request_response_wiring.py +785 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- omnibase_infra/services/registry_api/models/__init__.py +25 -0
- omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
- omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
- omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
- omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
- omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
- omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
- omnibase_infra/services/registry_api/routes.py +205 -6
- omnibase_infra/services/registry_api/service.py +528 -1
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +54 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -243,6 +243,9 @@ from omnibase_infra.runtime.event_bus_subcontract_wiring import (
|
|
|
243
243
|
load_event_bus_subcontract,
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
+
# Request-response wiring (OMN-1742)
|
|
247
|
+
from omnibase_infra.runtime.request_response_wiring import RequestResponseWiring
|
|
248
|
+
|
|
246
249
|
# Runtime contract config loader (OMN-1519)
|
|
247
250
|
from omnibase_infra.runtime.runtime_contract_config_loader import (
|
|
248
251
|
RuntimeContractConfigLoader,
|
|
@@ -429,6 +432,8 @@ __all__: list[str] = [
|
|
|
429
432
|
# Event bus subcontract wiring (OMN-1621)
|
|
430
433
|
"EventBusSubcontractWiring",
|
|
431
434
|
"load_event_bus_subcontract",
|
|
435
|
+
# Request-response wiring (OMN-1742)
|
|
436
|
+
"RequestResponseWiring",
|
|
432
437
|
# Runtime contract config loader (OMN-1519)
|
|
433
438
|
"RuntimeContractConfigLoader",
|
|
434
439
|
# Security constants and configuration (OMN-1519)
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Contract registration event router for kernel event processing.
|
|
4
|
+
|
|
5
|
+
This module provides an event router for routing contract registration events
|
|
6
|
+
in the ONEX kernel. Follows the same pattern as IntrospectionEventRouter.
|
|
7
|
+
|
|
8
|
+
The router:
|
|
9
|
+
- Subscribes to contract registration Kafka topics
|
|
10
|
+
- Parses incoming event messages based on topic suffix
|
|
11
|
+
- Routes to ContractRegistryReducer.reduce() for state transitions
|
|
12
|
+
- Emits intents for Effect layer execution (PostgreSQL operations)
|
|
13
|
+
|
|
14
|
+
Topics Handled:
|
|
15
|
+
- {env}.onex.evt.platform.contract-registered.v1 -> ModelContractRegisteredEvent
|
|
16
|
+
- {env}.onex.evt.platform.contract-deregistered.v1 -> ModelContractDeregisteredEvent
|
|
17
|
+
- {env}.onex.evt.platform.node-heartbeat.v1 -> ModelNodeHeartbeatEvent
|
|
18
|
+
|
|
19
|
+
Design:
|
|
20
|
+
This class encapsulates the message routing logic for contract registration
|
|
21
|
+
events. The router uses topic suffix matching to determine the event type
|
|
22
|
+
and routes to the appropriate reducer handler.
|
|
23
|
+
|
|
24
|
+
The reducer returns ModelReducerOutput containing:
|
|
25
|
+
- New state (ModelContractRegistryState)
|
|
26
|
+
- Intents tuple for Effect layer execution
|
|
27
|
+
|
|
28
|
+
Intents are NOT published back to Kafka by the router. They are returned
|
|
29
|
+
for the caller (typically the kernel) to dispatch to the Effect layer.
|
|
30
|
+
|
|
31
|
+
Related:
|
|
32
|
+
- OMN-1869: Contract Registration Event Router
|
|
33
|
+
- OMN-1653: Contract Registry Reducer
|
|
34
|
+
- IntrospectionEventRouter: Reference implementation pattern
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
__all__ = ["ContractRegistrationEventRouter"]
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import logging
|
|
43
|
+
import time
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
from uuid import UUID, uuid4
|
|
46
|
+
|
|
47
|
+
from pydantic import ValidationError
|
|
48
|
+
|
|
49
|
+
from omnibase_core.container import ModelONEXContainer
|
|
50
|
+
from omnibase_core.models.events import (
|
|
51
|
+
ModelContractDeregisteredEvent,
|
|
52
|
+
ModelContractRegisteredEvent,
|
|
53
|
+
ModelNodeHeartbeatEvent,
|
|
54
|
+
)
|
|
55
|
+
from omnibase_core.nodes import ModelReducerOutput
|
|
56
|
+
from omnibase_core.types import JsonType
|
|
57
|
+
from omnibase_infra.event_bus.models.model_event_message import ModelEventMessage
|
|
58
|
+
from omnibase_infra.nodes.contract_registry_reducer.models.model_contract_registry_state import (
|
|
59
|
+
ModelContractRegistryState,
|
|
60
|
+
)
|
|
61
|
+
from omnibase_infra.nodes.contract_registry_reducer.reducer import (
|
|
62
|
+
ContractRegistryReducer,
|
|
63
|
+
)
|
|
64
|
+
from omnibase_infra.utils import sanitize_error_message
|
|
65
|
+
|
|
66
|
+
if TYPE_CHECKING:
|
|
67
|
+
from omnibase_infra.protocols import ProtocolEventBusLike
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
# Topic suffix patterns for event type matching
|
|
72
|
+
# These match the topic suffix after the environment prefix
|
|
73
|
+
TOPIC_SUFFIX_CONTRACT_REGISTERED = "onex.evt.platform.contract-registered.v1"
|
|
74
|
+
TOPIC_SUFFIX_CONTRACT_DEREGISTERED = "onex.evt.platform.contract-deregistered.v1"
|
|
75
|
+
TOPIC_SUFFIX_NODE_HEARTBEAT = "onex.evt.platform.node-heartbeat.v1"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ContractRegistrationEventRouter:
|
|
79
|
+
"""Router for contract registration event messages from event bus.
|
|
80
|
+
|
|
81
|
+
This router handles incoming event messages for the contract registration
|
|
82
|
+
domain. It parses events based on topic suffix and routes them to the
|
|
83
|
+
ContractRegistryReducer for state machine processing.
|
|
84
|
+
|
|
85
|
+
The router propagates correlation IDs from incoming messages for
|
|
86
|
+
distributed tracing. If no correlation ID is present, it generates
|
|
87
|
+
a new one to ensure all operations can be traced.
|
|
88
|
+
|
|
89
|
+
This class follows the container-based dependency injection pattern,
|
|
90
|
+
receiving a ModelONEXContainer for service resolution while also
|
|
91
|
+
accepting explicit dependencies for router-specific configuration.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
_container: ONEX service container for dependency resolution.
|
|
95
|
+
_reducer: The ContractRegistryReducer to route events to.
|
|
96
|
+
_event_bus: Event bus implementing ProtocolEventBusLike for publishing.
|
|
97
|
+
_output_topic: The topic to publish intent completion events to.
|
|
98
|
+
_state: Current contract registry state (maintained across messages).
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
102
|
+
>>> container = ModelONEXContainer()
|
|
103
|
+
>>> reducer = ContractRegistryReducer()
|
|
104
|
+
>>> router = ContractRegistrationEventRouter(
|
|
105
|
+
... container=container,
|
|
106
|
+
... reducer=reducer,
|
|
107
|
+
... event_bus=event_bus,
|
|
108
|
+
... output_topic="contract.intents.output",
|
|
109
|
+
... )
|
|
110
|
+
>>> # Use as callback for event bus subscription
|
|
111
|
+
>>> await event_bus.subscribe(
|
|
112
|
+
... topic="dev.onex.evt.platform.contract-registered.v1",
|
|
113
|
+
... group_id="contract-registry",
|
|
114
|
+
... on_message=router.handle_message,
|
|
115
|
+
... )
|
|
116
|
+
|
|
117
|
+
See Also:
|
|
118
|
+
- docs/patterns/container_dependency_injection.md for DI patterns.
|
|
119
|
+
- IntrospectionEventRouter for reference implementation.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
container: ModelONEXContainer,
|
|
125
|
+
reducer: ContractRegistryReducer,
|
|
126
|
+
event_bus: ProtocolEventBusLike,
|
|
127
|
+
output_topic: str,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Initialize ContractRegistrationEventRouter with container-based DI.
|
|
130
|
+
|
|
131
|
+
Follows the ONEX container-based DI pattern where the container is passed
|
|
132
|
+
as the first parameter for service resolution, with additional explicit
|
|
133
|
+
parameters for router-specific configuration.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
container: ONEX service container for dependency resolution. Provides
|
|
137
|
+
access to service_registry for resolving shared services.
|
|
138
|
+
reducer: The ContractRegistryReducer to route events to.
|
|
139
|
+
event_bus: Event bus implementing ProtocolEventBusLike for publishing.
|
|
140
|
+
output_topic: The topic to publish intent completion events to.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If output_topic is empty.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
147
|
+
>>> container = ModelONEXContainer()
|
|
148
|
+
>>> reducer = ContractRegistryReducer()
|
|
149
|
+
>>> router = ContractRegistrationEventRouter(
|
|
150
|
+
... container=container,
|
|
151
|
+
... reducer=reducer,
|
|
152
|
+
... event_bus=event_bus,
|
|
153
|
+
... output_topic="contract.intents.output",
|
|
154
|
+
... )
|
|
155
|
+
|
|
156
|
+
See Also:
|
|
157
|
+
- docs/patterns/container_dependency_injection.md for DI patterns.
|
|
158
|
+
"""
|
|
159
|
+
if not output_topic:
|
|
160
|
+
raise ValueError("output_topic cannot be empty")
|
|
161
|
+
|
|
162
|
+
self._container = container
|
|
163
|
+
self._reducer = reducer
|
|
164
|
+
self._event_bus = event_bus
|
|
165
|
+
self._output_topic = output_topic
|
|
166
|
+
# Initialize empty state - maintained across message processing
|
|
167
|
+
self._state = ModelContractRegistryState()
|
|
168
|
+
|
|
169
|
+
logger.debug(
|
|
170
|
+
"ContractRegistrationEventRouter initialized",
|
|
171
|
+
extra={
|
|
172
|
+
"output_topic": output_topic,
|
|
173
|
+
"reducer_type": type(self._reducer).__name__,
|
|
174
|
+
"event_bus_type": type(self._event_bus).__name__,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def container(self) -> ModelONEXContainer:
|
|
180
|
+
"""Return the ONEX service container.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The ModelONEXContainer instance passed during initialization.
|
|
184
|
+
"""
|
|
185
|
+
return self._container
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def output_topic(self) -> str:
|
|
189
|
+
"""Return the configured output topic for event publishing."""
|
|
190
|
+
return self._output_topic
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def reducer(self) -> ContractRegistryReducer:
|
|
194
|
+
"""Return the reducer instance."""
|
|
195
|
+
return self._reducer
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def event_bus(self) -> ProtocolEventBusLike:
|
|
199
|
+
"""Return the event bus instance."""
|
|
200
|
+
return self._event_bus
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def state(self) -> ModelContractRegistryState:
|
|
204
|
+
"""Return the current contract registry state."""
|
|
205
|
+
return self._state
|
|
206
|
+
|
|
207
|
+
def extract_correlation_id_from_message(self, msg: ModelEventMessage) -> UUID:
|
|
208
|
+
"""Extract correlation ID from message headers or payload.
|
|
209
|
+
|
|
210
|
+
Attempts to extract the correlation_id from message headers or payload
|
|
211
|
+
to ensure proper propagation for distributed tracing. Falls back to
|
|
212
|
+
generating a new UUID if no correlation ID is found.
|
|
213
|
+
|
|
214
|
+
This is a public method that may be called by external components
|
|
215
|
+
(e.g., ServiceKernel) to extract correlation IDs for intent execution.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
msg: The incoming event message.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
UUID: The extracted or generated correlation ID.
|
|
222
|
+
"""
|
|
223
|
+
# Try to extract from message headers if available
|
|
224
|
+
if hasattr(msg, "headers") and msg.headers is not None:
|
|
225
|
+
headers = msg.headers
|
|
226
|
+
if (
|
|
227
|
+
hasattr(headers, "correlation_id")
|
|
228
|
+
and headers.correlation_id is not None
|
|
229
|
+
):
|
|
230
|
+
try:
|
|
231
|
+
correlation_id = headers.correlation_id
|
|
232
|
+
# Handle bytes-like values (duck typing)
|
|
233
|
+
if hasattr(correlation_id, "decode"):
|
|
234
|
+
correlation_id = correlation_id.decode("utf-8")
|
|
235
|
+
return UUID(str(correlation_id))
|
|
236
|
+
except (ValueError, TypeError, UnicodeDecodeError, AttributeError):
|
|
237
|
+
pass # Fall through to try payload extraction
|
|
238
|
+
|
|
239
|
+
# Try to extract from payload
|
|
240
|
+
try:
|
|
241
|
+
if msg.value is not None:
|
|
242
|
+
if hasattr(msg.value, "decode"):
|
|
243
|
+
payload_dict = json.loads(msg.value.decode("utf-8"))
|
|
244
|
+
else:
|
|
245
|
+
try:
|
|
246
|
+
payload_dict = json.loads(msg.value)
|
|
247
|
+
except TypeError:
|
|
248
|
+
payload_dict = msg.value
|
|
249
|
+
|
|
250
|
+
if payload_dict and "correlation_id" in payload_dict:
|
|
251
|
+
return UUID(str(payload_dict["correlation_id"]))
|
|
252
|
+
except (json.JSONDecodeError, ValueError, TypeError, KeyError, AttributeError):
|
|
253
|
+
pass # Fall through to generate new ID
|
|
254
|
+
|
|
255
|
+
# Generate new correlation ID as last resort
|
|
256
|
+
return uuid4()
|
|
257
|
+
|
|
258
|
+
def _determine_event_type(
|
|
259
|
+
self, topic: str
|
|
260
|
+
) -> (
|
|
261
|
+
type[
|
|
262
|
+
ModelContractRegisteredEvent
|
|
263
|
+
| ModelContractDeregisteredEvent
|
|
264
|
+
| ModelNodeHeartbeatEvent
|
|
265
|
+
]
|
|
266
|
+
| None
|
|
267
|
+
):
|
|
268
|
+
"""Determine event type based on topic suffix.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
topic: The Kafka topic name.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The event model class to use for parsing, or None if topic not recognized.
|
|
275
|
+
"""
|
|
276
|
+
if topic.endswith(TOPIC_SUFFIX_CONTRACT_REGISTERED):
|
|
277
|
+
return ModelContractRegisteredEvent
|
|
278
|
+
elif topic.endswith(TOPIC_SUFFIX_CONTRACT_DEREGISTERED):
|
|
279
|
+
return ModelContractDeregisteredEvent
|
|
280
|
+
elif topic.endswith(TOPIC_SUFFIX_NODE_HEARTBEAT):
|
|
281
|
+
return ModelNodeHeartbeatEvent
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
async def handle_message(
|
|
285
|
+
self, msg: ModelEventMessage
|
|
286
|
+
) -> ModelReducerOutput[ModelContractRegistryState] | None:
|
|
287
|
+
"""Handle incoming contract registration event message.
|
|
288
|
+
|
|
289
|
+
This callback is invoked for each message received on the subscribed topics.
|
|
290
|
+
It parses the raw JSON payload based on topic suffix, routes to the
|
|
291
|
+
ContractRegistryReducer, and updates internal state.
|
|
292
|
+
|
|
293
|
+
The method propagates the correlation_id from the incoming message
|
|
294
|
+
for distributed tracing. If no correlation_id is present in the message,
|
|
295
|
+
a new one is generated.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
msg: The event message containing raw bytes in .value field.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
ModelReducerOutput containing new state and intents, or None on error.
|
|
302
|
+
The intents should be dispatched to the Effect layer by the caller.
|
|
303
|
+
"""
|
|
304
|
+
# Extract correlation_id from message for proper propagation
|
|
305
|
+
callback_correlation_id = self.extract_correlation_id_from_message(msg)
|
|
306
|
+
callback_start_time = time.time()
|
|
307
|
+
|
|
308
|
+
# Extract topic from message
|
|
309
|
+
topic = getattr(msg, "topic", "") or ""
|
|
310
|
+
partition = getattr(msg, "partition", 0) or 0
|
|
311
|
+
offset = getattr(msg, "offset", 0) or 0
|
|
312
|
+
|
|
313
|
+
logger.debug(
|
|
314
|
+
"Contract registration message callback invoked (correlation_id=%s)",
|
|
315
|
+
callback_correlation_id,
|
|
316
|
+
extra={
|
|
317
|
+
"message_offset": offset,
|
|
318
|
+
"message_partition": partition,
|
|
319
|
+
"message_topic": topic,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
# Determine event type from topic
|
|
325
|
+
event_class = self._determine_event_type(topic)
|
|
326
|
+
if event_class is None:
|
|
327
|
+
logger.debug(
|
|
328
|
+
"Topic not recognized as contract registration event, skipping "
|
|
329
|
+
"(correlation_id=%s)",
|
|
330
|
+
callback_correlation_id,
|
|
331
|
+
extra={"topic": topic},
|
|
332
|
+
)
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
# Parse message value
|
|
336
|
+
if msg.value is None:
|
|
337
|
+
logger.debug(
|
|
338
|
+
"Message value is None, skipping (correlation_id=%s)",
|
|
339
|
+
callback_correlation_id,
|
|
340
|
+
)
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
# Parse message value using duck-typing patterns
|
|
344
|
+
if hasattr(msg.value, "decode"):
|
|
345
|
+
logger.debug(
|
|
346
|
+
"Parsing message value as bytes-like (correlation_id=%s)",
|
|
347
|
+
callback_correlation_id,
|
|
348
|
+
extra={"value_length": len(msg.value)},
|
|
349
|
+
)
|
|
350
|
+
payload_dict = json.loads(msg.value.decode("utf-8"))
|
|
351
|
+
else:
|
|
352
|
+
try:
|
|
353
|
+
logger.debug(
|
|
354
|
+
"Parsing message value as string-like (correlation_id=%s)",
|
|
355
|
+
callback_correlation_id,
|
|
356
|
+
extra={
|
|
357
|
+
"value_length": len(msg.value)
|
|
358
|
+
if hasattr(msg.value, "__len__")
|
|
359
|
+
else None
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
payload_dict = json.loads(msg.value)
|
|
363
|
+
except TypeError:
|
|
364
|
+
if hasattr(msg.value, "keys"):
|
|
365
|
+
logger.debug(
|
|
366
|
+
"Message value already dict-like (correlation_id=%s)",
|
|
367
|
+
callback_correlation_id,
|
|
368
|
+
)
|
|
369
|
+
payload_dict = msg.value
|
|
370
|
+
else:
|
|
371
|
+
logger.debug(
|
|
372
|
+
"Unexpected message value type: %s (correlation_id=%s)",
|
|
373
|
+
type(msg.value).__name__,
|
|
374
|
+
callback_correlation_id,
|
|
375
|
+
)
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
# Parse as the appropriate event model
|
|
379
|
+
logger.debug(
|
|
380
|
+
"Validating payload as %s (correlation_id=%s)",
|
|
381
|
+
event_class.__name__,
|
|
382
|
+
callback_correlation_id,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
event = event_class.model_validate(payload_dict)
|
|
386
|
+
|
|
387
|
+
logger.info(
|
|
388
|
+
"Event parsed successfully (correlation_id=%s)",
|
|
389
|
+
callback_correlation_id,
|
|
390
|
+
extra={
|
|
391
|
+
"event_type": type(event).__name__,
|
|
392
|
+
"event_id": str(event.event_id),
|
|
393
|
+
"node_name": event.node_name,
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Build event metadata for reducer
|
|
398
|
+
event_metadata: dict[str, JsonType] = {
|
|
399
|
+
"topic": topic,
|
|
400
|
+
"partition": partition,
|
|
401
|
+
"offset": offset,
|
|
402
|
+
"correlation_id": str(callback_correlation_id),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Route to reducer
|
|
406
|
+
logger.info(
|
|
407
|
+
"Routing to contract registry reducer (correlation_id=%s)",
|
|
408
|
+
callback_correlation_id,
|
|
409
|
+
extra={
|
|
410
|
+
"event_type": type(event).__name__,
|
|
411
|
+
"node_name": event.node_name,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
reducer_start_time = time.time()
|
|
415
|
+
result = self._reducer.reduce(self._state, event, event_metadata)
|
|
416
|
+
reducer_duration = time.time() - reducer_start_time
|
|
417
|
+
|
|
418
|
+
# Update internal state
|
|
419
|
+
self._state = result.result
|
|
420
|
+
|
|
421
|
+
logger.info(
|
|
422
|
+
"Contract registration event processed successfully: node_name=%s "
|
|
423
|
+
"in %.3fs (correlation_id=%s)",
|
|
424
|
+
event.node_name,
|
|
425
|
+
reducer_duration,
|
|
426
|
+
callback_correlation_id,
|
|
427
|
+
extra={
|
|
428
|
+
"reducer_duration_seconds": reducer_duration,
|
|
429
|
+
"event_type": type(event).__name__,
|
|
430
|
+
"node_name": event.node_name,
|
|
431
|
+
"intents_count": len(result.intents),
|
|
432
|
+
"items_processed": result.items_processed,
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Log intent summary for debugging
|
|
437
|
+
if result.intents:
|
|
438
|
+
logger.debug(
|
|
439
|
+
"Reducer emitted %d intents (correlation_id=%s)",
|
|
440
|
+
len(result.intents),
|
|
441
|
+
callback_correlation_id,
|
|
442
|
+
extra={
|
|
443
|
+
"intent_targets": [intent.target for intent in result.intents],
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
except ValidationError as validation_error:
|
|
450
|
+
# Not a valid event for this topic - skip
|
|
451
|
+
logger.debug(
|
|
452
|
+
"Message is not a valid contract registration event, skipping "
|
|
453
|
+
"(correlation_id=%s)",
|
|
454
|
+
callback_correlation_id,
|
|
455
|
+
extra={
|
|
456
|
+
"validation_error_count": validation_error.error_count(),
|
|
457
|
+
"topic": topic,
|
|
458
|
+
},
|
|
459
|
+
)
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
except json.JSONDecodeError as json_error:
|
|
463
|
+
logger.warning(
|
|
464
|
+
"Failed to decode JSON from message: %s (correlation_id=%s)",
|
|
465
|
+
sanitize_error_message(json_error),
|
|
466
|
+
callback_correlation_id,
|
|
467
|
+
extra={
|
|
468
|
+
"error_type": type(json_error).__name__,
|
|
469
|
+
"error_position": getattr(json_error, "pos", None),
|
|
470
|
+
"topic": topic,
|
|
471
|
+
},
|
|
472
|
+
)
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
except Exception as msg_error:
|
|
476
|
+
# Use warning instead of exception to avoid credential exposure
|
|
477
|
+
# in tracebacks (connection errors may contain DSN with password)
|
|
478
|
+
logger.warning(
|
|
479
|
+
"Failed to process contract registration message: %s (correlation_id=%s)",
|
|
480
|
+
sanitize_error_message(msg_error),
|
|
481
|
+
callback_correlation_id,
|
|
482
|
+
extra={
|
|
483
|
+
"error_type": type(msg_error).__name__,
|
|
484
|
+
"topic": topic,
|
|
485
|
+
},
|
|
486
|
+
)
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
finally:
|
|
490
|
+
callback_duration = time.time() - callback_start_time
|
|
491
|
+
logger.debug(
|
|
492
|
+
"Contract registration message callback completed in %.3fs "
|
|
493
|
+
"(correlation_id=%s)",
|
|
494
|
+
callback_duration,
|
|
495
|
+
callback_correlation_id,
|
|
496
|
+
extra={
|
|
497
|
+
"callback_duration_seconds": callback_duration,
|
|
498
|
+
"topic": topic,
|
|
499
|
+
},
|
|
500
|
+
)
|
|
@@ -56,8 +56,10 @@ from __future__ import annotations
|
|
|
56
56
|
|
|
57
57
|
from omnibase_infra.runtime.db.models import (
|
|
58
58
|
ModelDbOperation,
|
|
59
|
+
ModelDbParam,
|
|
59
60
|
ModelDbRepositoryContract,
|
|
60
61
|
ModelDbReturn,
|
|
62
|
+
ModelDbSafetyPolicy,
|
|
61
63
|
ModelRepositoryRuntimeConfig,
|
|
62
64
|
)
|
|
63
65
|
from omnibase_infra.runtime.db.postgres_repository_runtime import (
|
|
@@ -66,8 +68,10 @@ from omnibase_infra.runtime.db.postgres_repository_runtime import (
|
|
|
66
68
|
|
|
67
69
|
__all__: list[str] = [
|
|
68
70
|
"ModelDbOperation",
|
|
71
|
+
"ModelDbParam",
|
|
69
72
|
"ModelDbRepositoryContract",
|
|
70
73
|
"ModelDbReturn",
|
|
74
|
+
"ModelDbSafetyPolicy",
|
|
71
75
|
"ModelRepositoryRuntimeConfig",
|
|
72
76
|
"PostgresRepositoryRuntime",
|
|
73
77
|
]
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
# Copyright (c) 2025 OmniNode Team
|
|
3
3
|
"""Database Runtime Models Module.
|
|
4
4
|
|
|
5
|
-
This module exports Pydantic models for database runtime configuration
|
|
6
|
-
|
|
7
|
-
are imported from omnibase_core.models.contracts.
|
|
5
|
+
This module exports Pydantic models for database runtime configuration
|
|
6
|
+
and repository contracts.
|
|
8
7
|
|
|
9
8
|
Exports:
|
|
10
9
|
ModelRepositoryRuntimeConfig: Configuration for PostgresRepositoryRuntime
|
|
@@ -14,19 +13,23 @@ Exports:
|
|
|
14
13
|
- Determinism controls (primary_key_column, default_order_by)
|
|
15
14
|
- Metrics emission configuration
|
|
16
15
|
|
|
17
|
-
ModelDbRepositoryContract:
|
|
18
|
-
ModelDbOperation:
|
|
19
|
-
|
|
16
|
+
ModelDbRepositoryContract: Repository contract definition
|
|
17
|
+
ModelDbOperation: Individual database operation definition
|
|
18
|
+
ModelDbParam: Parameter definition for operations
|
|
19
|
+
ModelDbReturn: Return type specification
|
|
20
|
+
ModelDbSafetyPolicy: Safety constraints for operations
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
from __future__ import annotations
|
|
23
24
|
|
|
24
|
-
# Contract models
|
|
25
|
-
from
|
|
26
|
-
|
|
25
|
+
# Contract models (local to omnibase_infra since 0.3.2)
|
|
26
|
+
from omnibase_infra.runtime.db.models.model_db_operation import ModelDbOperation
|
|
27
|
+
from omnibase_infra.runtime.db.models.model_db_param import ModelDbParam
|
|
28
|
+
from omnibase_infra.runtime.db.models.model_db_repository_contract import (
|
|
27
29
|
ModelDbRepositoryContract,
|
|
28
|
-
ModelDbReturn,
|
|
29
30
|
)
|
|
31
|
+
from omnibase_infra.runtime.db.models.model_db_return import ModelDbReturn
|
|
32
|
+
from omnibase_infra.runtime.db.models.model_db_safety_policy import ModelDbSafetyPolicy
|
|
30
33
|
|
|
31
34
|
# Runtime config is local to omnibase_infra
|
|
32
35
|
from omnibase_infra.runtime.db.models.model_repository_runtime_config import (
|
|
@@ -35,7 +38,9 @@ from omnibase_infra.runtime.db.models.model_repository_runtime_config import (
|
|
|
35
38
|
|
|
36
39
|
__all__: list[str] = [
|
|
37
40
|
"ModelDbOperation",
|
|
41
|
+
"ModelDbParam",
|
|
38
42
|
"ModelDbRepositoryContract",
|
|
39
43
|
"ModelDbReturn",
|
|
44
|
+
"ModelDbSafetyPolicy",
|
|
40
45
|
"ModelRepositoryRuntimeConfig",
|
|
41
46
|
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Database operation model for SQL operations."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from omnibase_infra.runtime.db.models.model_db_param import ModelDbParam
|
|
10
|
+
from omnibase_infra.runtime.db.models.model_db_return import ModelDbReturn
|
|
11
|
+
from omnibase_infra.runtime.db.models.model_db_safety_policy import ModelDbSafetyPolicy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelDbOperation(BaseModel):
|
|
15
|
+
"""Individual database operation definition.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
mode: Operation mode ("read", "write", "delete")
|
|
19
|
+
sql: SQL query template with positional parameters ($1, $2, ...)
|
|
20
|
+
params: Parameter definitions (name -> ModelDbParam)
|
|
21
|
+
returns: Return type specification
|
|
22
|
+
safety_policy: Safety constraints for this operation
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
26
|
+
|
|
27
|
+
mode: str = Field(..., description="Operation mode (read/write/delete)")
|
|
28
|
+
sql: str = Field(..., description="SQL query template")
|
|
29
|
+
params: dict[str, ModelDbParam] = Field(
|
|
30
|
+
default_factory=dict, description="Parameter definitions"
|
|
31
|
+
)
|
|
32
|
+
returns: ModelDbReturn = Field(
|
|
33
|
+
default_factory=ModelDbReturn, description="Return type specification"
|
|
34
|
+
)
|
|
35
|
+
safety_policy: ModelDbSafetyPolicy = Field(
|
|
36
|
+
default_factory=ModelDbSafetyPolicy, description="Safety constraints"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["ModelDbOperation"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Database parameter model for SQL operations."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelDbParam(BaseModel):
|
|
11
|
+
"""Parameter definition for a database operation.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
name: Parameter name (used for documentation/error messages)
|
|
15
|
+
param_type: Parameter type (e.g., "integer", "string", "uuid")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
19
|
+
|
|
20
|
+
name: str = Field(..., description="Parameter name")
|
|
21
|
+
param_type: str = Field(..., description="Parameter type")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["ModelDbParam"]
|