omnibase_infra 0.3.1__py3-none-any.whl → 0.4.0__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/errors/__init__.py +4 -0
- omnibase_infra/errors/error_infra.py +60 -0
- omnibase_infra/handlers/__init__.py +3 -0
- omnibase_infra/handlers/handler_slack_webhook.py +426 -0
- omnibase_infra/handlers/models/__init__.py +14 -0
- omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
- omnibase_infra/handlers/models/model_slack_alert.py +24 -0
- omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
- omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- 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/models/discovery/model_dependency_spec.py +1 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +28 -1
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +1 -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/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- 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 +131 -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 +251 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
- omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
- omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +12 -0
- omnibase_infra/runtime/baseline_subscriptions.py +13 -6
- omnibase_infra/runtime/contract_dependency_resolver.py +455 -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/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- 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/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +793 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- omnibase_infra/runtime/service_runtime_host_process.py +149 -5
- omnibase_infra/runtime/util_version.py +5 -1
- omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
- omnibase_infra/services/contract_publisher/config.py +4 -4
- omnibase_infra/services/contract_publisher/service.py +8 -5
- omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
- omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
- omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
- omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
- omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
- 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/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +65 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -50,6 +50,7 @@ import asyncpg
|
|
|
50
50
|
|
|
51
51
|
from omnibase_core.container import ModelONEXContainer
|
|
52
52
|
from omnibase_core.enums.enum_node_kind import EnumNodeKind
|
|
53
|
+
from omnibase_core.models.primitives.model_semver import ModelSemVer
|
|
53
54
|
from omnibase_infra.enums import EnumInfraTransportType
|
|
54
55
|
from omnibase_infra.errors import (
|
|
55
56
|
InfraConnectionError,
|
|
@@ -87,6 +88,9 @@ DEFAULT_POOL_SIZE = 10
|
|
|
87
88
|
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
88
89
|
|
|
89
90
|
# SQL statements
|
|
91
|
+
# NOTE: Database column is `registered_at` but model uses `created_at`. The column
|
|
92
|
+
# is aliased in queries for mapping. This aligns with the existing database schema
|
|
93
|
+
# on 192.168.86.200 which uses `registered_at` for the creation timestamp.
|
|
90
94
|
SQL_CREATE_TABLE = """
|
|
91
95
|
CREATE TABLE IF NOT EXISTS node_registrations (
|
|
92
96
|
node_id UUID PRIMARY KEY,
|
|
@@ -95,13 +99,13 @@ CREATE TABLE IF NOT EXISTS node_registrations (
|
|
|
95
99
|
capabilities JSONB NOT NULL DEFAULT '[]',
|
|
96
100
|
endpoints JSONB NOT NULL DEFAULT '{}',
|
|
97
101
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
98
|
-
|
|
102
|
+
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
99
103
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
100
104
|
);
|
|
101
105
|
"""
|
|
102
106
|
|
|
103
107
|
SQL_UPSERT = """
|
|
104
|
-
INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata,
|
|
108
|
+
INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at, updated_at)
|
|
105
109
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
106
110
|
ON CONFLICT (node_id) DO UPDATE SET
|
|
107
111
|
node_type = EXCLUDED.node_type,
|
|
@@ -114,7 +118,7 @@ RETURNING (xmax = 0) AS was_insert;
|
|
|
114
118
|
"""
|
|
115
119
|
|
|
116
120
|
SQL_QUERY_BASE = """
|
|
117
|
-
SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, created_at, updated_at
|
|
121
|
+
SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at AS created_at, updated_at
|
|
118
122
|
FROM node_registrations
|
|
119
123
|
"""
|
|
120
124
|
|
|
@@ -384,9 +388,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
384
388
|
result = await asyncio.wait_for(
|
|
385
389
|
conn.fetchrow(
|
|
386
390
|
SQL_UPSERT,
|
|
387
|
-
record.node_id,
|
|
391
|
+
str(record.node_id), # VARCHAR column requires string
|
|
388
392
|
record.node_type.value,
|
|
389
|
-
record.node_version,
|
|
393
|
+
str(record.node_version), # VARCHAR column requires string
|
|
390
394
|
capabilities_json,
|
|
391
395
|
endpoints_json,
|
|
392
396
|
metadata_json,
|
|
@@ -502,7 +506,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
502
506
|
# Filter by node_id if specified (exact match)
|
|
503
507
|
if query.node_id is not None:
|
|
504
508
|
conditions.append(f"node_id = ${param_idx}")
|
|
505
|
-
params.append(query.node_id)
|
|
509
|
+
params.append(str(query.node_id)) # VARCHAR column requires string
|
|
506
510
|
param_idx += 1
|
|
507
511
|
|
|
508
512
|
# Filter by node_type if specified
|
|
@@ -531,15 +535,15 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
531
535
|
count_params = params[:-2] # Exclude limit and offset
|
|
532
536
|
|
|
533
537
|
async with pool.acquire() as conn:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
538
|
+
# NOTE: asyncpg connections don't support concurrent operations,
|
|
539
|
+
# so we run these queries sequentially instead of with asyncio.gather
|
|
540
|
+
rows = await asyncio.wait_for(
|
|
541
|
+
conn.fetch(sql_query, *params),
|
|
542
|
+
timeout=self._timeout_seconds,
|
|
543
|
+
)
|
|
544
|
+
count_result = await asyncio.wait_for(
|
|
545
|
+
conn.fetchval(count_query, *count_params),
|
|
546
|
+
timeout=self._timeout_seconds,
|
|
543
547
|
)
|
|
544
548
|
|
|
545
549
|
# Reset circuit breaker on success
|
|
@@ -555,11 +559,14 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
555
559
|
endpoints = json.loads(row["endpoints"]) if row["endpoints"] else {}
|
|
556
560
|
metadata = json.loads(row["metadata"]) if row["metadata"] else {}
|
|
557
561
|
|
|
562
|
+
# Convert database types to model types:
|
|
563
|
+
# - node_id: VARCHAR -> UUID
|
|
564
|
+
# - node_version: VARCHAR -> ModelSemVer
|
|
558
565
|
records.append(
|
|
559
566
|
ModelRegistrationRecord(
|
|
560
|
-
node_id=row["node_id"],
|
|
567
|
+
node_id=UUID(row["node_id"]),
|
|
561
568
|
node_type=EnumNodeKind(row["node_type"]),
|
|
562
|
-
node_version=row["node_version"],
|
|
569
|
+
node_version=ModelSemVer.parse(row["node_version"]),
|
|
563
570
|
capabilities=capabilities,
|
|
564
571
|
endpoints=endpoints,
|
|
565
572
|
metadata=metadata,
|
|
@@ -684,11 +691,11 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
684
691
|
result = await asyncio.wait_for(
|
|
685
692
|
conn.fetchval(
|
|
686
693
|
SQL_UPDATE,
|
|
687
|
-
node_id,
|
|
694
|
+
str(node_id), # VARCHAR column requires string
|
|
688
695
|
capabilities_json,
|
|
689
696
|
endpoints_json,
|
|
690
697
|
metadata_json,
|
|
691
|
-
node_version,
|
|
698
|
+
str(node_version) if node_version is not None else None,
|
|
692
699
|
),
|
|
693
700
|
timeout=self._timeout_seconds,
|
|
694
701
|
)
|
|
@@ -792,7 +799,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
792
799
|
|
|
793
800
|
async with pool.acquire() as conn:
|
|
794
801
|
result = await asyncio.wait_for(
|
|
795
|
-
conn.fetchval(
|
|
802
|
+
conn.fetchval(
|
|
803
|
+
SQL_DELETE, str(node_id)
|
|
804
|
+
), # VARCHAR column requires string
|
|
796
805
|
timeout=self._timeout_seconds,
|
|
797
806
|
)
|
|
798
807
|
|
|
@@ -7,6 +7,7 @@ Reusable mixin classes providing:
|
|
|
7
7
|
- Infrastructure error integration
|
|
8
8
|
- Correlation ID propagation
|
|
9
9
|
- Configurable behavior
|
|
10
|
+
- PostgreSQL error response building for effect persistence
|
|
10
11
|
|
|
11
12
|
Exports (in __all__):
|
|
12
13
|
Mixins:
|
|
@@ -14,8 +15,13 @@ Exports (in __all__):
|
|
|
14
15
|
- MixinDictLikeAccessors: Dictionary-style access helpers
|
|
15
16
|
- MixinEnvelopeExtraction: Event envelope extraction utilities
|
|
16
17
|
- MixinNodeIntrospection: Node capability introspection
|
|
18
|
+
- MixinPostgresErrorResponse: PostgreSQL exception handling for persistence
|
|
19
|
+
- MixinPostgresOpExecutor: PostgreSQL operation execution with error handling
|
|
17
20
|
- MixinRetryExecution: Retry logic with exponential backoff
|
|
18
21
|
|
|
22
|
+
Dataclasses:
|
|
23
|
+
- PostgresErrorContext: Context for PostgreSQL error handling
|
|
24
|
+
|
|
19
25
|
Protocols (co-located with their tightly-coupled mixins):
|
|
20
26
|
- ProtocolCircuitBreakerAware: Interface for circuit breaker capability.
|
|
21
27
|
Co-located here because it is tightly coupled to MixinAsyncCircuitBreaker.
|
|
@@ -48,6 +54,11 @@ from omnibase_infra.mixins.mixin_async_circuit_breaker import MixinAsyncCircuitB
|
|
|
48
54
|
from omnibase_infra.mixins.mixin_dict_like_accessors import MixinDictLikeAccessors
|
|
49
55
|
from omnibase_infra.mixins.mixin_envelope_extraction import MixinEnvelopeExtraction
|
|
50
56
|
from omnibase_infra.mixins.mixin_node_introspection import MixinNodeIntrospection
|
|
57
|
+
from omnibase_infra.mixins.mixin_postgres_error_response import (
|
|
58
|
+
MixinPostgresErrorResponse,
|
|
59
|
+
PostgresErrorContext,
|
|
60
|
+
)
|
|
61
|
+
from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
|
|
51
62
|
from omnibase_infra.mixins.mixin_retry_execution import MixinRetryExecution
|
|
52
63
|
from omnibase_infra.mixins.protocol_circuit_breaker_aware import (
|
|
53
64
|
ProtocolCircuitBreakerAware,
|
|
@@ -63,6 +74,9 @@ __all__: list[str] = [
|
|
|
63
74
|
"MixinDictLikeAccessors",
|
|
64
75
|
"MixinEnvelopeExtraction",
|
|
65
76
|
"MixinNodeIntrospection",
|
|
77
|
+
"MixinPostgresErrorResponse",
|
|
78
|
+
"MixinPostgresOpExecutor",
|
|
79
|
+
"PostgresErrorContext",
|
|
66
80
|
"MixinRetryExecution",
|
|
67
81
|
"ModelCircuitBreakerConfig",
|
|
68
82
|
"ModelRetryErrorClassification",
|
|
@@ -154,6 +154,7 @@ Usage:
|
|
|
154
154
|
config = ModelIntrospectionConfig(
|
|
155
155
|
node_id=node_config.node_id,
|
|
156
156
|
node_type=EnumNodeKind.EFFECT,
|
|
157
|
+
node_name="my_node",
|
|
157
158
|
event_bus=event_bus,
|
|
158
159
|
)
|
|
159
160
|
self.initialize_introspection(config)
|
|
@@ -214,6 +215,7 @@ from omnibase_infra.capabilities import ContractCapabilityExtractor
|
|
|
214
215
|
from omnibase_infra.constants_topic_patterns import TOPIC_NAME_PATTERN
|
|
215
216
|
from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
|
|
216
217
|
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
218
|
+
from omnibase_infra.models import ModelNodeIdentity
|
|
217
219
|
from omnibase_infra.models.discovery import (
|
|
218
220
|
ModelDiscoveredCapabilities,
|
|
219
221
|
ModelIntrospectionConfig,
|
|
@@ -422,6 +424,7 @@ class MixinNodeIntrospection:
|
|
|
422
424
|
config = ModelIntrospectionConfig(
|
|
423
425
|
node_id=node_id,
|
|
424
426
|
node_type=EnumNodeKind.EFFECT,
|
|
427
|
+
node_name="postgres_adapter",
|
|
425
428
|
event_bus=adapter_config.event_bus,
|
|
426
429
|
)
|
|
427
430
|
self.initialize_introspection(config)
|
|
@@ -463,6 +466,9 @@ class MixinNodeIntrospection:
|
|
|
463
466
|
_introspection_node_type: EnumNodeKind | None
|
|
464
467
|
_introspection_event_bus: ProtocolEventBus | None
|
|
465
468
|
_introspection_version: str
|
|
469
|
+
_introspection_node_name: str
|
|
470
|
+
_introspection_env: str
|
|
471
|
+
_introspection_service: str
|
|
466
472
|
_introspection_start_time: float | None
|
|
467
473
|
_introspection_contract: ModelContractBase | None
|
|
468
474
|
|
|
@@ -590,6 +596,7 @@ class MixinNodeIntrospection:
|
|
|
590
596
|
config = ModelIntrospectionConfig(
|
|
591
597
|
node_id=node_config.node_id,
|
|
592
598
|
node_type=EnumNodeKind.EFFECT,
|
|
599
|
+
node_name="my_node",
|
|
593
600
|
event_bus=node_config.event_bus,
|
|
594
601
|
version="1.2.0",
|
|
595
602
|
)
|
|
@@ -601,6 +608,7 @@ class MixinNodeIntrospection:
|
|
|
601
608
|
config = ModelIntrospectionConfig(
|
|
602
609
|
node_id=node_config.node_id,
|
|
603
610
|
node_type=EnumNodeKind.EFFECT,
|
|
611
|
+
node_name="my_effect_node",
|
|
604
612
|
event_bus=node_config.event_bus,
|
|
605
613
|
operation_keywords=frozenset({"fetch", "upload", "download"}),
|
|
606
614
|
)
|
|
@@ -638,6 +646,9 @@ class MixinNodeIntrospection:
|
|
|
638
646
|
)
|
|
639
647
|
self._introspection_event_bus = config.event_bus
|
|
640
648
|
self._introspection_version = config.version
|
|
649
|
+
self._introspection_node_name = config.node_name
|
|
650
|
+
self._introspection_env = config.env
|
|
651
|
+
self._introspection_service = config.service
|
|
641
652
|
self._introspection_cache_ttl = config.cache_ttl
|
|
642
653
|
|
|
643
654
|
# Capability discovery configuration - frozensets are immutable, no copy needed
|
|
@@ -984,38 +995,40 @@ class MixinNodeIntrospection:
|
|
|
984
995
|
) -> ModelNodeEventBusConfig | None:
|
|
985
996
|
"""Extract and resolve event_bus config from contract.
|
|
986
997
|
|
|
987
|
-
Extracts topic
|
|
988
|
-
|
|
998
|
+
Extracts topic names from the contract's event_bus subcontract.
|
|
999
|
+
Topics are realm-agnostic in ONEX - environment/realm is enforced via
|
|
1000
|
+
envelope identity, not topic naming.
|
|
989
1001
|
|
|
990
|
-
Topic
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1002
|
+
Topic Format:
|
|
1003
|
+
Topics follow ONEX naming convention:
|
|
1004
|
+
onex.{kind}.{producer}.{event-name}.v{n}
|
|
1005
|
+
|
|
1006
|
+
Example: "onex.evt.intent-classified.v1"
|
|
994
1007
|
|
|
995
1008
|
Args:
|
|
996
|
-
env_prefix: Environment prefix (
|
|
997
|
-
|
|
1009
|
+
env_prefix: Environment prefix (retained for compatibility, but no longer
|
|
1010
|
+
used for topic resolution as topics are realm-agnostic).
|
|
998
1011
|
|
|
999
1012
|
Returns:
|
|
1000
|
-
Resolved event bus config with
|
|
1013
|
+
Resolved event bus config with topic strings, or None if:
|
|
1001
1014
|
- No contract is configured (_introspection_contract is None)
|
|
1002
1015
|
- Contract has no event_bus subcontract
|
|
1003
1016
|
- event_bus subcontract has no publish_topics or subscribe_topics
|
|
1004
1017
|
|
|
1005
1018
|
Raises:
|
|
1006
|
-
ValueError: If topic
|
|
1007
|
-
(e.g., "{env}" or "{namespace}" remaining in the
|
|
1019
|
+
ValueError: If topic validation fails due to unresolved placeholders
|
|
1020
|
+
(e.g., "{env}" or "{namespace}" remaining in the topic).
|
|
1008
1021
|
This is a fail-fast mechanism to prevent misconfigured topics
|
|
1009
1022
|
from being published to the registry.
|
|
1010
1023
|
|
|
1011
1024
|
Example:
|
|
1012
1025
|
>>> config = self._extract_event_bus_config("dev")
|
|
1013
1026
|
>>> config.publish_topic_strings
|
|
1014
|
-
['
|
|
1027
|
+
['onex.evt.node-registered.v1']
|
|
1015
1028
|
|
|
1016
1029
|
See Also:
|
|
1017
|
-
- ModelEventBusSubcontract: Contract model with
|
|
1018
|
-
- ModelNodeEventBusConfig: Registry storage model
|
|
1030
|
+
- ModelEventBusSubcontract: Contract model with topics
|
|
1031
|
+
- ModelNodeEventBusConfig: Registry storage model
|
|
1019
1032
|
"""
|
|
1020
1033
|
if self._introspection_contract is None:
|
|
1021
1034
|
return None
|
|
@@ -1037,8 +1050,12 @@ class MixinNodeIntrospection:
|
|
|
1037
1050
|
return None
|
|
1038
1051
|
|
|
1039
1052
|
def resolve_topic(suffix: str) -> str:
|
|
1040
|
-
"""Resolve topic suffix to
|
|
1041
|
-
|
|
1053
|
+
"""Resolve topic suffix to topic name (realm-agnostic).
|
|
1054
|
+
|
|
1055
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
1056
|
+
envelope identity, not topic naming. This enables cross-environment event
|
|
1057
|
+
routing when needed while maintaining proper isolation through identity.
|
|
1058
|
+
"""
|
|
1042
1059
|
# Strip whitespace from suffix to handle YAML formatting artifacts
|
|
1043
1060
|
suffix = suffix.strip()
|
|
1044
1061
|
|
|
@@ -1065,9 +1082,8 @@ class MixinNodeIntrospection:
|
|
|
1065
1082
|
parameter="topic_suffix",
|
|
1066
1083
|
)
|
|
1067
1084
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return full_topic
|
|
1085
|
+
# Return topic unchanged - topics are realm-agnostic
|
|
1086
|
+
return suffix
|
|
1071
1087
|
|
|
1072
1088
|
def build_entry(suffix: str) -> ModelEventBusTopicEntry:
|
|
1073
1089
|
"""Build topic entry from suffix."""
|
|
@@ -2081,9 +2097,15 @@ class MixinNodeIntrospection:
|
|
|
2081
2097
|
return False
|
|
2082
2098
|
|
|
2083
2099
|
request_topic = self._request_introspection_topic
|
|
2100
|
+
node_identity = ModelNodeIdentity(
|
|
2101
|
+
env=self._introspection_env,
|
|
2102
|
+
service=self._introspection_service,
|
|
2103
|
+
node_name=self._introspection_node_name,
|
|
2104
|
+
version=self._introspection_version,
|
|
2105
|
+
)
|
|
2084
2106
|
unsubscribe = await event_bus.subscribe(
|
|
2085
2107
|
topic=request_topic,
|
|
2086
|
-
|
|
2108
|
+
node_identity=node_identity,
|
|
2087
2109
|
on_message=self._handle_introspection_request,
|
|
2088
2110
|
)
|
|
2089
2111
|
self._registry_unsubscribe = unsubscribe
|
|
@@ -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"]
|