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.
Files changed (72) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  6. omnibase_infra/mixins/__init__.py +14 -0
  7. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  8. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  9. omnibase_infra/models/__init__.py +3 -0
  10. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  11. omnibase_infra/models/projection/__init__.py +11 -0
  12. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  13. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  14. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  15. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  16. omnibase_infra/nodes/effects/__init__.py +1 -1
  17. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  18. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  19. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  20. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  21. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  23. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  24. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  25. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  26. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  27. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  28. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  29. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  30. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  31. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  32. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  33. omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
  34. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  35. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
  36. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  37. omnibase_infra/projectors/__init__.py +6 -0
  38. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  39. omnibase_infra/runtime/__init__.py +5 -0
  40. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  41. omnibase_infra/runtime/db/__init__.py +4 -0
  42. omnibase_infra/runtime/db/models/__init__.py +15 -10
  43. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  44. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  45. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  46. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  47. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  48. omnibase_infra/runtime/intent_execution_router.py +430 -0
  49. omnibase_infra/runtime/models/__init__.py +6 -0
  50. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  51. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  52. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  53. omnibase_infra/runtime/protocols/__init__.py +16 -0
  54. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  55. omnibase_infra/runtime/request_response_wiring.py +785 -0
  56. omnibase_infra/runtime/service_kernel.py +295 -8
  57. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  58. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  59. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  60. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  61. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  62. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  63. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  64. omnibase_infra/services/registry_api/routes.py +205 -6
  65. omnibase_infra/services/registry_api/service.py +528 -1
  66. omnibase_infra/validation/infra_validators.py +3 -1
  67. omnibase_infra/validation/validation_exemptions.yaml +54 -0
  68. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
  69. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
  70. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
  71. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
  72. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,314 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """PostgreSQL Error Response Mixin.
4
+
5
+ Provides standardized PostgreSQL exception handling for persistence operations
6
+ in NodeContractPersistenceEffect. Extracts the common ~60-line exception
7
+ handling pattern into a reusable mixin.
8
+
9
+ Architecture:
10
+ MixinPostgresErrorResponse is designed to be mixed into PostgreSQL
11
+ persistence classes to provide consistent error handling, sanitization,
12
+ logging, and ModelBackendResult construction.
13
+
14
+ The mixin handles:
15
+ - TimeoutError/InfraTimeoutError -> TIMEOUT_ERROR code
16
+ - InfraAuthenticationError -> AUTH_ERROR code
17
+ - InfraConnectionError -> CONNECTION_ERROR code
18
+ - RepositoryExecutionError -> operation-specific error code
19
+ - Generic Exception -> UNKNOWN_ERROR code
20
+
21
+ Error Sanitization:
22
+ All error messages are sanitized using utility functions to prevent
23
+ exposure of sensitive information (credentials, connection strings)
24
+ in logs and responses.
25
+
26
+ Logging:
27
+ - Timeout/Connection errors: logger.warning (retriable)
28
+ - Auth errors: logger.exception (non-retriable, needs attention)
29
+ - Repository errors: logger.warning (may be retriable)
30
+ - Unknown errors: logger.exception (needs investigation)
31
+
32
+ Usage:
33
+ >>> class MyPersistence(MixinPostgresErrorResponse):
34
+ ... async def handle(self, payload, correlation_id):
35
+ ... start_time = time.perf_counter()
36
+ ... try:
37
+ ... # ... database operation ...
38
+ ... except Exception as e:
39
+ ... ctx = PostgresErrorContext(
40
+ ... exception=e,
41
+ ... operation="my_operation",
42
+ ... correlation_id=correlation_id,
43
+ ... start_time=start_time,
44
+ ... log_context={"my_field": "value"},
45
+ ... operation_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
46
+ ... )
47
+ ... return self._build_error_response(ctx)
48
+
49
+ Related:
50
+ - NodeContractPersistenceEffect: Parent effect node
51
+ - EnumPostgresErrorCode: Error code enumeration
52
+ - ModelBackendResult: Structured result model
53
+ - OMN-1845: Implementation ticket
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import logging
59
+ import time
60
+ from dataclasses import dataclass, field
61
+ from typing import TYPE_CHECKING
62
+
63
+ from omnibase_infra.enums import EnumPostgresErrorCode
64
+ from omnibase_infra.errors import (
65
+ InfraAuthenticationError,
66
+ InfraConnectionError,
67
+ InfraTimeoutError,
68
+ RepositoryExecutionError,
69
+ )
70
+ from omnibase_infra.utils import sanitize_backend_error, sanitize_error_message
71
+
72
+ if TYPE_CHECKING:
73
+ from uuid import UUID
74
+
75
+ from omnibase_infra.models.model_backend_result import (
76
+ ModelBackendResult,
77
+ )
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ @dataclass
83
+ class PostgresErrorContext:
84
+ """Context for PostgreSQL error handling.
85
+
86
+ Encapsulates all parameters needed for error handling to reduce
87
+ function parameter count and improve readability.
88
+
89
+ Attributes:
90
+ exception: The exception that was raised during the operation.
91
+ operation: Name of the operation for logging.
92
+ correlation_id: Request correlation ID for distributed tracing.
93
+ start_time: Result of time.perf_counter() captured before operation.
94
+ log_context: Additional context fields for log messages.
95
+ operation_error_code: Error code for RepositoryExecutionError.
96
+ """
97
+
98
+ exception: Exception
99
+ operation: str
100
+ correlation_id: UUID
101
+ start_time: float
102
+ log_context: dict[str, object] = field(default_factory=dict)
103
+ operation_error_code: EnumPostgresErrorCode | None = None
104
+
105
+
106
+ class MixinPostgresErrorResponse:
107
+ """Mixin providing standardized PostgreSQL exception handling.
108
+
109
+ Consolidates the common exception handling pattern used across all
110
+ PostgreSQL handlers in NodeContractPersistenceEffect. This ensures
111
+ consistent error classification, sanitization, logging, and result
112
+ construction.
113
+
114
+ The mixin is designed to be used with any class that needs to handle
115
+ PostgreSQL operation errors and return ModelBackendResult.
116
+
117
+ Error Handling Matrix:
118
+ | Exception Type | Error Code | Log Level | Retriable |
119
+ |--------------------------|------------------|------------|-----------|
120
+ | TimeoutError | TIMEOUT_ERROR | warning | Yes |
121
+ | InfraTimeoutError | TIMEOUT_ERROR | warning | Yes |
122
+ | InfraAuthenticationError | AUTH_ERROR | exception | No |
123
+ | InfraConnectionError | CONNECTION_ERROR | warning | Yes |
124
+ | RepositoryExecutionError | (configurable) | warning | Maybe |
125
+ | Exception (catch-all) | UNKNOWN_ERROR | exception | No |
126
+
127
+ Example:
128
+ >>> class HandlerPostgresExample(MixinPostgresErrorResponse):
129
+ ... async def handle(self, payload, correlation_id):
130
+ ... start_time = time.perf_counter()
131
+ ... try:
132
+ ... async with self._pool.acquire() as conn:
133
+ ... await conn.execute("SELECT 1")
134
+ ... duration_ms = (time.perf_counter() - start_time) * 1000
135
+ ... return ModelBackendResult(
136
+ ... success=True,
137
+ ... duration_ms=duration_ms,
138
+ ... backend_id="postgres",
139
+ ... correlation_id=correlation_id,
140
+ ... )
141
+ ... except Exception as e:
142
+ ... ctx = PostgresErrorContext(
143
+ ... exception=e,
144
+ ... operation="example_operation",
145
+ ... correlation_id=correlation_id,
146
+ ... start_time=start_time,
147
+ ... )
148
+ ... return self._build_error_response(ctx)
149
+
150
+ See Also:
151
+ - HandlerPostgresContractUpsert: Example handler using this mixin
152
+ - HandlerPostgresTopicUpdate: Example handler using this mixin
153
+ - EnumPostgresErrorCode: Error code classification
154
+ """
155
+
156
+ def _build_error_response(
157
+ self,
158
+ ctx: PostgresErrorContext,
159
+ ) -> ModelBackendResult:
160
+ """Build ModelBackendResult for PostgreSQL operation exceptions.
161
+
162
+ Processes an exception raised during a PostgreSQL operation and
163
+ returns a properly constructed ModelBackendResult with:
164
+ - Appropriate error code based on exception type
165
+ - Sanitized error message (no credentials/PII)
166
+ - Operation duration in milliseconds
167
+ - Correlation ID for distributed tracing
168
+
169
+ Args:
170
+ ctx: PostgresErrorContext containing all error handling parameters.
171
+
172
+ Returns:
173
+ ModelBackendResult with:
174
+ - success: Always False (this is error handling)
175
+ - error: Sanitized error message safe for logs/responses
176
+ - error_code: EnumPostgresErrorCode based on exception type
177
+ - duration_ms: Operation duration in milliseconds
178
+ - backend_id: Always "postgres"
179
+ - correlation_id: Passed through for tracing
180
+
181
+ Note:
182
+ This method never raises exceptions. All error paths return
183
+ a properly constructed ModelBackendResult.
184
+ """
185
+ # Extract context fields for readability
186
+ exception = ctx.exception
187
+ operation = ctx.operation
188
+ correlation_id = ctx.correlation_id
189
+ start_time = ctx.start_time
190
+ log_context = ctx.log_context
191
+ operation_error_code = ctx.operation_error_code
192
+ # Local import to avoid circular import at module load time
193
+ # (mixins/__init__.py loads before nodes/__init__.py in some paths)
194
+ from omnibase_infra.models.model_backend_result import (
195
+ ModelBackendResult as BackendResult,
196
+ )
197
+
198
+ duration_ms = (time.perf_counter() - start_time) * 1000
199
+
200
+ # Build base log context
201
+ base_context: dict[str, object] = {
202
+ "correlation_id": str(correlation_id),
203
+ "duration_ms": duration_ms,
204
+ }
205
+
206
+ # Merge caller-provided context
207
+ if log_context:
208
+ base_context.update(log_context)
209
+
210
+ # Handle timeout errors - retriable infrastructure failures
211
+ if isinstance(exception, (TimeoutError, InfraTimeoutError)):
212
+ sanitized_error = sanitize_error_message(exception)
213
+ base_context["error"] = sanitized_error
214
+
215
+ logger.warning(
216
+ f"{operation} timed out",
217
+ extra=base_context,
218
+ )
219
+
220
+ return BackendResult(
221
+ success=False,
222
+ error=sanitized_error,
223
+ error_code=EnumPostgresErrorCode.TIMEOUT_ERROR,
224
+ duration_ms=duration_ms,
225
+ backend_id="postgres",
226
+ correlation_id=correlation_id,
227
+ )
228
+
229
+ # Handle authentication errors - non-retriable configuration failures
230
+ if isinstance(exception, InfraAuthenticationError):
231
+ sanitized_error = sanitize_error_message(exception)
232
+ base_context["error"] = sanitized_error
233
+
234
+ logger.exception(
235
+ f"{operation} authentication failed",
236
+ extra=base_context,
237
+ )
238
+
239
+ return BackendResult(
240
+ success=False,
241
+ error=sanitized_error,
242
+ error_code=EnumPostgresErrorCode.AUTH_ERROR,
243
+ duration_ms=duration_ms,
244
+ backend_id="postgres",
245
+ correlation_id=correlation_id,
246
+ )
247
+
248
+ # Handle connection errors - retriable infrastructure failures
249
+ if isinstance(exception, InfraConnectionError):
250
+ sanitized_error = sanitize_error_message(exception)
251
+ base_context["error"] = sanitized_error
252
+
253
+ logger.warning(
254
+ f"{operation} connection failed",
255
+ extra=base_context,
256
+ )
257
+
258
+ return BackendResult(
259
+ success=False,
260
+ error=sanitized_error,
261
+ error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
262
+ duration_ms=duration_ms,
263
+ backend_id="postgres",
264
+ correlation_id=correlation_id,
265
+ )
266
+
267
+ # Handle repository execution errors - operation-specific failures
268
+ if isinstance(exception, RepositoryExecutionError):
269
+ sanitized_error = sanitize_error_message(exception)
270
+ base_context["error"] = sanitized_error
271
+
272
+ logger.warning(
273
+ f"{operation} execution failed",
274
+ extra=base_context,
275
+ )
276
+
277
+ # Use operation-specific error code if provided, else fall back to UNKNOWN
278
+ error_code = operation_error_code or EnumPostgresErrorCode.UNKNOWN_ERROR
279
+
280
+ return BackendResult(
281
+ success=False,
282
+ error=sanitized_error,
283
+ error_code=error_code,
284
+ duration_ms=duration_ms,
285
+ backend_id="postgres",
286
+ correlation_id=correlation_id,
287
+ )
288
+
289
+ # Generic catch-all for unexpected exceptions
290
+ # This catch-all is required because database adapters may raise
291
+ # unexpected exceptions beyond typed infrastructure errors (e.g.,
292
+ # driver errors, encoding errors, connection pool errors, asyncpg-
293
+ # specific exceptions). All errors must be sanitized to prevent
294
+ # credential exposure.
295
+ sanitized_error = sanitize_backend_error("postgres", exception)
296
+ base_context["error"] = sanitized_error
297
+ base_context["error_type"] = type(exception).__name__
298
+
299
+ logger.exception(
300
+ f"{operation} failed with unexpected error",
301
+ extra=base_context,
302
+ )
303
+
304
+ return BackendResult(
305
+ success=False,
306
+ error=sanitized_error,
307
+ error_code=EnumPostgresErrorCode.UNKNOWN_ERROR,
308
+ duration_ms=duration_ms,
309
+ backend_id="postgres",
310
+ correlation_id=correlation_id,
311
+ )
312
+
313
+
314
+ __all__: list[str] = ["MixinPostgresErrorResponse", "PostgresErrorContext"]
@@ -0,0 +1,298 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Shared execution core for PostgreSQL operation handlers.
4
+
5
+ This mixin centralizes the mechanical aspects of PostgreSQL handler execution:
6
+ - Timing via time.perf_counter()
7
+ - Error classification and sanitization
8
+ - ModelBackendResult construction
9
+ - Structured logging with correlation IDs
10
+
11
+ By extracting this boilerplate into a reusable mixin, handlers are reduced from
12
+ ~200 lines to ~30 lines, eliminating drift risk where error handling patterns
13
+ could diverge across handlers.
14
+
15
+ Architecture:
16
+ Handlers inherit from MixinPostgresOpExecutor and call _execute_postgres_op()
17
+ with their operation-specific logic wrapped in a callable. The mixin handles
18
+ all timing, error classification, sanitization, and result construction.
19
+
20
+ Error Classification:
21
+ - TimeoutError, InfraTimeoutError → POSTGRES_TIMEOUT_ERROR (retriable)
22
+ - InfraAuthenticationError → POSTGRES_AUTH_ERROR (non-retriable)
23
+ - InfraConnectionError → POSTGRES_CONNECTION_ERROR (retriable)
24
+ - RepositoryExecutionError → op_error_code (handler-specified)
25
+ - Exception → POSTGRES_UNKNOWN_ERROR (non-retriable)
26
+
27
+ Usage:
28
+ ```python
29
+ class HandlerPostgresHeartbeat(MixinPostgresOpExecutor):
30
+ async def handle(self, payload, correlation_id) -> ModelBackendResult:
31
+ return await self._execute_postgres_op(
32
+ op_error_code=EnumPostgresErrorCode.HEARTBEAT_ERROR,
33
+ correlation_id=correlation_id,
34
+ log_context={"contract_id": payload.contract_id},
35
+ fn=lambda: self._do_heartbeat(payload),
36
+ )
37
+ ```
38
+
39
+ Related:
40
+ - EnumPostgresErrorCode: Error code enumeration with retriability metadata
41
+ - MixinAsyncCircuitBreaker: Circuit breaker mixin (not integrated here)
42
+ - OMN-1857: Extraction ticket for this mixin
43
+
44
+ Note on Circuit Breaker:
45
+ Per OMN-1857 design decision 1A, the executor should manage circuit breaker
46
+ internally. However, this initial implementation focuses on the core execution
47
+ mechanics. Circuit breaker integration will be added as a follow-up once the
48
+ basic pattern is validated.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import logging
54
+ import time
55
+ from collections.abc import Awaitable, Callable
56
+ from typing import TYPE_CHECKING, TypeVar
57
+
58
+ from omnibase_infra.enums import EnumPostgresErrorCode
59
+ from omnibase_infra.errors import (
60
+ InfraAuthenticationError,
61
+ InfraConnectionError,
62
+ InfraTimeoutError,
63
+ RepositoryExecutionError,
64
+ )
65
+ from omnibase_infra.models.model_backend_result import ModelBackendResult
66
+ from omnibase_infra.utils import sanitize_backend_error, sanitize_error_message
67
+
68
+ if TYPE_CHECKING:
69
+ from uuid import UUID
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+ T = TypeVar("T")
74
+
75
+
76
+ class MixinPostgresOpExecutor:
77
+ """Shared execution core for PostgreSQL operation handlers.
78
+
79
+ Centralizes timing, error handling, sanitization, and result construction
80
+ for PostgreSQL operations. Handlers inherit this mixin and delegate to
81
+ _execute_postgres_op() for consistent mechanical behavior.
82
+
83
+ This mixin does NOT manage circuit breaker state - that responsibility
84
+ remains with the handler or a separate MixinAsyncCircuitBreaker composition.
85
+
86
+ Example:
87
+ ```python
88
+ class HandlerPostgresUpsert(MixinPostgresOpExecutor):
89
+ def __init__(self, pool: asyncpg.Pool) -> None:
90
+ self._pool = pool
91
+
92
+ async def handle(
93
+ self, payload: ModelPayloadUpsertContract, correlation_id: UUID
94
+ ) -> ModelBackendResult:
95
+ return await self._execute_postgres_op(
96
+ op_error_code=EnumPostgresErrorCode.UPSERT_ERROR,
97
+ correlation_id=correlation_id,
98
+ log_context={
99
+ "contract_id": payload.contract_id,
100
+ "node_name": payload.node_name,
101
+ },
102
+ fn=lambda: self._execute_upsert(payload),
103
+ )
104
+ ```
105
+
106
+ See Also:
107
+ - EnumPostgresErrorCode: Error codes with is_retriable property
108
+ - sanitize_error_message: Error sanitization utility
109
+ - ModelBackendResult: Structured result model
110
+ """
111
+
112
+ async def _execute_postgres_op(
113
+ self,
114
+ *,
115
+ op_error_code: EnumPostgresErrorCode,
116
+ correlation_id: UUID,
117
+ log_context: dict[str, object],
118
+ fn: Callable[[], Awaitable[T]],
119
+ ) -> ModelBackendResult:
120
+ """Execute a PostgreSQL operation with timing, error handling, and sanitization.
121
+
122
+ This method wraps the actual database operation (fn) with:
123
+ 1. Timing measurement via time.perf_counter()
124
+ 2. Exception classification into appropriate error codes
125
+ 3. Error message sanitization to prevent credential exposure
126
+ 4. Structured logging with correlation ID and context
127
+ 5. ModelBackendResult construction
128
+
129
+ Args:
130
+ op_error_code: Operation-specific error code for non-infrastructure
131
+ failures (e.g., UPSERT_ERROR, HEARTBEAT_ERROR). Used when the
132
+ operation fails due to business logic or query issues rather
133
+ than connection/auth problems.
134
+ correlation_id: Request correlation ID for distributed tracing.
135
+ log_context: Additional fields for structured logging (e.g.,
136
+ contract_id, node_name). Included in all log messages.
137
+ fn: Async callable that performs the actual database operation.
138
+ Should return any value on success. The return value is not
139
+ used - only success/failure matters.
140
+
141
+ Returns:
142
+ ModelBackendResult with:
143
+ - success: True if fn() completed without exception
144
+ - error: Sanitized error message (empty string on success)
145
+ - error_code: Appropriate EnumPostgresErrorCode
146
+ - duration_ms: Operation duration in milliseconds
147
+ - backend_id: "postgres"
148
+ - correlation_id: Passed through for tracing
149
+
150
+ Error Classification:
151
+ | Exception Type | Error Code | Retriable |
152
+ |-----------------------------|-------------------------|-----------|
153
+ | TimeoutError | TIMEOUT_ERROR | Yes |
154
+ | InfraTimeoutError | TIMEOUT_ERROR | Yes |
155
+ | InfraAuthenticationError | AUTH_ERROR | No |
156
+ | InfraConnectionError | CONNECTION_ERROR | Yes |
157
+ | RepositoryExecutionError | op_error_code | No |
158
+ | Exception | UNKNOWN_ERROR | No |
159
+
160
+ Note:
161
+ This method never raises exceptions. All errors are captured,
162
+ sanitized, logged, and returned in the result model.
163
+ """
164
+ start_time = time.perf_counter()
165
+ log_extra = {
166
+ "correlation_id": str(correlation_id),
167
+ **log_context,
168
+ }
169
+
170
+ try:
171
+ # Execute the operation
172
+ await fn()
173
+
174
+ duration_ms = (time.perf_counter() - start_time) * 1000
175
+
176
+ logger.debug(
177
+ "PostgreSQL operation completed successfully",
178
+ extra={**log_extra, "duration_ms": duration_ms},
179
+ )
180
+
181
+ return ModelBackendResult(
182
+ success=True,
183
+ duration_ms=duration_ms,
184
+ backend_id="postgres",
185
+ correlation_id=correlation_id,
186
+ )
187
+
188
+ except (TimeoutError, InfraTimeoutError) as e:
189
+ # Timeout - retriable error
190
+ duration_ms = (time.perf_counter() - start_time) * 1000
191
+ sanitized_error = sanitize_error_message(e)
192
+ logger.warning(
193
+ "PostgreSQL operation timed out",
194
+ extra={
195
+ **log_extra,
196
+ "duration_ms": duration_ms,
197
+ "error": sanitized_error,
198
+ },
199
+ )
200
+ return ModelBackendResult(
201
+ success=False,
202
+ error=sanitized_error,
203
+ error_code=EnumPostgresErrorCode.TIMEOUT_ERROR,
204
+ duration_ms=duration_ms,
205
+ backend_id="postgres",
206
+ correlation_id=correlation_id,
207
+ )
208
+
209
+ except InfraAuthenticationError as e:
210
+ # Authentication failure - non-retriable
211
+ duration_ms = (time.perf_counter() - start_time) * 1000
212
+ sanitized_error = sanitize_error_message(e)
213
+ logger.exception(
214
+ "PostgreSQL authentication failed",
215
+ extra={
216
+ **log_extra,
217
+ "duration_ms": duration_ms,
218
+ "error": sanitized_error,
219
+ },
220
+ )
221
+ return ModelBackendResult(
222
+ success=False,
223
+ error=sanitized_error,
224
+ error_code=EnumPostgresErrorCode.AUTH_ERROR,
225
+ duration_ms=duration_ms,
226
+ backend_id="postgres",
227
+ correlation_id=correlation_id,
228
+ )
229
+
230
+ except InfraConnectionError as e:
231
+ # Connection failure - retriable
232
+ duration_ms = (time.perf_counter() - start_time) * 1000
233
+ sanitized_error = sanitize_error_message(e)
234
+ logger.warning(
235
+ "PostgreSQL connection failed",
236
+ extra={
237
+ **log_extra,
238
+ "duration_ms": duration_ms,
239
+ "error": sanitized_error,
240
+ },
241
+ )
242
+ return ModelBackendResult(
243
+ success=False,
244
+ error=sanitized_error,
245
+ error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
246
+ duration_ms=duration_ms,
247
+ backend_id="postgres",
248
+ correlation_id=correlation_id,
249
+ )
250
+
251
+ except RepositoryExecutionError as e:
252
+ # Query/operation failure - use handler-provided error code
253
+ duration_ms = (time.perf_counter() - start_time) * 1000
254
+ sanitized_error = sanitize_error_message(e)
255
+ logger.warning(
256
+ "PostgreSQL operation failed",
257
+ extra={
258
+ **log_extra,
259
+ "duration_ms": duration_ms,
260
+ "error": sanitized_error,
261
+ "error_code": op_error_code.value,
262
+ },
263
+ )
264
+ return ModelBackendResult(
265
+ success=False,
266
+ error=sanitized_error,
267
+ error_code=op_error_code,
268
+ duration_ms=duration_ms,
269
+ backend_id="postgres",
270
+ correlation_id=correlation_id,
271
+ )
272
+
273
+ except (
274
+ Exception
275
+ ) as e: # ONEX: catch-all for driver errors, encoding errors, pool errors
276
+ # Unknown error - non-retriable, requires investigation
277
+ duration_ms = (time.perf_counter() - start_time) * 1000
278
+ sanitized_error = sanitize_backend_error("postgres", e)
279
+ logger.exception(
280
+ "PostgreSQL operation failed with unexpected error",
281
+ extra={
282
+ **log_extra,
283
+ "duration_ms": duration_ms,
284
+ "error_type": type(e).__name__,
285
+ "error": sanitized_error,
286
+ },
287
+ )
288
+ return ModelBackendResult(
289
+ success=False,
290
+ error=sanitized_error,
291
+ error_code=EnumPostgresErrorCode.UNKNOWN_ERROR,
292
+ duration_ms=duration_ms,
293
+ backend_id="postgres",
294
+ correlation_id=correlation_id,
295
+ )
296
+
297
+
298
+ __all__ = ["MixinPostgresOpExecutor"]
@@ -30,6 +30,7 @@ from omnibase_infra.models.event_bus import (
30
30
  from omnibase_infra.models.handlers import ModelHandlerIdentifier
31
31
  from omnibase_infra.models.health import ModelHealthCheckResult
32
32
  from omnibase_infra.models.logging import ModelLogContext
33
+ from omnibase_infra.models.model_backend_result import ModelBackendResult
33
34
  from omnibase_infra.models.model_node_identity import ModelNodeIdentity
34
35
  from omnibase_infra.models.model_retry_error_classification import (
35
36
  ModelRetryErrorClassification,
@@ -93,6 +94,8 @@ __all__: list[str] = [
93
94
  "ModelConsumerRetryConfig",
94
95
  "ModelIdempotencyConfig",
95
96
  "ModelOffsetPolicyConfig",
97
+ # Backend result models
98
+ "ModelBackendResult",
96
99
  # Resilience models
97
100
  "ModelCircuitBreakerConfig",
98
101
  # Validation models
@@ -61,7 +61,9 @@ class ModelBackendResult(BaseModel):
61
61
  Attributes:
62
62
  success: Whether the backend operation completed successfully.
63
63
  error: Sanitized error message if success is False.
64
- error_code: Optional error code for programmatic handling.
64
+ error_code: Error code for programmatic handling. PostgreSQL handlers
65
+ use EnumPostgresErrorCode enum values which serialize to strings
66
+ (e.g., "POSTGRES_CONNECTION_ERROR"). Other backends use string codes.
65
67
  duration_ms: Time taken for the operation in milliseconds.
66
68
  backend_id: Optional identifier for the backend instance.
67
69
 
@@ -96,18 +98,30 @@ class ModelBackendResult(BaseModel):
96
98
  >>> result.success
97
99
  True
98
100
 
99
- Example (failure case):
101
+ Example (failure case with PostgreSQL enum):
102
+ >>> from omnibase_infra.enums import EnumPostgresErrorCode
100
103
  >>> result = ModelBackendResult(
101
104
  ... success=False,
102
105
  ... error="Connection refused to database host",
103
- ... error_code="DATABASE_CONNECTION_ERROR",
106
+ ... error_code=EnumPostgresErrorCode.CONNECTION_ERROR,
104
107
  ... duration_ms=5000.0,
105
108
  ... backend_id="postgres",
106
109
  ... )
107
110
  >>> result.success
108
111
  False
109
- >>> result.error
110
- 'Connection refused to database host'
112
+ >>> result.error_code # Enum serializes to string
113
+ 'POSTGRES_CONNECTION_ERROR'
114
+
115
+ Example (failure case with Consul string code):
116
+ >>> result = ModelBackendResult(
117
+ ... success=False,
118
+ ... error="Service registration failed",
119
+ ... error_code="CONSUL_CONNECTION_ERROR",
120
+ ... duration_ms=1500.0,
121
+ ... backend_id="consul",
122
+ ... )
123
+ >>> result.error_code
124
+ 'CONSUL_CONNECTION_ERROR'
111
125
  """
112
126
 
113
127
  model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
@@ -122,7 +136,9 @@ class ModelBackendResult(BaseModel):
122
136
  )
123
137
  error_code: str | None = Field(
124
138
  default=None,
125
- description="Error code for programmatic handling (e.g., DATABASE_CONNECTION_ERROR)",
139
+ description="Error code for programmatic handling. PostgreSQL handlers use "
140
+ "EnumPostgresErrorCode enum values (which serialize to strings like "
141
+ "'POSTGRES_CONNECTION_ERROR'). Other backends use string codes directly.",
126
142
  )
127
143
  duration_ms: float = Field(
128
144
  default=0.0,