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
omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Handler for PostgreSQL topic routing table updates.
|
|
4
|
+
|
|
5
|
+
This handler encapsulates PostgreSQL-specific persistence logic for the
|
|
6
|
+
NodeContractPersistenceEffect node, following the declarative node pattern where
|
|
7
|
+
handlers are extracted for testability and separation of concerns.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
HandlerPostgresTopicUpdate is responsible for:
|
|
11
|
+
- Normalizing topic suffixes (stripping environment prefixes)
|
|
12
|
+
- Executing upsert operations against the PostgreSQL topics table
|
|
13
|
+
- Managing JSONB array of contract_ids for topic-to-contract mapping
|
|
14
|
+
- Returning structured ModelBackendResult
|
|
15
|
+
|
|
16
|
+
Timing, error classification, and sanitization are delegated to
|
|
17
|
+
MixinPostgresOpExecutor to eliminate boilerplate drift across handlers.
|
|
18
|
+
|
|
19
|
+
Topic Normalization:
|
|
20
|
+
Topics in contracts may include environment placeholders (e.g., "{env}.topic.name")
|
|
21
|
+
or actual environment prefixes (e.g., "dev.topic.name"). Before storage, these
|
|
22
|
+
prefixes are stripped to store only the topic suffix. This enables:
|
|
23
|
+
- Environment-agnostic topic routing queries
|
|
24
|
+
- Consistent topic identity across environments
|
|
25
|
+
- Simplified topic discovery and management
|
|
26
|
+
|
|
27
|
+
Coroutine Safety:
|
|
28
|
+
This handler is stateless and coroutine-safe for concurrent calls
|
|
29
|
+
with different payload instances. Thread-safety depends on the
|
|
30
|
+
underlying asyncpg connection pool implementation.
|
|
31
|
+
|
|
32
|
+
SQL Security:
|
|
33
|
+
All SQL queries use parameterized queries with positional placeholders
|
|
34
|
+
($1, $2, etc.) to prevent SQL injection attacks. The asyncpg library
|
|
35
|
+
handles proper escaping and type conversion for all parameters.
|
|
36
|
+
|
|
37
|
+
Related:
|
|
38
|
+
- NodeContractPersistenceEffect: Parent effect node that coordinates handlers
|
|
39
|
+
- ModelPayloadUpdateTopic: Input payload model
|
|
40
|
+
- ModelBackendResult: Structured result model for backend operations
|
|
41
|
+
- MixinPostgresOpExecutor: Shared execution core for timing/error handling
|
|
42
|
+
- OMN-1845: Implementation ticket
|
|
43
|
+
- OMN-1857: Executor extraction ticket
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import logging
|
|
49
|
+
from typing import TYPE_CHECKING
|
|
50
|
+
from uuid import UUID
|
|
51
|
+
|
|
52
|
+
from omnibase_infra.enums import (
|
|
53
|
+
EnumHandlerType,
|
|
54
|
+
EnumHandlerTypeCategory,
|
|
55
|
+
EnumPostgresErrorCode,
|
|
56
|
+
)
|
|
57
|
+
from omnibase_infra.errors import ModelInfraErrorContext, RepositoryExecutionError
|
|
58
|
+
from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
|
|
59
|
+
from omnibase_infra.models.model_backend_result import ModelBackendResult
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
import asyncpg
|
|
63
|
+
|
|
64
|
+
from omnibase_infra.nodes.contract_registry_reducer.models import (
|
|
65
|
+
ModelPayloadUpdateTopic,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
# Known environment prefixes to strip from topic suffixes before storage.
|
|
71
|
+
# The placeholder prefix is checked first, then actual environment prefixes.
|
|
72
|
+
_ENVIRONMENT_PREFIXES: tuple[str, ...] = (
|
|
73
|
+
"{env}.", # Placeholder prefix (most common)
|
|
74
|
+
"dev.",
|
|
75
|
+
"prod.",
|
|
76
|
+
"staging.",
|
|
77
|
+
"local.",
|
|
78
|
+
"test.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# SQL statement for topic upsert with JSONB array merge.
|
|
82
|
+
# Uses ON CONFLICT to handle existing topic+direction combinations.
|
|
83
|
+
# The JSONB containment operator (?) checks if contract_id already exists in array.
|
|
84
|
+
# If not present, appends to array; otherwise keeps existing array unchanged.
|
|
85
|
+
SQL_UPSERT_TOPIC = """
|
|
86
|
+
INSERT INTO topics (topic_suffix, direction, contract_ids, first_seen_at, last_seen_at, is_active)
|
|
87
|
+
VALUES ($1, $2, jsonb_build_array($3), $4, $5, TRUE)
|
|
88
|
+
ON CONFLICT (topic_suffix, direction) DO UPDATE SET
|
|
89
|
+
contract_ids = CASE
|
|
90
|
+
WHEN NOT topics.contract_ids ? $3
|
|
91
|
+
THEN topics.contract_ids || to_jsonb($3::text)
|
|
92
|
+
ELSE topics.contract_ids
|
|
93
|
+
END,
|
|
94
|
+
last_seen_at = EXCLUDED.last_seen_at,
|
|
95
|
+
is_active = TRUE,
|
|
96
|
+
updated_at = NOW()
|
|
97
|
+
RETURNING topic_suffix, direction, contract_ids;
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def normalize_topic_for_storage(topic: str) -> str:
|
|
102
|
+
"""Strip environment placeholder/prefix from topic before storage.
|
|
103
|
+
|
|
104
|
+
Topics in contracts may include environment placeholders like "{env}." or
|
|
105
|
+
actual environment prefixes like "dev.", "prod.", etc. This function
|
|
106
|
+
normalizes topics by stripping these prefixes to store only the topic suffix.
|
|
107
|
+
|
|
108
|
+
The normalization enables environment-agnostic topic routing queries and
|
|
109
|
+
consistent topic identity across different deployment environments.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
topic: The topic string to normalize, potentially with an environment
|
|
113
|
+
prefix (e.g., "{env}.onex.evt.platform.contract-registered.v1" or
|
|
114
|
+
"dev.onex.evt.platform.contract-registered.v1").
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The normalized topic suffix without environment prefix
|
|
118
|
+
(e.g., "onex.evt.platform.contract-registered.v1").
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
>>> normalize_topic_for_storage("{env}.onex.evt.platform.contract-registered.v1")
|
|
122
|
+
'onex.evt.platform.contract-registered.v1'
|
|
123
|
+
|
|
124
|
+
>>> normalize_topic_for_storage("dev.onex.evt.platform.contract-registered.v1")
|
|
125
|
+
'onex.evt.platform.contract-registered.v1'
|
|
126
|
+
|
|
127
|
+
>>> normalize_topic_for_storage("onex.evt.platform.contract-registered.v1")
|
|
128
|
+
'onex.evt.platform.contract-registered.v1'
|
|
129
|
+
"""
|
|
130
|
+
for prefix in _ENVIRONMENT_PREFIXES:
|
|
131
|
+
if topic.startswith(prefix):
|
|
132
|
+
return topic[len(prefix) :]
|
|
133
|
+
return topic
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class HandlerPostgresTopicUpdate(MixinPostgresOpExecutor):
|
|
137
|
+
"""Handler for PostgreSQL topic routing table updates.
|
|
138
|
+
|
|
139
|
+
Encapsulates all PostgreSQL-specific persistence logic for topic
|
|
140
|
+
routing updates.
|
|
141
|
+
|
|
142
|
+
Timing, error classification, and sanitization are handled by the
|
|
143
|
+
MixinPostgresOpExecutor base class, reducing boilerplate and ensuring
|
|
144
|
+
consistent error handling across all PostgreSQL handlers.
|
|
145
|
+
|
|
146
|
+
Topic Contract Mapping:
|
|
147
|
+
The topics table stores a JSONB array of contract_ids that reference
|
|
148
|
+
each topic. This handler uses JSONB operations to safely add contracts
|
|
149
|
+
to the array without duplicates:
|
|
150
|
+
- If contract_id not in array: append to array
|
|
151
|
+
- If contract_id already in array: keep array unchanged
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
_pool: asyncpg connection pool for database operations.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> import asyncpg
|
|
158
|
+
>>> pool = await asyncpg.create_pool(dsn="...")
|
|
159
|
+
>>> handler = HandlerPostgresTopicUpdate(pool)
|
|
160
|
+
>>> result = await handler.handle(payload, correlation_id)
|
|
161
|
+
>>> result.success
|
|
162
|
+
True
|
|
163
|
+
|
|
164
|
+
See Also:
|
|
165
|
+
- NodeContractPersistenceEffect: Parent node that uses this handler
|
|
166
|
+
- ModelPayloadUpdateTopic: Input payload model
|
|
167
|
+
- normalize_topic_for_storage: Topic normalization function
|
|
168
|
+
- MixinPostgresOpExecutor: Shared execution mechanics
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
172
|
+
"""Initialize handler with asyncpg connection pool.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
pool: asyncpg connection pool for database operations.
|
|
176
|
+
The pool should be pre-configured and ready for use.
|
|
177
|
+
"""
|
|
178
|
+
self._pool = pool
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def handler_type(self) -> EnumHandlerType:
|
|
182
|
+
"""Architectural role of this handler."""
|
|
183
|
+
return EnumHandlerType.INFRA_HANDLER
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def handler_category(self) -> EnumHandlerTypeCategory:
|
|
187
|
+
"""Behavioral classification of this handler."""
|
|
188
|
+
return EnumHandlerTypeCategory.EFFECT
|
|
189
|
+
|
|
190
|
+
async def handle(
|
|
191
|
+
self,
|
|
192
|
+
payload: ModelPayloadUpdateTopic,
|
|
193
|
+
correlation_id: UUID,
|
|
194
|
+
) -> ModelBackendResult:
|
|
195
|
+
"""Execute PostgreSQL topic routing table update.
|
|
196
|
+
|
|
197
|
+
Performs the upsert operation against the topics table with:
|
|
198
|
+
- Topic suffix normalization (strip environment prefix)
|
|
199
|
+
- JSONB array merge for contract_ids
|
|
200
|
+
- Parameterized SQL for injection prevention
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
payload: Update topic payload containing topic_suffix, direction,
|
|
204
|
+
contract_id, and last_seen_at timestamp.
|
|
205
|
+
correlation_id: Request correlation ID for distributed tracing.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
ModelBackendResult with:
|
|
209
|
+
- success: True if upsert completed successfully
|
|
210
|
+
- error: Sanitized error message if failed
|
|
211
|
+
- error_code: Error code for programmatic handling
|
|
212
|
+
- duration_ms: Operation duration in milliseconds
|
|
213
|
+
- backend_id: Set to "postgres"
|
|
214
|
+
- correlation_id: Passed through for tracing
|
|
215
|
+
|
|
216
|
+
Note:
|
|
217
|
+
Topic suffixes are normalized before storage - environment
|
|
218
|
+
prefixes like "{env}." or "dev." are stripped.
|
|
219
|
+
"""
|
|
220
|
+
# Normalize topic suffix by stripping environment prefix
|
|
221
|
+
normalized_topic = normalize_topic_for_storage(payload.topic_suffix)
|
|
222
|
+
|
|
223
|
+
return await self._execute_postgres_op(
|
|
224
|
+
op_error_code=EnumPostgresErrorCode.TOPIC_UPDATE_ERROR,
|
|
225
|
+
correlation_id=correlation_id,
|
|
226
|
+
log_context={
|
|
227
|
+
"topic_suffix": normalized_topic,
|
|
228
|
+
"original_topic": payload.topic_suffix,
|
|
229
|
+
"direction": payload.direction,
|
|
230
|
+
"contract_id": payload.contract_id,
|
|
231
|
+
},
|
|
232
|
+
fn=lambda: self._execute_topic_update(
|
|
233
|
+
payload, normalized_topic, correlation_id
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
async def _execute_topic_update(
|
|
238
|
+
self,
|
|
239
|
+
payload: ModelPayloadUpdateTopic,
|
|
240
|
+
normalized_topic: str,
|
|
241
|
+
correlation_id: UUID,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Execute the topic upsert query.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
payload: Topic update payload.
|
|
247
|
+
normalized_topic: Environment-stripped topic suffix.
|
|
248
|
+
correlation_id: Correlation ID for logging.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
RepositoryExecutionError: If no result returned from upsert.
|
|
252
|
+
Any exception from asyncpg (handled by MixinPostgresOpExecutor).
|
|
253
|
+
"""
|
|
254
|
+
async with self._pool.acquire() as conn:
|
|
255
|
+
result = await conn.fetchrow(
|
|
256
|
+
SQL_UPSERT_TOPIC,
|
|
257
|
+
normalized_topic,
|
|
258
|
+
payload.direction,
|
|
259
|
+
payload.contract_id,
|
|
260
|
+
payload.last_seen_at, # first_seen_at (on insert)
|
|
261
|
+
payload.last_seen_at, # last_seen_at
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if result is None:
|
|
265
|
+
# RETURNING clause should always return a row on success
|
|
266
|
+
# If None, something unexpected happened
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Topic update returned no result",
|
|
269
|
+
extra={
|
|
270
|
+
"topic_suffix": normalized_topic,
|
|
271
|
+
"direction": payload.direction,
|
|
272
|
+
"correlation_id": str(correlation_id),
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
276
|
+
correlation_id=correlation_id,
|
|
277
|
+
transport_type="db",
|
|
278
|
+
operation="topic_update",
|
|
279
|
+
)
|
|
280
|
+
raise RepositoryExecutionError(
|
|
281
|
+
"postgres operation failed: no result returned",
|
|
282
|
+
context=context,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Log for observability
|
|
286
|
+
logger.info(
|
|
287
|
+
"Topic update completed",
|
|
288
|
+
extra={
|
|
289
|
+
"topic_suffix": normalized_topic,
|
|
290
|
+
"original_topic": payload.topic_suffix,
|
|
291
|
+
"direction": payload.direction,
|
|
292
|
+
"contract_id": payload.contract_id,
|
|
293
|
+
"correlation_id": str(correlation_id),
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__: list[str] = ["HandlerPostgresTopicUpdate", "normalize_topic_for_storage"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Models for NodeContractPersistenceEffect.
|
|
4
|
+
|
|
5
|
+
Related:
|
|
6
|
+
- OMN-1845: NodeContractPersistenceEffect implementation
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.models.model_persistence_result import (
|
|
12
|
+
ModelPersistenceResult,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["ModelPersistenceResult"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Persistence result model for NodeContractPersistenceEffect.
|
|
4
|
+
|
|
5
|
+
Related:
|
|
6
|
+
- OMN-1845: NodeContractPersistenceEffect implementation
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
from omnibase_infra.enums import EnumPostgresErrorCode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelPersistenceResult(BaseModel):
|
|
20
|
+
"""Result of a contract persistence operation.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
success: Whether the operation succeeded.
|
|
24
|
+
error: Error message if operation failed (sanitized).
|
|
25
|
+
error_code: Typed error code for programmatic handling. Uses
|
|
26
|
+
EnumPostgresErrorCode for strong typing and validation.
|
|
27
|
+
duration_ms: Operation duration in milliseconds.
|
|
28
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
29
|
+
rows_affected: Number of database rows affected.
|
|
30
|
+
timestamp: When the operation completed.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
|
|
34
|
+
|
|
35
|
+
success: bool = Field(..., description="Whether the operation succeeded.")
|
|
36
|
+
error: str | None = Field(default=None, description="Sanitized error message.")
|
|
37
|
+
error_code: EnumPostgresErrorCode | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
description="Typed error code for programmatic handling.",
|
|
40
|
+
)
|
|
41
|
+
duration_ms: float = Field(default=0.0, description="Operation duration in ms.")
|
|
42
|
+
correlation_id: UUID | None = Field(
|
|
43
|
+
default=None, description="Correlation ID for tracing."
|
|
44
|
+
)
|
|
45
|
+
rows_affected: int = Field(default=0, description="Database rows affected.")
|
|
46
|
+
timestamp: datetime = Field(
|
|
47
|
+
default_factory=lambda: datetime.now(UTC),
|
|
48
|
+
description="Operation completion time.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["ModelPersistenceResult"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Node Contract Persistence Effect - Declarative effect node for contract registry persistence.
|
|
4
|
+
|
|
5
|
+
This node follows the ONEX declarative pattern:
|
|
6
|
+
- DECLARATIVE effect driven by contract.yaml
|
|
7
|
+
- Zero custom routing logic - all behavior from handler_routing
|
|
8
|
+
- Lightweight shell that delegates to handlers via container resolution
|
|
9
|
+
- Used for ONEX-compliant runtime execution via RuntimeHostProcess
|
|
10
|
+
- Pattern: "Contract-driven, handlers wired externally"
|
|
11
|
+
|
|
12
|
+
Extends NodeEffect from omnibase_core for infrastructure I/O operations.
|
|
13
|
+
All handler routing is 100% driven by contract.yaml, not Python code.
|
|
14
|
+
|
|
15
|
+
Handler Routing Pattern:
|
|
16
|
+
1. Receive intent from ContractRegistryReducer (ModelIntent with typed payload)
|
|
17
|
+
2. Route to appropriate handler based on payload.intent_type (handler_routing)
|
|
18
|
+
3. Execute PostgreSQL I/O via handler
|
|
19
|
+
4. Return structured response (output_model in contract)
|
|
20
|
+
|
|
21
|
+
Design Decisions:
|
|
22
|
+
- 100% Contract-Driven: All routing logic in YAML, not Python
|
|
23
|
+
- Zero Custom Routing: Base class handles handler dispatch via contract
|
|
24
|
+
- Declarative Handlers: handler_routing section defines dispatch rules
|
|
25
|
+
- Container DI: Backend adapters resolved via container, not setter methods
|
|
26
|
+
|
|
27
|
+
Supported Intent Types (from ContractRegistryReducer):
|
|
28
|
+
- postgres.upsert_contract: Insert/update contract record
|
|
29
|
+
- postgres.update_topic: Update topic routing table
|
|
30
|
+
- postgres.mark_stale: Batch mark contracts as stale
|
|
31
|
+
- postgres.update_heartbeat: Update last_seen_at timestamp
|
|
32
|
+
- postgres.deactivate_contract: Mark contract as inactive (soft delete)
|
|
33
|
+
- postgres.cleanup_topic_references: Remove contract from topic arrays
|
|
34
|
+
|
|
35
|
+
Node Responsibilities:
|
|
36
|
+
- Route intents to appropriate PostgreSQL handlers
|
|
37
|
+
- Delegate all execution to handlers via base class
|
|
38
|
+
- NO custom logic - pure declarative shell
|
|
39
|
+
|
|
40
|
+
Related Modules:
|
|
41
|
+
- contract.yaml: Handler routing and I/O model definitions
|
|
42
|
+
- handlers/: PostgreSQL operation handlers
|
|
43
|
+
- contract_registry_reducer/: Source of intents
|
|
44
|
+
- models/model_payload_*.py: Intent payload types
|
|
45
|
+
|
|
46
|
+
Related Tickets:
|
|
47
|
+
- OMN-1845: NodeContractPersistenceEffect implementation
|
|
48
|
+
- OMN-1653: ContractRegistryReducer (source of intents)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from typing import TYPE_CHECKING
|
|
54
|
+
|
|
55
|
+
from omnibase_core.nodes.node_effect import NodeEffect
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from omnibase_core.models.container.model_onex_container import ModelONEXContainer
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NodeContractPersistenceEffect(NodeEffect):
|
|
62
|
+
"""Declarative effect node for contract registry persistence.
|
|
63
|
+
|
|
64
|
+
This effect node is a lightweight shell that routes intents from
|
|
65
|
+
ContractRegistryReducer to PostgreSQL handlers. All routing and
|
|
66
|
+
execution logic is driven by contract.yaml - this class contains
|
|
67
|
+
NO custom routing code.
|
|
68
|
+
|
|
69
|
+
Supported Intent Types:
|
|
70
|
+
- postgres.upsert_contract: Upsert contract record
|
|
71
|
+
- postgres.update_topic: Update topic routing table
|
|
72
|
+
- postgres.mark_stale: Batch mark stale contracts
|
|
73
|
+
- postgres.update_heartbeat: Update heartbeat timestamp
|
|
74
|
+
- postgres.deactivate_contract: Soft delete contract
|
|
75
|
+
- postgres.cleanup_topic_references: Remove contract from topics
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
container: ONEX dependency injection container.
|
|
79
|
+
|
|
80
|
+
Dependency Injection:
|
|
81
|
+
Backend adapters (PostgreSQL) are resolved via container.
|
|
82
|
+
Handlers receive their dependencies directly via constructor injection.
|
|
83
|
+
This node contains NO instance variables for backend clients.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
```python
|
|
87
|
+
from omnibase_core.models.container import ModelONEXContainer
|
|
88
|
+
from omnibase_infra.nodes.node_contract_persistence_effect import (
|
|
89
|
+
NodeContractPersistenceEffect,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Create effect node via container
|
|
93
|
+
container = ModelONEXContainer()
|
|
94
|
+
effect = NodeContractPersistenceEffect(container)
|
|
95
|
+
|
|
96
|
+
# Handlers receive dependencies directly via constructor
|
|
97
|
+
postgres_adapter = container.resolve(ProtocolPostgresAdapter)
|
|
98
|
+
upsert_handler = HandlerPostgresContractUpsert(postgres_adapter)
|
|
99
|
+
|
|
100
|
+
# Execute handler with intent payload
|
|
101
|
+
result = await upsert_handler.handle(intent_payload)
|
|
102
|
+
```
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
106
|
+
"""Initialize effect node with container dependency injection.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
container: ONEX dependency injection container.
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(container)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["NodeContractPersistenceEffect"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Registry package for NodeContractPersistenceEffect.
|
|
4
|
+
|
|
5
|
+
This package provides infrastructure registry components for the
|
|
6
|
+
NodeContractPersistenceEffect node, following ONEX naming conventions.
|
|
7
|
+
|
|
8
|
+
Exports:
|
|
9
|
+
RegistryInfraContractPersistenceEffect: Factory and metadata registry for
|
|
10
|
+
creating NodeContractPersistenceEffect instances with dependency injection.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
>>> from omnibase_infra.nodes.node_contract_persistence_effect.registry import (
|
|
14
|
+
... RegistryInfraContractPersistenceEffect,
|
|
15
|
+
... )
|
|
16
|
+
>>> effect = RegistryInfraContractPersistenceEffect.create(container)
|
|
17
|
+
|
|
18
|
+
.. versionadded:: 0.5.0
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.registry.registry_infra_contract_persistence_effect import (
|
|
24
|
+
RegistryInfraContractPersistenceEffect,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = ["RegistryInfraContractPersistenceEffect"]
|