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
|
@@ -35,12 +35,16 @@ from omnibase_infra.nodes.node_service_discovery_effect.models.enum_health_statu
|
|
|
35
35
|
)
|
|
36
36
|
from omnibase_infra.services.registry_api.models import (
|
|
37
37
|
ModelCapabilityWidgetMapping,
|
|
38
|
+
ModelContractRef,
|
|
39
|
+
ModelContractView,
|
|
38
40
|
ModelPaginationInfo,
|
|
39
41
|
ModelRegistryDiscoveryResponse,
|
|
40
42
|
ModelRegistryHealthResponse,
|
|
41
43
|
ModelRegistryInstanceView,
|
|
42
44
|
ModelRegistryNodeView,
|
|
43
45
|
ModelRegistrySummary,
|
|
46
|
+
ModelTopicSummary,
|
|
47
|
+
ModelTopicView,
|
|
44
48
|
ModelWarning,
|
|
45
49
|
ModelWidgetDefaults,
|
|
46
50
|
ModelWidgetMapping,
|
|
@@ -49,7 +53,10 @@ from omnibase_infra.services.registry_api.models import (
|
|
|
49
53
|
if TYPE_CHECKING:
|
|
50
54
|
from omnibase_infra.handlers.service_discovery import HandlerServiceDiscoveryConsul
|
|
51
55
|
from omnibase_infra.models.projection import ModelRegistrationProjection
|
|
52
|
-
from omnibase_infra.projectors import
|
|
56
|
+
from omnibase_infra.projectors import (
|
|
57
|
+
ProjectionReaderContract,
|
|
58
|
+
ProjectionReaderRegistration,
|
|
59
|
+
)
|
|
53
60
|
|
|
54
61
|
logger = logging.getLogger(__name__)
|
|
55
62
|
|
|
@@ -112,6 +119,7 @@ class ServiceRegistryDiscovery:
|
|
|
112
119
|
container: ModelONEXContainer,
|
|
113
120
|
projection_reader: ProjectionReaderRegistration | None = None,
|
|
114
121
|
consul_handler: HandlerServiceDiscoveryConsul | None = None,
|
|
122
|
+
contract_reader: ProjectionReaderContract | None = None,
|
|
115
123
|
widget_mapping_path: Path | None = None,
|
|
116
124
|
) -> None:
|
|
117
125
|
"""Initialize the registry discovery service.
|
|
@@ -123,6 +131,8 @@ class ServiceRegistryDiscovery:
|
|
|
123
131
|
If not provided, node queries will return empty results with warnings.
|
|
124
132
|
consul_handler: Optional Consul handler for live instances.
|
|
125
133
|
If not provided, instance queries will return empty results with warnings.
|
|
134
|
+
contract_reader: Optional projection reader for contract registry.
|
|
135
|
+
If not provided, contract/topic queries will return empty results with warnings.
|
|
126
136
|
widget_mapping_path: Path to widget mapping YAML file.
|
|
127
137
|
Defaults to configs/widget_mapping.yaml relative to package.
|
|
128
138
|
"""
|
|
@@ -142,6 +152,9 @@ class ServiceRegistryDiscovery:
|
|
|
142
152
|
# via the consul_handler parameter instead.
|
|
143
153
|
self._consul_handler = consul_handler
|
|
144
154
|
|
|
155
|
+
# Contract reader for contract registry queries
|
|
156
|
+
self._contract_reader = contract_reader
|
|
157
|
+
|
|
145
158
|
self._widget_mapping_path = widget_mapping_path or DEFAULT_WIDGET_MAPPING_PATH
|
|
146
159
|
self._widget_mapping_cache: ModelWidgetMapping | None = None
|
|
147
160
|
self._widget_mapping_mtime: float | None = None
|
|
@@ -151,6 +164,7 @@ class ServiceRegistryDiscovery:
|
|
|
151
164
|
extra={
|
|
152
165
|
"has_projection_reader": self._projection_reader is not None,
|
|
153
166
|
"has_consul_handler": self._consul_handler is not None,
|
|
167
|
+
"has_contract_reader": self._contract_reader is not None,
|
|
154
168
|
"widget_mapping_path": str(self._widget_mapping_path),
|
|
155
169
|
},
|
|
156
170
|
)
|
|
@@ -170,6 +184,11 @@ class ServiceRegistryDiscovery:
|
|
|
170
184
|
"""Get the Consul handler for lifecycle management."""
|
|
171
185
|
return self._consul_handler
|
|
172
186
|
|
|
187
|
+
@property
|
|
188
|
+
def has_contract_reader(self) -> bool:
|
|
189
|
+
"""Check if contract reader is configured."""
|
|
190
|
+
return self._contract_reader is not None
|
|
191
|
+
|
|
173
192
|
def invalidate_widget_mapping_cache(self) -> None:
|
|
174
193
|
"""Clear widget mapping cache, forcing reload on next access.
|
|
175
194
|
|
|
@@ -833,5 +852,513 @@ class ServiceRegistryDiscovery:
|
|
|
833
852
|
version="1.0.0",
|
|
834
853
|
)
|
|
835
854
|
|
|
855
|
+
# ============================================================
|
|
856
|
+
# Contract Registry Methods
|
|
857
|
+
# ============================================================
|
|
858
|
+
|
|
859
|
+
async def list_contracts(
|
|
860
|
+
self,
|
|
861
|
+
limit: int = 100,
|
|
862
|
+
offset: int = 0,
|
|
863
|
+
active_only: bool = True,
|
|
864
|
+
node_name: str | None = None,
|
|
865
|
+
correlation_id: UUID | None = None,
|
|
866
|
+
) -> tuple[list[ModelContractView], ModelPaginationInfo, list[ModelWarning]]:
|
|
867
|
+
"""List contracts from projection reader.
|
|
868
|
+
|
|
869
|
+
Retrieves registered contracts with optional filtering by node name
|
|
870
|
+
and active status. Supports pagination.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
limit: Maximum number of contracts to return (1-1000).
|
|
874
|
+
offset: Number of contracts to skip for pagination.
|
|
875
|
+
active_only: If True, return only active contracts.
|
|
876
|
+
node_name: Optional filter by node name.
|
|
877
|
+
correlation_id: Optional correlation ID for tracing.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Tuple of (contracts, pagination_info, warnings).
|
|
881
|
+
"""
|
|
882
|
+
correlation_id = correlation_id or uuid4()
|
|
883
|
+
warnings: list[ModelWarning] = []
|
|
884
|
+
contracts: list[ModelContractView] = []
|
|
885
|
+
total = 0
|
|
886
|
+
|
|
887
|
+
if self._contract_reader is None:
|
|
888
|
+
warnings.append(
|
|
889
|
+
ModelWarning(
|
|
890
|
+
source="postgres",
|
|
891
|
+
message="Contract reader not configured",
|
|
892
|
+
code="NO_CONTRACT_READER",
|
|
893
|
+
timestamp=datetime.now(UTC),
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
else:
|
|
897
|
+
try:
|
|
898
|
+
# Fetch contracts based on filters
|
|
899
|
+
if node_name:
|
|
900
|
+
# Filter by node name
|
|
901
|
+
projections = (
|
|
902
|
+
await self._contract_reader.list_contracts_by_node_name(
|
|
903
|
+
node_name=node_name,
|
|
904
|
+
include_inactive=not active_only,
|
|
905
|
+
correlation_id=correlation_id,
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
elif active_only:
|
|
909
|
+
# Active contracts only
|
|
910
|
+
projections = await self._contract_reader.list_active_contracts(
|
|
911
|
+
limit=limit + offset + 1, # Fetch extra to detect has_more
|
|
912
|
+
offset=0,
|
|
913
|
+
correlation_id=correlation_id,
|
|
914
|
+
)
|
|
915
|
+
else:
|
|
916
|
+
# All contracts (active + inactive)
|
|
917
|
+
# Note: The reader doesn't have a single method for all contracts,
|
|
918
|
+
# so we query active and then could extend for inactive if needed.
|
|
919
|
+
# For now, we use list_active_contracts as a fallback.
|
|
920
|
+
projections = await self._contract_reader.list_active_contracts(
|
|
921
|
+
limit=limit + offset + 1,
|
|
922
|
+
offset=0,
|
|
923
|
+
correlation_id=correlation_id,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
# Calculate total and apply pagination
|
|
927
|
+
total = len(projections)
|
|
928
|
+
|
|
929
|
+
# Apply pagination in-memory for node_name filter
|
|
930
|
+
# (the reader's list_by_node_name doesn't support offset/limit)
|
|
931
|
+
if node_name:
|
|
932
|
+
projections_slice = projections[offset : offset + limit]
|
|
933
|
+
else:
|
|
934
|
+
# Already paginated by the reader
|
|
935
|
+
projections_slice = projections[offset : offset + limit]
|
|
936
|
+
total = max(total, offset + len(projections_slice))
|
|
937
|
+
|
|
938
|
+
# Batch fetch topics for all contracts to avoid N+1 query pattern
|
|
939
|
+
# This uses a single query instead of O(N) queries
|
|
940
|
+
contract_ids = [proj.contract_id for proj in projections_slice]
|
|
941
|
+
topics_by_contract: dict[str, list] = {}
|
|
942
|
+
|
|
943
|
+
if contract_ids:
|
|
944
|
+
try:
|
|
945
|
+
topics_by_contract = (
|
|
946
|
+
await self._contract_reader.get_topics_for_contracts(
|
|
947
|
+
contract_ids=contract_ids,
|
|
948
|
+
correlation_id=correlation_id,
|
|
949
|
+
)
|
|
950
|
+
)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
# Log but continue - partial success with empty topics
|
|
953
|
+
logger.warning(
|
|
954
|
+
"Failed to batch fetch topics for contracts",
|
|
955
|
+
extra={
|
|
956
|
+
"contract_count": len(contract_ids),
|
|
957
|
+
"error": str(e),
|
|
958
|
+
"correlation_id": str(correlation_id),
|
|
959
|
+
},
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
# Convert to view models
|
|
963
|
+
for proj in projections_slice:
|
|
964
|
+
# Get topics for this contract from batch result
|
|
965
|
+
topics_published: list[str] = []
|
|
966
|
+
topics_subscribed: list[str] = []
|
|
967
|
+
|
|
968
|
+
contract_topics = topics_by_contract.get(proj.contract_id, [])
|
|
969
|
+
for topic in contract_topics:
|
|
970
|
+
if topic.direction == "publish":
|
|
971
|
+
topics_published.append(topic.topic_suffix)
|
|
972
|
+
elif topic.direction == "subscribe":
|
|
973
|
+
topics_subscribed.append(topic.topic_suffix)
|
|
974
|
+
|
|
975
|
+
contracts.append(
|
|
976
|
+
ModelContractView(
|
|
977
|
+
contract_id=proj.contract_id,
|
|
978
|
+
node_name=proj.node_name,
|
|
979
|
+
version=f"{proj.version_major}.{proj.version_minor}.{proj.version_patch}",
|
|
980
|
+
contract_hash=proj.contract_hash,
|
|
981
|
+
is_active=proj.is_active,
|
|
982
|
+
registered_at=proj.registered_at,
|
|
983
|
+
last_seen_at=proj.last_seen_at,
|
|
984
|
+
deregistered_at=proj.deregistered_at,
|
|
985
|
+
topics_published=topics_published,
|
|
986
|
+
topics_subscribed=topics_subscribed,
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
except Exception as e:
|
|
991
|
+
logger.exception(
|
|
992
|
+
"Failed to query contracts",
|
|
993
|
+
extra={"correlation_id": str(correlation_id)},
|
|
994
|
+
)
|
|
995
|
+
warnings.append(
|
|
996
|
+
ModelWarning(
|
|
997
|
+
source="postgres",
|
|
998
|
+
message=f"Failed to query contracts: {type(e).__name__}",
|
|
999
|
+
code="CONTRACT_QUERY_FAILED",
|
|
1000
|
+
timestamp=datetime.now(UTC),
|
|
1001
|
+
)
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
pagination = ModelPaginationInfo(
|
|
1005
|
+
total=total,
|
|
1006
|
+
limit=limit,
|
|
1007
|
+
offset=offset,
|
|
1008
|
+
has_more=offset + len(contracts) < total,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
return contracts, pagination, warnings
|
|
1012
|
+
|
|
1013
|
+
async def get_contract(
|
|
1014
|
+
self,
|
|
1015
|
+
contract_id: str,
|
|
1016
|
+
correlation_id: UUID | None = None,
|
|
1017
|
+
) -> tuple[ModelContractView | None, list[ModelWarning]]:
|
|
1018
|
+
"""Get contract detail with topic references.
|
|
1019
|
+
|
|
1020
|
+
Retrieves a single contract by ID along with its published and
|
|
1021
|
+
subscribed topics.
|
|
1022
|
+
|
|
1023
|
+
Args:
|
|
1024
|
+
contract_id: Contract ID (e.g., "my-node:1.0.0")
|
|
1025
|
+
correlation_id: Optional correlation ID for tracing.
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
Tuple of (contract or None, warnings).
|
|
1029
|
+
"""
|
|
1030
|
+
correlation_id = correlation_id or uuid4()
|
|
1031
|
+
warnings: list[ModelWarning] = []
|
|
1032
|
+
|
|
1033
|
+
if self._contract_reader is None:
|
|
1034
|
+
warnings.append(
|
|
1035
|
+
ModelWarning(
|
|
1036
|
+
source="postgres",
|
|
1037
|
+
message="Contract reader not configured",
|
|
1038
|
+
code="NO_CONTRACT_READER",
|
|
1039
|
+
timestamp=datetime.now(UTC),
|
|
1040
|
+
)
|
|
1041
|
+
)
|
|
1042
|
+
return None, warnings
|
|
1043
|
+
|
|
1044
|
+
try:
|
|
1045
|
+
proj = await self._contract_reader.get_contract_by_id(
|
|
1046
|
+
contract_id=contract_id,
|
|
1047
|
+
correlation_id=correlation_id,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
if proj is None:
|
|
1051
|
+
return None, warnings
|
|
1052
|
+
|
|
1053
|
+
# Get topics for this contract
|
|
1054
|
+
topics_published: list[str] = []
|
|
1055
|
+
topics_subscribed: list[str] = []
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
topics = await self._contract_reader.get_topics_by_contract(
|
|
1059
|
+
contract_id=contract_id,
|
|
1060
|
+
correlation_id=correlation_id,
|
|
1061
|
+
)
|
|
1062
|
+
for topic in topics:
|
|
1063
|
+
if topic.direction == "publish":
|
|
1064
|
+
topics_published.append(topic.topic_suffix)
|
|
1065
|
+
elif topic.direction == "subscribe":
|
|
1066
|
+
topics_subscribed.append(topic.topic_suffix)
|
|
1067
|
+
except Exception as e:
|
|
1068
|
+
logger.warning(
|
|
1069
|
+
"Failed to get topics for contract",
|
|
1070
|
+
extra={
|
|
1071
|
+
"contract_id": contract_id,
|
|
1072
|
+
"error": str(e),
|
|
1073
|
+
"correlation_id": str(correlation_id),
|
|
1074
|
+
},
|
|
1075
|
+
)
|
|
1076
|
+
warnings.append(
|
|
1077
|
+
ModelWarning(
|
|
1078
|
+
source="postgres",
|
|
1079
|
+
message=f"Failed to get topics: {type(e).__name__}",
|
|
1080
|
+
code="TOPIC_QUERY_FAILED",
|
|
1081
|
+
timestamp=datetime.now(UTC),
|
|
1082
|
+
)
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
contract = ModelContractView(
|
|
1086
|
+
contract_id=proj.contract_id,
|
|
1087
|
+
node_name=proj.node_name,
|
|
1088
|
+
version=f"{proj.version_major}.{proj.version_minor}.{proj.version_patch}",
|
|
1089
|
+
contract_hash=proj.contract_hash,
|
|
1090
|
+
is_active=proj.is_active,
|
|
1091
|
+
registered_at=proj.registered_at,
|
|
1092
|
+
last_seen_at=proj.last_seen_at,
|
|
1093
|
+
deregistered_at=proj.deregistered_at,
|
|
1094
|
+
topics_published=topics_published,
|
|
1095
|
+
topics_subscribed=topics_subscribed,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
return contract, warnings
|
|
1099
|
+
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
logger.exception(
|
|
1102
|
+
"Failed to get contract",
|
|
1103
|
+
extra={
|
|
1104
|
+
"contract_id": contract_id,
|
|
1105
|
+
"correlation_id": str(correlation_id),
|
|
1106
|
+
},
|
|
1107
|
+
)
|
|
1108
|
+
warnings.append(
|
|
1109
|
+
ModelWarning(
|
|
1110
|
+
source="postgres",
|
|
1111
|
+
message=f"Failed to get contract: {type(e).__name__}",
|
|
1112
|
+
code="CONTRACT_QUERY_FAILED",
|
|
1113
|
+
timestamp=datetime.now(UTC),
|
|
1114
|
+
)
|
|
1115
|
+
)
|
|
1116
|
+
return None, warnings
|
|
1117
|
+
|
|
1118
|
+
async def list_topics(
|
|
1119
|
+
self,
|
|
1120
|
+
direction: str | None = None,
|
|
1121
|
+
limit: int = 100,
|
|
1122
|
+
offset: int = 0,
|
|
1123
|
+
correlation_id: UUID | None = None,
|
|
1124
|
+
) -> tuple[list[ModelTopicSummary], ModelPaginationInfo, list[ModelWarning]]:
|
|
1125
|
+
"""List topics from projection reader.
|
|
1126
|
+
|
|
1127
|
+
Retrieves topics with optional filtering by direction (publish/subscribe).
|
|
1128
|
+
Returns summary view with contract counts.
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
direction: Optional filter by direction ('publish' or 'subscribe').
|
|
1132
|
+
limit: Maximum number of topics to return (1-1000).
|
|
1133
|
+
offset: Number of topics to skip for pagination.
|
|
1134
|
+
correlation_id: Optional correlation ID for tracing.
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
Tuple of (topics, pagination_info, warnings).
|
|
1138
|
+
"""
|
|
1139
|
+
correlation_id = correlation_id or uuid4()
|
|
1140
|
+
warnings: list[ModelWarning] = []
|
|
1141
|
+
topics: list[ModelTopicSummary] = []
|
|
1142
|
+
total = 0
|
|
1143
|
+
|
|
1144
|
+
if self._contract_reader is None:
|
|
1145
|
+
warnings.append(
|
|
1146
|
+
ModelWarning(
|
|
1147
|
+
source="postgres",
|
|
1148
|
+
message="Contract reader not configured",
|
|
1149
|
+
code="NO_CONTRACT_READER",
|
|
1150
|
+
timestamp=datetime.now(UTC),
|
|
1151
|
+
)
|
|
1152
|
+
)
|
|
1153
|
+
else:
|
|
1154
|
+
try:
|
|
1155
|
+
projections = await self._contract_reader.list_topics(
|
|
1156
|
+
direction=direction,
|
|
1157
|
+
limit=limit + 1, # Fetch extra to detect has_more
|
|
1158
|
+
offset=offset,
|
|
1159
|
+
correlation_id=correlation_id,
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
# Detect has_more from fetching limit+1
|
|
1163
|
+
has_more = len(projections) > limit
|
|
1164
|
+
projections_slice = projections[:limit]
|
|
1165
|
+
|
|
1166
|
+
# Get accurate total count for pagination
|
|
1167
|
+
try:
|
|
1168
|
+
total = await self._contract_reader.count_topics(
|
|
1169
|
+
direction=direction,
|
|
1170
|
+
correlation_id=correlation_id,
|
|
1171
|
+
)
|
|
1172
|
+
except Exception as count_error:
|
|
1173
|
+
# Fall back to estimate if count query fails
|
|
1174
|
+
logger.warning(
|
|
1175
|
+
"Failed to get accurate topic count, using estimate",
|
|
1176
|
+
extra={
|
|
1177
|
+
"correlation_id": str(correlation_id),
|
|
1178
|
+
"error": str(count_error),
|
|
1179
|
+
},
|
|
1180
|
+
)
|
|
1181
|
+
total = offset + len(projections)
|
|
1182
|
+
|
|
1183
|
+
for proj in projections_slice:
|
|
1184
|
+
topics.append(
|
|
1185
|
+
ModelTopicSummary(
|
|
1186
|
+
topic_suffix=proj.topic_suffix,
|
|
1187
|
+
direction=proj.direction,
|
|
1188
|
+
contract_count=len(proj.contract_ids),
|
|
1189
|
+
last_seen_at=proj.last_seen_at,
|
|
1190
|
+
is_active=proj.is_active,
|
|
1191
|
+
)
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
except Exception as e:
|
|
1195
|
+
logger.exception(
|
|
1196
|
+
"Failed to query topics",
|
|
1197
|
+
extra={"correlation_id": str(correlation_id)},
|
|
1198
|
+
)
|
|
1199
|
+
warnings.append(
|
|
1200
|
+
ModelWarning(
|
|
1201
|
+
source="postgres",
|
|
1202
|
+
message=f"Failed to query topics: {type(e).__name__}",
|
|
1203
|
+
code="TOPIC_QUERY_FAILED",
|
|
1204
|
+
timestamp=datetime.now(UTC),
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
pagination = ModelPaginationInfo(
|
|
1209
|
+
total=total,
|
|
1210
|
+
limit=limit,
|
|
1211
|
+
offset=offset,
|
|
1212
|
+
has_more=offset + len(topics) < total,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
return topics, pagination, warnings
|
|
1216
|
+
|
|
1217
|
+
async def get_topic_detail(
|
|
1218
|
+
self,
|
|
1219
|
+
topic_suffix: str,
|
|
1220
|
+
correlation_id: UUID | None = None,
|
|
1221
|
+
) -> tuple[ModelTopicView | None, list[ModelWarning]]:
|
|
1222
|
+
"""Get topic detail with publisher/subscriber contracts.
|
|
1223
|
+
|
|
1224
|
+
Retrieves a topic by suffix, combining both publish and subscribe
|
|
1225
|
+
directions into a unified view with contract references.
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
topic_suffix: Topic suffix (without environment prefix)
|
|
1229
|
+
correlation_id: Optional correlation ID for tracing.
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
Tuple of (topic or None, warnings).
|
|
1233
|
+
"""
|
|
1234
|
+
correlation_id = correlation_id or uuid4()
|
|
1235
|
+
warnings: list[ModelWarning] = []
|
|
1236
|
+
|
|
1237
|
+
if self._contract_reader is None:
|
|
1238
|
+
warnings.append(
|
|
1239
|
+
ModelWarning(
|
|
1240
|
+
source="postgres",
|
|
1241
|
+
message="Contract reader not configured",
|
|
1242
|
+
code="NO_CONTRACT_READER",
|
|
1243
|
+
timestamp=datetime.now(UTC),
|
|
1244
|
+
)
|
|
1245
|
+
)
|
|
1246
|
+
return None, warnings
|
|
1247
|
+
|
|
1248
|
+
try:
|
|
1249
|
+
# Get both directions for this topic
|
|
1250
|
+
publish_topic = await self._contract_reader.get_topic(
|
|
1251
|
+
topic_suffix=topic_suffix,
|
|
1252
|
+
direction="publish",
|
|
1253
|
+
correlation_id=correlation_id,
|
|
1254
|
+
)
|
|
1255
|
+
subscribe_topic = await self._contract_reader.get_topic(
|
|
1256
|
+
topic_suffix=topic_suffix,
|
|
1257
|
+
direction="subscribe",
|
|
1258
|
+
correlation_id=correlation_id,
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
# If neither direction exists, topic not found
|
|
1262
|
+
if publish_topic is None and subscribe_topic is None:
|
|
1263
|
+
return None, warnings
|
|
1264
|
+
|
|
1265
|
+
# Get contract details for publishers
|
|
1266
|
+
publishers: list[ModelContractRef] = []
|
|
1267
|
+
if publish_topic:
|
|
1268
|
+
for contract_id in publish_topic.contract_ids:
|
|
1269
|
+
try:
|
|
1270
|
+
contract = await self._contract_reader.get_contract_by_id(
|
|
1271
|
+
contract_id=contract_id,
|
|
1272
|
+
correlation_id=correlation_id,
|
|
1273
|
+
)
|
|
1274
|
+
if contract:
|
|
1275
|
+
publishers.append(
|
|
1276
|
+
ModelContractRef(
|
|
1277
|
+
contract_id=contract.contract_id,
|
|
1278
|
+
node_name=contract.node_name,
|
|
1279
|
+
version=f"{contract.version_major}.{contract.version_minor}.{contract.version_patch}",
|
|
1280
|
+
)
|
|
1281
|
+
)
|
|
1282
|
+
except Exception as e:
|
|
1283
|
+
logger.warning(
|
|
1284
|
+
"Failed to get publisher contract",
|
|
1285
|
+
extra={
|
|
1286
|
+
"contract_id": contract_id,
|
|
1287
|
+
"error": str(e),
|
|
1288
|
+
"correlation_id": str(correlation_id),
|
|
1289
|
+
},
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
# Get contract details for subscribers
|
|
1293
|
+
subscribers: list[ModelContractRef] = []
|
|
1294
|
+
if subscribe_topic:
|
|
1295
|
+
for contract_id in subscribe_topic.contract_ids:
|
|
1296
|
+
try:
|
|
1297
|
+
contract = await self._contract_reader.get_contract_by_id(
|
|
1298
|
+
contract_id=contract_id,
|
|
1299
|
+
correlation_id=correlation_id,
|
|
1300
|
+
)
|
|
1301
|
+
if contract:
|
|
1302
|
+
subscribers.append(
|
|
1303
|
+
ModelContractRef(
|
|
1304
|
+
contract_id=contract.contract_id,
|
|
1305
|
+
node_name=contract.node_name,
|
|
1306
|
+
version=f"{contract.version_major}.{contract.version_minor}.{contract.version_patch}",
|
|
1307
|
+
)
|
|
1308
|
+
)
|
|
1309
|
+
except Exception as e:
|
|
1310
|
+
logger.warning(
|
|
1311
|
+
"Failed to get subscriber contract",
|
|
1312
|
+
extra={
|
|
1313
|
+
"contract_id": contract_id,
|
|
1314
|
+
"error": str(e),
|
|
1315
|
+
"correlation_id": str(correlation_id),
|
|
1316
|
+
},
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# Combine timestamps from both directions
|
|
1320
|
+
first_seen_at = min(
|
|
1321
|
+
t.first_seen_at
|
|
1322
|
+
for t in [publish_topic, subscribe_topic]
|
|
1323
|
+
if t is not None
|
|
1324
|
+
)
|
|
1325
|
+
last_seen_at = max(
|
|
1326
|
+
t.last_seen_at
|
|
1327
|
+
for t in [publish_topic, subscribe_topic]
|
|
1328
|
+
if t is not None
|
|
1329
|
+
)
|
|
1330
|
+
is_active = any(
|
|
1331
|
+
t.is_active for t in [publish_topic, subscribe_topic] if t is not None
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
topic = ModelTopicView(
|
|
1335
|
+
topic_suffix=topic_suffix,
|
|
1336
|
+
publishers=publishers,
|
|
1337
|
+
subscribers=subscribers,
|
|
1338
|
+
first_seen_at=first_seen_at,
|
|
1339
|
+
last_seen_at=last_seen_at,
|
|
1340
|
+
is_active=is_active,
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
return topic, warnings
|
|
1344
|
+
|
|
1345
|
+
except Exception as e:
|
|
1346
|
+
logger.exception(
|
|
1347
|
+
"Failed to get topic",
|
|
1348
|
+
extra={
|
|
1349
|
+
"topic_suffix": topic_suffix,
|
|
1350
|
+
"correlation_id": str(correlation_id),
|
|
1351
|
+
},
|
|
1352
|
+
)
|
|
1353
|
+
warnings.append(
|
|
1354
|
+
ModelWarning(
|
|
1355
|
+
source="postgres",
|
|
1356
|
+
message=f"Failed to get topic: {type(e).__name__}",
|
|
1357
|
+
code="TOPIC_QUERY_FAILED",
|
|
1358
|
+
timestamp=datetime.now(UTC),
|
|
1359
|
+
)
|
|
1360
|
+
)
|
|
1361
|
+
return None, warnings
|
|
1362
|
+
|
|
836
1363
|
|
|
837
1364
|
__all__ = ["ServiceRegistryDiscovery"]
|
omnibase_infra/utils/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ This package provides common utilities used across the infrastructure:
|
|
|
7
7
|
- util_atomic_file: Atomic file write primitives using temp-file-rename pattern
|
|
8
8
|
- util_consumer_group: Kafka consumer group ID generation with deterministic hashing
|
|
9
9
|
- util_datetime: Datetime validation and timezone normalization
|
|
10
|
+
- util_db_error_context: Database operation error handling context manager
|
|
10
11
|
- util_db_transaction: Database transaction context manager for asyncpg
|
|
11
12
|
- util_dsn_validation: PostgreSQL DSN validation and sanitization
|
|
12
13
|
- util_env_parsing: Type-safe environment variable parsing with validation
|
|
@@ -38,6 +39,10 @@ from omnibase_infra.utils.util_datetime import (
|
|
|
38
39
|
validate_timezone_aware_with_context,
|
|
39
40
|
warn_if_naive_datetime,
|
|
40
41
|
)
|
|
42
|
+
|
|
43
|
+
# Note: util_db_error_context is NOT imported here to avoid circular imports.
|
|
44
|
+
# Import directly: from omnibase_infra.utils.util_db_error_context import db_operation_error_context
|
|
45
|
+
# See: omnibase_infra.errors -> util_error_sanitization -> utils.__init__ -> util_db_error_context -> errors
|
|
41
46
|
from omnibase_infra.utils.util_db_transaction import (
|
|
42
47
|
transaction_context,
|
|
43
48
|
)
|
|
@@ -80,6 +85,8 @@ __all__: list[str] = [
|
|
|
80
85
|
"CorrelationContext",
|
|
81
86
|
"KAFKA_CONSUMER_GROUP_MAX_LENGTH",
|
|
82
87
|
"OptimisticConflictError",
|
|
88
|
+
# Note: ProtocolCircuitBreakerFailureRecorder and db_operation_error_context are NOT exported
|
|
89
|
+
# here to avoid circular imports. Import directly from util_db_error_context.
|
|
83
90
|
"SAFE_ERROR_PATTERNS",
|
|
84
91
|
"SEMVER_PATTERN",
|
|
85
92
|
"SENSITIVE_PATTERNS",
|