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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Handlers for NodeContractPersistenceEffect operations.
|
|
4
|
+
|
|
5
|
+
This package contains the handlers for the NodeContractPersistenceEffect node,
|
|
6
|
+
following the declarative node pattern where PostgreSQL operations are
|
|
7
|
+
encapsulated in dedicated handler classes.
|
|
8
|
+
|
|
9
|
+
Available Handlers:
|
|
10
|
+
HandlerPostgresContractUpsert: Upsert contract record handler.
|
|
11
|
+
HandlerPostgresTopicUpdate: Update topic routing handler.
|
|
12
|
+
HandlerPostgresMarkStale: Batch mark stale contracts handler.
|
|
13
|
+
HandlerPostgresHeartbeat: Update heartbeat timestamp handler.
|
|
14
|
+
HandlerPostgresDeactivate: Deactivate contract handler.
|
|
15
|
+
HandlerPostgresCleanupTopics: Cleanup topic references handler.
|
|
16
|
+
|
|
17
|
+
Architecture:
|
|
18
|
+
These handlers are used by NodeContractPersistenceEffect to execute
|
|
19
|
+
PostgreSQL operations based on intents from ContractRegistryReducer.
|
|
20
|
+
Each handler is responsible for:
|
|
21
|
+
- Operation timing and observability
|
|
22
|
+
- Error sanitization for security
|
|
23
|
+
- Structured result construction
|
|
24
|
+
- Retry logic per retry_policy configuration
|
|
25
|
+
|
|
26
|
+
Shared Patterns:
|
|
27
|
+
All handlers share a common error handling pattern:
|
|
28
|
+
- TimeoutError/InfraTimeoutError: Returns *_TIMEOUT_ERROR code
|
|
29
|
+
- InfraAuthenticationError: Returns *_AUTH_ERROR code (non-retriable)
|
|
30
|
+
- InfraConnectionError: Returns *_CONNECTION_ERROR code (retriable)
|
|
31
|
+
- RepositoryExecutionError: Returns operation-specific error code
|
|
32
|
+
- Exception: Returns *_UNKNOWN_ERROR code
|
|
33
|
+
|
|
34
|
+
Each handler sanitizes errors via sanitize_error_message() to prevent
|
|
35
|
+
credential exposure in logs and error responses.
|
|
36
|
+
|
|
37
|
+
Related:
|
|
38
|
+
- NodeContractPersistenceEffect: Parent effect node coordinating handlers
|
|
39
|
+
- ContractRegistryReducer: Source of intents
|
|
40
|
+
- OMN-1845: Implementation ticket
|
|
41
|
+
- OMN-1653: ContractRegistryReducer ticket
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_cleanup_topics import (
|
|
47
|
+
HandlerPostgresCleanupTopics,
|
|
48
|
+
)
|
|
49
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_contract_upsert import (
|
|
50
|
+
HandlerPostgresContractUpsert,
|
|
51
|
+
)
|
|
52
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_deactivate import (
|
|
53
|
+
HandlerPostgresDeactivate,
|
|
54
|
+
)
|
|
55
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_heartbeat import (
|
|
56
|
+
HandlerPostgresHeartbeat,
|
|
57
|
+
)
|
|
58
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_mark_stale import (
|
|
59
|
+
HandlerPostgresMarkStale,
|
|
60
|
+
)
|
|
61
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers.handler_postgres_topic_update import (
|
|
62
|
+
HandlerPostgresTopicUpdate,
|
|
63
|
+
normalize_topic_for_storage,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__: list[str] = [
|
|
67
|
+
"HandlerPostgresCleanupTopics",
|
|
68
|
+
"HandlerPostgresContractUpsert",
|
|
69
|
+
"HandlerPostgresDeactivate",
|
|
70
|
+
"HandlerPostgresHeartbeat",
|
|
71
|
+
"HandlerPostgresMarkStale",
|
|
72
|
+
"HandlerPostgresTopicUpdate",
|
|
73
|
+
"normalize_topic_for_storage",
|
|
74
|
+
]
|
omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Handler for PostgreSQL topic reference cleanup.
|
|
4
|
+
|
|
5
|
+
This handler encapsulates PostgreSQL-specific topic cleanup 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
|
+
HandlerPostgresCleanupTopics is responsible for:
|
|
11
|
+
- Removing contract_id from topics.contract_ids JSONB arrays
|
|
12
|
+
- Returning structured ModelBackendResult
|
|
13
|
+
|
|
14
|
+
Timing, error classification, and sanitization are delegated to
|
|
15
|
+
MixinPostgresOpExecutor to eliminate boilerplate drift across handlers.
|
|
16
|
+
|
|
17
|
+
This handler removes a contract_id from the contract_ids JSONB array in
|
|
18
|
+
all topics that reference it. Topic rows are NOT deleted even when the
|
|
19
|
+
contract_ids array becomes empty.
|
|
20
|
+
|
|
21
|
+
Topic Orphan Handling (OMN-1709):
|
|
22
|
+
When all contracts are removed from a topic, the topic row remains with
|
|
23
|
+
empty contract_ids array. This is intentional:
|
|
24
|
+
- Preserves topic routing history for auditing and debugging
|
|
25
|
+
- Allows topic reactivation if a new contract references the same topic
|
|
26
|
+
- Avoids complex cascading deletes during high-volume deregistration
|
|
27
|
+
|
|
28
|
+
To clean up orphaned topics, run:
|
|
29
|
+
DELETE FROM topics WHERE contract_ids = '[]' AND is_active = FALSE;
|
|
30
|
+
|
|
31
|
+
Coroutine Safety:
|
|
32
|
+
This handler is stateless and coroutine-safe for concurrent calls
|
|
33
|
+
with different request instances. Thread-safety depends on the
|
|
34
|
+
underlying asyncpg.Pool implementation.
|
|
35
|
+
|
|
36
|
+
Related:
|
|
37
|
+
- NodeContractPersistenceEffect: Parent effect node that coordinates handlers
|
|
38
|
+
- ModelPayloadCleanupTopicReferences: Input payload model
|
|
39
|
+
- ModelBackendResult: Structured result model for backend operations
|
|
40
|
+
- MixinPostgresOpExecutor: Shared execution core for timing/error handling
|
|
41
|
+
- OMN-1845: Implementation ticket
|
|
42
|
+
- OMN-1857: Executor extraction ticket
|
|
43
|
+
- OMN-1709: Topic orphan handling documentation
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import logging
|
|
49
|
+
from typing import TYPE_CHECKING
|
|
50
|
+
from uuid import UUID
|
|
51
|
+
|
|
52
|
+
import asyncpg
|
|
53
|
+
|
|
54
|
+
from omnibase_infra.enums import (
|
|
55
|
+
EnumHandlerType,
|
|
56
|
+
EnumHandlerTypeCategory,
|
|
57
|
+
EnumPostgresErrorCode,
|
|
58
|
+
)
|
|
59
|
+
from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
|
|
60
|
+
from omnibase_infra.models.model_backend_result import ModelBackendResult
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from omnibase_infra.nodes.contract_registry_reducer.models import (
|
|
66
|
+
ModelPayloadCleanupTopicReferences,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# SQL for removing contract_id from all topic contract_ids arrays
|
|
70
|
+
# Uses parameterized query: $1 = contract_id (as text)
|
|
71
|
+
#
|
|
72
|
+
# JSONB operators:
|
|
73
|
+
# - `contract_ids ? $1` checks if string exists as element in JSONB array
|
|
74
|
+
# - `contract_ids - $1` removes the string element from JSONB array
|
|
75
|
+
#
|
|
76
|
+
# Note: updated_at is handled by the trigger (trigger_topics_updated_at)
|
|
77
|
+
# but we update it explicitly here for clarity and for cases where
|
|
78
|
+
# the trigger might not be installed.
|
|
79
|
+
SQL_CLEANUP_TOPIC_REFERENCES = """
|
|
80
|
+
UPDATE topics
|
|
81
|
+
SET contract_ids = contract_ids - $1,
|
|
82
|
+
updated_at = NOW()
|
|
83
|
+
WHERE contract_ids ? $1
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HandlerPostgresCleanupTopics(MixinPostgresOpExecutor):
|
|
88
|
+
"""Handler for removing contract references from topics.
|
|
89
|
+
|
|
90
|
+
Encapsulates all PostgreSQL-specific topic cleanup logic extracted from
|
|
91
|
+
NodeContractPersistenceEffect for declarative node compliance.
|
|
92
|
+
|
|
93
|
+
Timing, error classification, and sanitization are handled by the
|
|
94
|
+
MixinPostgresOpExecutor base class, reducing boilerplate and ensuring
|
|
95
|
+
consistent error handling across all PostgreSQL handlers.
|
|
96
|
+
|
|
97
|
+
Important: Topic rows are NOT deleted even when contract_ids becomes empty.
|
|
98
|
+
This is intentional per OMN-1709 - orphaned topics are preserved for
|
|
99
|
+
auditing and can be cleaned up separately if needed.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
_pool: asyncpg connection pool for database operations.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> import asyncpg
|
|
106
|
+
>>> pool = await asyncpg.create_pool(dsn="postgresql://...")
|
|
107
|
+
>>> handler = HandlerPostgresCleanupTopics(pool)
|
|
108
|
+
>>> result = await handler.handle(payload, correlation_id)
|
|
109
|
+
>>> result.success
|
|
110
|
+
True
|
|
111
|
+
|
|
112
|
+
See Also:
|
|
113
|
+
- NodeContractPersistenceEffect: Parent node that uses this handler
|
|
114
|
+
- ModelPayloadCleanupTopicReferences: Input payload model
|
|
115
|
+
- MixinPostgresOpExecutor: Shared execution mechanics
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
119
|
+
"""Initialize handler with asyncpg connection pool.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
pool: asyncpg connection pool for executing database operations.
|
|
123
|
+
"""
|
|
124
|
+
self._pool = pool
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def handler_type(self) -> EnumHandlerType:
|
|
128
|
+
"""Architectural role of this handler."""
|
|
129
|
+
return EnumHandlerType.INFRA_HANDLER
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def handler_category(self) -> EnumHandlerTypeCategory:
|
|
133
|
+
"""Behavioral classification of this handler."""
|
|
134
|
+
return EnumHandlerTypeCategory.EFFECT
|
|
135
|
+
|
|
136
|
+
async def handle(
|
|
137
|
+
self,
|
|
138
|
+
payload: ModelPayloadCleanupTopicReferences,
|
|
139
|
+
correlation_id: UUID,
|
|
140
|
+
) -> ModelBackendResult:
|
|
141
|
+
"""Remove contract_id from all topic contract_ids arrays.
|
|
142
|
+
|
|
143
|
+
The cleanup removes the contract_id from all topics.contract_ids
|
|
144
|
+
JSONB arrays. Topic rows are preserved even if contract_ids becomes
|
|
145
|
+
empty (per OMN-1709 topic orphan handling).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
payload: Cleanup payload containing contract_id to remove and
|
|
149
|
+
cleaned_at timestamp.
|
|
150
|
+
correlation_id: Request correlation ID for distributed tracing.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ModelBackendResult with:
|
|
154
|
+
- success: True if cleanup completed successfully
|
|
155
|
+
- error: Sanitized error message if failed
|
|
156
|
+
- error_code: Error code for programmatic handling
|
|
157
|
+
- duration_ms: Operation duration in milliseconds
|
|
158
|
+
- backend_id: Set to "postgres"
|
|
159
|
+
- correlation_id: Passed through for tracing
|
|
160
|
+
|
|
161
|
+
Note:
|
|
162
|
+
If no topics contain the contract_id, success=True is still
|
|
163
|
+
returned. This follows the idempotency principle - cleaning up
|
|
164
|
+
references for a contract that has no topic associations is
|
|
165
|
+
not an error.
|
|
166
|
+
"""
|
|
167
|
+
return await self._execute_postgres_op(
|
|
168
|
+
op_error_code=EnumPostgresErrorCode.CLEANUP_ERROR,
|
|
169
|
+
correlation_id=correlation_id,
|
|
170
|
+
log_context={
|
|
171
|
+
"contract_id": payload.contract_id,
|
|
172
|
+
},
|
|
173
|
+
fn=lambda: self._execute_cleanup(payload, correlation_id),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def _execute_cleanup(
|
|
177
|
+
self,
|
|
178
|
+
payload: ModelPayloadCleanupTopicReferences,
|
|
179
|
+
correlation_id: UUID,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Execute the topic cleanup UPDATE query.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
payload: Cleanup payload with contract_id.
|
|
185
|
+
correlation_id: Correlation ID for logging.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
Any exception from asyncpg (handled by MixinPostgresOpExecutor).
|
|
189
|
+
"""
|
|
190
|
+
async with self._pool.acquire() as conn:
|
|
191
|
+
# Execute the update and get the number of affected rows
|
|
192
|
+
result = await conn.execute(
|
|
193
|
+
SQL_CLEANUP_TOPIC_REFERENCES,
|
|
194
|
+
payload.contract_id,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Parse the result to get affected row count
|
|
198
|
+
# asyncpg execute returns a string like "UPDATE N"
|
|
199
|
+
topics_updated = 0
|
|
200
|
+
if result and result.startswith("UPDATE "):
|
|
201
|
+
try:
|
|
202
|
+
topics_updated = int(result.split(" ")[1])
|
|
203
|
+
except (ValueError, IndexError):
|
|
204
|
+
pass # Keep default of 0 if parsing fails
|
|
205
|
+
|
|
206
|
+
# Log for observability
|
|
207
|
+
logger.info(
|
|
208
|
+
"Topic cleanup operation completed",
|
|
209
|
+
extra={
|
|
210
|
+
"correlation_id": str(correlation_id),
|
|
211
|
+
"contract_id": payload.contract_id,
|
|
212
|
+
"topics_updated": topics_updated,
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
__all__: list[str] = ["HandlerPostgresCleanupTopics"]
|
omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Handler for PostgreSQL contract record upsert.
|
|
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
|
+
HandlerPostgresContractUpsert is responsible for:
|
|
11
|
+
- Executing upsert operations against the PostgreSQL contracts table
|
|
12
|
+
- Serializing contract_yaml dict to YAML string before INSERT
|
|
13
|
+
- Returning structured ModelBackendResult
|
|
14
|
+
|
|
15
|
+
Timing, error classification, and sanitization are delegated to
|
|
16
|
+
MixinPostgresOpExecutor to eliminate boilerplate drift across handlers.
|
|
17
|
+
|
|
18
|
+
Coroutine Safety:
|
|
19
|
+
This handler is stateless and coroutine-safe for concurrent calls
|
|
20
|
+
with different payload instances. Thread-safety depends on the
|
|
21
|
+
underlying asyncpg connection pool implementation.
|
|
22
|
+
|
|
23
|
+
SQL Security:
|
|
24
|
+
All SQL queries use parameterized queries with positional placeholders
|
|
25
|
+
($1, $2, etc.) to prevent SQL injection attacks. The asyncpg library
|
|
26
|
+
handles proper escaping and type conversion for all parameters.
|
|
27
|
+
|
|
28
|
+
Related:
|
|
29
|
+
- NodeContractPersistenceEffect: Parent effect node that coordinates handlers
|
|
30
|
+
- ModelPayloadUpsertContract: Input payload model
|
|
31
|
+
- ModelBackendResult: Structured result model for backend operations
|
|
32
|
+
- MixinPostgresOpExecutor: Shared execution core for timing/error handling
|
|
33
|
+
- OMN-1845: Implementation ticket
|
|
34
|
+
- OMN-1857: Executor extraction ticket
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
from uuid import UUID
|
|
42
|
+
|
|
43
|
+
import yaml
|
|
44
|
+
|
|
45
|
+
from omnibase_infra.enums import (
|
|
46
|
+
EnumHandlerType,
|
|
47
|
+
EnumHandlerTypeCategory,
|
|
48
|
+
EnumPostgresErrorCode,
|
|
49
|
+
)
|
|
50
|
+
from omnibase_infra.errors import ModelInfraErrorContext, RepositoryExecutionError
|
|
51
|
+
from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
|
|
52
|
+
from omnibase_infra.models.model_backend_result import ModelBackendResult
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
import asyncpg
|
|
56
|
+
|
|
57
|
+
from omnibase_infra.nodes.contract_registry_reducer.models import (
|
|
58
|
+
ModelPayloadUpsertContract,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
# SQL statement for contract upsert with ON CONFLICT for idempotency.
|
|
64
|
+
# Uses RETURNING to confirm the operation was executed.
|
|
65
|
+
SQL_UPSERT_CONTRACT = """
|
|
66
|
+
INSERT INTO contracts (
|
|
67
|
+
contract_id, node_name, version_major, version_minor, version_patch,
|
|
68
|
+
contract_hash, contract_yaml, is_active, registered_at, last_seen_at
|
|
69
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
70
|
+
ON CONFLICT (contract_id) DO UPDATE SET
|
|
71
|
+
contract_hash = EXCLUDED.contract_hash,
|
|
72
|
+
contract_yaml = EXCLUDED.contract_yaml,
|
|
73
|
+
is_active = EXCLUDED.is_active,
|
|
74
|
+
last_seen_at = EXCLUDED.last_seen_at,
|
|
75
|
+
updated_at = NOW()
|
|
76
|
+
RETURNING contract_id, (xmax = 0) AS was_insert;
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class HandlerPostgresContractUpsert(MixinPostgresOpExecutor):
|
|
81
|
+
"""Handler for PostgreSQL contract record upsert.
|
|
82
|
+
|
|
83
|
+
Encapsulates all PostgreSQL-specific persistence logic for contract
|
|
84
|
+
record upserts.
|
|
85
|
+
|
|
86
|
+
Timing, error classification, and sanitization are handled by the
|
|
87
|
+
MixinPostgresOpExecutor base class, reducing boilerplate and ensuring
|
|
88
|
+
consistent error handling across all PostgreSQL handlers.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
_pool: asyncpg connection pool for database operations.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> import asyncpg
|
|
95
|
+
>>> pool = await asyncpg.create_pool(dsn="...")
|
|
96
|
+
>>> handler = HandlerPostgresContractUpsert(pool)
|
|
97
|
+
>>> result = await handler.handle(payload, correlation_id)
|
|
98
|
+
>>> result.success
|
|
99
|
+
True
|
|
100
|
+
|
|
101
|
+
See Also:
|
|
102
|
+
- NodeContractPersistenceEffect: Parent node that uses this handler
|
|
103
|
+
- ModelPayloadUpsertContract: Input payload model
|
|
104
|
+
- MixinPostgresOpExecutor: Shared execution mechanics
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
108
|
+
"""Initialize handler with asyncpg connection pool.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
pool: asyncpg connection pool for database operations.
|
|
112
|
+
The pool should be pre-configured and ready for use.
|
|
113
|
+
"""
|
|
114
|
+
self._pool = pool
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def handler_type(self) -> EnumHandlerType:
|
|
118
|
+
"""Architectural role of this handler."""
|
|
119
|
+
return EnumHandlerType.INFRA_HANDLER
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def handler_category(self) -> EnumHandlerTypeCategory:
|
|
123
|
+
"""Behavioral classification of this handler."""
|
|
124
|
+
return EnumHandlerTypeCategory.EFFECT
|
|
125
|
+
|
|
126
|
+
async def handle(
|
|
127
|
+
self,
|
|
128
|
+
payload: ModelPayloadUpsertContract,
|
|
129
|
+
correlation_id: UUID,
|
|
130
|
+
) -> ModelBackendResult:
|
|
131
|
+
"""Execute PostgreSQL contract record upsert.
|
|
132
|
+
|
|
133
|
+
Performs the upsert operation against the contracts table with:
|
|
134
|
+
- Contract YAML serialization (dict to YAML string)
|
|
135
|
+
- Parameterized SQL for injection prevention
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
payload: Upsert contract payload containing all contract fields
|
|
139
|
+
including contract_id, node_name, version components,
|
|
140
|
+
contract_hash, contract_yaml, and timestamps.
|
|
141
|
+
correlation_id: Request correlation ID for distributed tracing.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ModelBackendResult with:
|
|
145
|
+
- success: True if upsert completed successfully
|
|
146
|
+
- error: Sanitized error message if failed
|
|
147
|
+
- error_code: Error code for programmatic handling
|
|
148
|
+
- duration_ms: Operation duration in milliseconds
|
|
149
|
+
- backend_id: Set to "postgres"
|
|
150
|
+
- correlation_id: Passed through for tracing
|
|
151
|
+
"""
|
|
152
|
+
return await self._execute_postgres_op(
|
|
153
|
+
op_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
|
|
154
|
+
correlation_id=correlation_id,
|
|
155
|
+
log_context={
|
|
156
|
+
"contract_id": payload.contract_id,
|
|
157
|
+
"node_name": payload.node_name,
|
|
158
|
+
},
|
|
159
|
+
fn=lambda: self._execute_upsert(payload, correlation_id),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def _execute_upsert(
|
|
163
|
+
self,
|
|
164
|
+
payload: ModelPayloadUpsertContract,
|
|
165
|
+
correlation_id: UUID,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Execute the contract upsert query.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
payload: Contract upsert payload.
|
|
171
|
+
correlation_id: Correlation ID for logging.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
RepositoryExecutionError: If no result returned from upsert.
|
|
175
|
+
Any exception from asyncpg (handled by MixinPostgresOpExecutor).
|
|
176
|
+
"""
|
|
177
|
+
# Serialize contract_yaml to YAML string if it's a dict
|
|
178
|
+
# The PostgreSQL column is TEXT type, so we need a string representation
|
|
179
|
+
contract_yaml_str: str
|
|
180
|
+
if isinstance(payload.contract_yaml, dict):
|
|
181
|
+
contract_yaml_str = yaml.safe_dump(
|
|
182
|
+
payload.contract_yaml,
|
|
183
|
+
default_flow_style=False,
|
|
184
|
+
sort_keys=True,
|
|
185
|
+
allow_unicode=True,
|
|
186
|
+
)
|
|
187
|
+
elif isinstance(payload.contract_yaml, str):
|
|
188
|
+
contract_yaml_str = payload.contract_yaml
|
|
189
|
+
else:
|
|
190
|
+
# Handle unexpected types by converting to string representation
|
|
191
|
+
contract_yaml_str = str(payload.contract_yaml)
|
|
192
|
+
|
|
193
|
+
async with self._pool.acquire() as conn:
|
|
194
|
+
result = await conn.fetchrow(
|
|
195
|
+
SQL_UPSERT_CONTRACT,
|
|
196
|
+
payload.contract_id,
|
|
197
|
+
payload.node_name,
|
|
198
|
+
payload.version_major,
|
|
199
|
+
payload.version_minor,
|
|
200
|
+
payload.version_patch,
|
|
201
|
+
payload.contract_hash,
|
|
202
|
+
contract_yaml_str,
|
|
203
|
+
payload.is_active,
|
|
204
|
+
payload.registered_at,
|
|
205
|
+
payload.last_seen_at,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result is None:
|
|
209
|
+
# RETURNING clause should always return a row on success
|
|
210
|
+
# If None, something unexpected happened
|
|
211
|
+
logger.warning(
|
|
212
|
+
"Contract upsert returned no result",
|
|
213
|
+
extra={
|
|
214
|
+
"contract_id": payload.contract_id,
|
|
215
|
+
"correlation_id": str(correlation_id),
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
219
|
+
correlation_id=correlation_id,
|
|
220
|
+
transport_type="db",
|
|
221
|
+
operation="contract_upsert",
|
|
222
|
+
)
|
|
223
|
+
raise RepositoryExecutionError(
|
|
224
|
+
"postgres operation failed: no result returned",
|
|
225
|
+
context=context,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Log for observability
|
|
229
|
+
was_insert = result["was_insert"]
|
|
230
|
+
operation = "insert" if was_insert else "update"
|
|
231
|
+
logger.info(
|
|
232
|
+
"Contract upsert completed",
|
|
233
|
+
extra={
|
|
234
|
+
"contract_id": payload.contract_id,
|
|
235
|
+
"node_name": payload.node_name,
|
|
236
|
+
"operation": operation,
|
|
237
|
+
"correlation_id": str(correlation_id),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__: list[str] = ["HandlerPostgresContractUpsert"]
|