omnibase_infra 0.2.1__py3-none-any.whl → 0.2.3__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/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
- omnibase_infra/enums/__init__.py +6 -0
- omnibase_infra/enums/enum_handler_error_type.py +10 -0
- omnibase_infra/enums/enum_handler_source_mode.py +72 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/event_bus/event_bus_kafka.py +1 -1
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
- omnibase_infra/handlers/__init__.py +8 -1
- omnibase_infra/handlers/handler_consul.py +7 -1
- omnibase_infra/handlers/handler_db.py +10 -3
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_http.py +8 -2
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_mcp.py +745 -63
- omnibase_infra/handlers/handler_vault.py +11 -5
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +42 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +11 -0
- omnibase_infra/models/handlers/__init__.py +48 -5
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
- omnibase_infra/models/runtime/model_handler_contract.py +25 -9
- omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/__init__.py +2 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +90 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +267 -186
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +19 -2
- omnibase_infra/runtime/handler_registry.py +11 -3
- omnibase_infra/runtime/handler_source_resolver.py +326 -0
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +11 -3
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +589 -106
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +17 -4
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +21 -0
- omnibase_infra/services/corpus_capture.py +7 -1
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +20 -3
- omnibase_infra/services/service_timeout_scanner.py +7 -3
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +120 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/utils/__init__.py +19 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +140 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -27,6 +27,7 @@ from uuid import uuid4
|
|
|
27
27
|
|
|
28
28
|
import hvac
|
|
29
29
|
|
|
30
|
+
from omnibase_core.container import ModelONEXContainer
|
|
30
31
|
from omnibase_core.models.dispatch import ModelHandlerOutput
|
|
31
32
|
from omnibase_infra.enums import (
|
|
32
33
|
EnumHandlerType,
|
|
@@ -122,13 +123,18 @@ class HandlerVault(
|
|
|
122
123
|
- MixinVaultToken: Token management and renewal
|
|
123
124
|
"""
|
|
124
125
|
|
|
125
|
-
def __init__(self) -> None:
|
|
126
|
-
"""Initialize HandlerVault
|
|
126
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
127
|
+
"""Initialize HandlerVault with ONEX container for dependency injection.
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
Args:
|
|
130
|
+
container: ONEX container for dependency injection.
|
|
131
|
+
|
|
132
|
+
Note:
|
|
133
|
+
Circuit breaker is initialized during initialize() call when
|
|
134
|
+
configuration is available. The mixin's _init_circuit_breaker() method
|
|
135
|
+
is called there with the actual config values.
|
|
131
136
|
"""
|
|
137
|
+
self._container = container
|
|
132
138
|
self._client: hvac.Client | None = None
|
|
133
139
|
self._config: ModelVaultHandlerConfig | None = None
|
|
134
140
|
self._initialized: bool = False
|
|
@@ -94,7 +94,7 @@ class MixinConsulKV:
|
|
|
94
94
|
correlation_id: UUID,
|
|
95
95
|
) -> T:
|
|
96
96
|
"""Execute operation with retry logic - provided by host class."""
|
|
97
|
-
raise NotImplementedError("Must be provided by implementing class")
|
|
97
|
+
raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
|
|
98
98
|
|
|
99
99
|
def _build_response(
|
|
100
100
|
self,
|
|
@@ -103,6 +103,7 @@ class MixinConsulKV:
|
|
|
103
103
|
input_envelope_id: UUID,
|
|
104
104
|
) -> ModelHandlerOutput[ModelConsulHandlerResponse]:
|
|
105
105
|
"""Build standardized response - provided by host class."""
|
|
106
|
+
raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
|
|
106
107
|
|
|
107
108
|
async def _kv_get(
|
|
108
109
|
self,
|
|
@@ -176,7 +177,7 @@ class MixinConsulKV:
|
|
|
176
177
|
get_func,
|
|
177
178
|
correlation_id,
|
|
178
179
|
)
|
|
179
|
-
index, data = cast(KVGetResult, result)
|
|
180
|
+
index, data = cast("KVGetResult", result)
|
|
180
181
|
|
|
181
182
|
# Handle response - data can be None if key doesn't exist
|
|
182
183
|
if data is None:
|
|
@@ -325,7 +326,7 @@ class MixinConsulKV:
|
|
|
325
326
|
put_func,
|
|
326
327
|
correlation_id,
|
|
327
328
|
)
|
|
328
|
-
success = cast(bool, result)
|
|
329
|
+
success = cast("bool", result)
|
|
329
330
|
|
|
330
331
|
typed_payload = ModelConsulKVPutPayload(
|
|
331
332
|
success=success,
|
|
@@ -91,7 +91,7 @@ class MixinConsulService:
|
|
|
91
91
|
correlation_id: UUID,
|
|
92
92
|
) -> T:
|
|
93
93
|
"""Execute operation with retry logic - provided by host class."""
|
|
94
|
-
raise NotImplementedError("Must be provided by implementing class")
|
|
94
|
+
raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
|
|
95
95
|
|
|
96
96
|
def _build_response(
|
|
97
97
|
self,
|
|
@@ -100,6 +100,7 @@ class MixinConsulService:
|
|
|
100
100
|
input_envelope_id: UUID,
|
|
101
101
|
) -> ModelHandlerOutput[ModelConsulHandlerResponse]:
|
|
102
102
|
"""Build standardized response - provided by host class."""
|
|
103
|
+
raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
|
|
103
104
|
|
|
104
105
|
async def _register_service(
|
|
105
106
|
self,
|
|
@@ -48,6 +48,7 @@ from uuid import UUID, uuid4
|
|
|
48
48
|
# Import asyncpg at module level to avoid redundant imports inside methods
|
|
49
49
|
import asyncpg
|
|
50
50
|
|
|
51
|
+
from omnibase_core.container import ModelONEXContainer
|
|
51
52
|
from omnibase_core.enums.enum_node_kind import EnumNodeKind
|
|
52
53
|
from omnibase_infra.enums import EnumInfraTransportType
|
|
53
54
|
from omnibase_infra.errors import (
|
|
@@ -152,7 +153,10 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
152
153
|
handler_type: Returns "postgresql" identifier.
|
|
153
154
|
|
|
154
155
|
Example:
|
|
156
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
157
|
+
>>> container = ModelONEXContainer(...)
|
|
155
158
|
>>> handler = HandlerRegistrationStoragePostgres(
|
|
159
|
+
... container=container,
|
|
156
160
|
... postgres_adapter=postgres_adapter,
|
|
157
161
|
... circuit_breaker_config={"threshold": 5, "reset_timeout": 30.0},
|
|
158
162
|
... )
|
|
@@ -161,6 +165,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
161
165
|
|
|
162
166
|
def __init__(
|
|
163
167
|
self,
|
|
168
|
+
container: ModelONEXContainer,
|
|
164
169
|
postgres_adapter: ProtocolPostgresAdapter | None = None,
|
|
165
170
|
dsn: str | None = None,
|
|
166
171
|
host: str = "localhost",
|
|
@@ -178,6 +183,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
178
183
|
"""Initialize HandlerRegistrationStoragePostgres.
|
|
179
184
|
|
|
180
185
|
Args:
|
|
186
|
+
container: ONEX dependency injection container (required).
|
|
181
187
|
postgres_adapter: Optional existing PostgreSQL adapter (ProtocolPostgresAdapter).
|
|
182
188
|
If not provided, a new asyncpg connection pool will be created.
|
|
183
189
|
dsn: Optional PostgreSQL connection DSN (overrides host/port/etc).
|
|
@@ -197,6 +203,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
197
203
|
table on first connection. Default is False. Production deployments
|
|
198
204
|
should use database migrations instead of auto-creation.
|
|
199
205
|
"""
|
|
206
|
+
self._container = container
|
|
200
207
|
# Normalize circuit breaker config to ModelCircuitBreakerConfig
|
|
201
208
|
if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
|
|
202
209
|
cb_config = circuit_breaker_config
|
|
@@ -30,6 +30,7 @@ from uuid import NAMESPACE_DNS, UUID, uuid4, uuid5
|
|
|
30
30
|
|
|
31
31
|
import consul
|
|
32
32
|
|
|
33
|
+
from omnibase_core.container import ModelONEXContainer
|
|
33
34
|
from omnibase_infra.enums import EnumInfraTransportType
|
|
34
35
|
from omnibase_infra.errors import (
|
|
35
36
|
InfraConnectionError,
|
|
@@ -91,7 +92,10 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
91
92
|
handler_type: Returns "consul" identifier.
|
|
92
93
|
|
|
93
94
|
Example:
|
|
95
|
+
>>> from unittest.mock import MagicMock
|
|
96
|
+
>>> container = MagicMock(spec=ModelONEXContainer)
|
|
94
97
|
>>> handler = HandlerServiceDiscoveryConsul(
|
|
98
|
+
... container=container,
|
|
95
99
|
... consul_client=consul_client,
|
|
96
100
|
... circuit_breaker_config=ModelCircuitBreakerConfig(threshold=5),
|
|
97
101
|
... )
|
|
@@ -100,6 +104,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
100
104
|
|
|
101
105
|
def __init__(
|
|
102
106
|
self,
|
|
107
|
+
container: ModelONEXContainer,
|
|
103
108
|
consul_client: ProtocolConsulClient | None = None,
|
|
104
109
|
consul_host: str = "localhost",
|
|
105
110
|
consul_port: int = 8500,
|
|
@@ -114,6 +119,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
114
119
|
"""Initialize HandlerServiceDiscoveryConsul.
|
|
115
120
|
|
|
116
121
|
Args:
|
|
122
|
+
container: ONEX container for dependency injection and service resolution.
|
|
117
123
|
consul_client: Optional existing Consul client (ProtocolConsulClient).
|
|
118
124
|
If not provided, a new python-consul client will be created.
|
|
119
125
|
consul_host: Consul server hostname (default: "localhost").
|
|
@@ -129,6 +135,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
129
135
|
max_workers: Thread pool max workers (default: 10).
|
|
130
136
|
timeout_seconds: Operation timeout in seconds (default: 30.0).
|
|
131
137
|
"""
|
|
138
|
+
self._container = container
|
|
132
139
|
# Parse circuit breaker configuration using ModelCircuitBreakerConfig
|
|
133
140
|
if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
|
|
134
141
|
cb_config = circuit_breaker_config
|
|
@@ -567,15 +574,15 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
567
574
|
service_infos: list[ModelServiceInfo] = []
|
|
568
575
|
for svc in services:
|
|
569
576
|
# Cast nested dicts for type safety
|
|
570
|
-
svc_data = cast(dict[str, object], svc.get("Service", {}))
|
|
571
|
-
node_data = cast(dict[str, object], svc.get("Node", {}))
|
|
577
|
+
svc_data = cast("dict[str, object]", svc.get("Service", {}))
|
|
578
|
+
node_data = cast("dict[str, object]", svc.get("Node", {}))
|
|
572
579
|
svc_id_str = str(svc_data.get("ID", ""))
|
|
573
580
|
svc_name = svc_data.get("Service", "")
|
|
574
581
|
address = svc_data.get("Address", "") or node_data.get("Address", "")
|
|
575
582
|
port_raw = svc_data.get("Port", 0)
|
|
576
583
|
port = int(port_raw) if isinstance(port_raw, int | float | str) else 0
|
|
577
|
-
svc_tags = cast(list[str], svc_data.get("Tags", []))
|
|
578
|
-
svc_meta = cast(dict[str, str], svc_data.get("Meta", {}))
|
|
584
|
+
svc_tags = cast("list[str]", svc_data.get("Tags", []))
|
|
585
|
+
svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
|
|
579
586
|
|
|
580
587
|
if svc_id_str and svc_name and address and port:
|
|
581
588
|
# Convert Consul service ID string to UUID
|
|
@@ -664,6 +671,303 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
664
671
|
context=context,
|
|
665
672
|
) from e
|
|
666
673
|
|
|
674
|
+
async def list_all_services(
|
|
675
|
+
self,
|
|
676
|
+
tag_filter: str | None = None,
|
|
677
|
+
correlation_id: UUID | None = None,
|
|
678
|
+
) -> dict[str, list[str]]:
|
|
679
|
+
"""List all registered service names with their tags.
|
|
680
|
+
|
|
681
|
+
Uses Consul catalog API (/v1/catalog/services) to retrieve all
|
|
682
|
+
service names registered in the Consul catalog, along with their tags.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
tag_filter: Optional tag to filter services by. If provided,
|
|
686
|
+
only services with this tag will be returned.
|
|
687
|
+
correlation_id: Optional correlation ID for tracing.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Dictionary mapping service names to their list of tags.
|
|
691
|
+
Example: {"my-service": ["web", "api"], "other-service": ["db"]}
|
|
692
|
+
|
|
693
|
+
Raises:
|
|
694
|
+
InfraConnectionError: If connection to Consul fails.
|
|
695
|
+
InfraTimeoutError: If operation times out.
|
|
696
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
697
|
+
"""
|
|
698
|
+
correlation_id = correlation_id or uuid4()
|
|
699
|
+
start_time = time.monotonic()
|
|
700
|
+
|
|
701
|
+
# Guard against use-after-shutdown
|
|
702
|
+
self._check_not_shutdown("list_all_services", correlation_id)
|
|
703
|
+
|
|
704
|
+
# Check circuit breaker
|
|
705
|
+
async with self._circuit_breaker_lock:
|
|
706
|
+
await self._check_circuit_breaker(
|
|
707
|
+
operation="list_all_services",
|
|
708
|
+
correlation_id=correlation_id,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
# Query Consul catalog for all services
|
|
713
|
+
client = self._consul_client
|
|
714
|
+
executor = await self._ensure_executor()
|
|
715
|
+
loop = asyncio.get_running_loop()
|
|
716
|
+
|
|
717
|
+
def _query_catalog() -> tuple[int, dict[str, list[str]]]:
|
|
718
|
+
# NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
|
|
719
|
+
# catalog.services exists on Optional[Consul] union type.
|
|
720
|
+
result: tuple[int, dict[str, list[str]]] = client.catalog.services() # type: ignore[union-attr] # NOTE: duck-typed client
|
|
721
|
+
return result
|
|
722
|
+
|
|
723
|
+
_, services = await asyncio.wait_for(
|
|
724
|
+
loop.run_in_executor(executor, _query_catalog),
|
|
725
|
+
timeout=self._timeout_seconds,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# Reset circuit breaker on success
|
|
729
|
+
async with self._circuit_breaker_lock:
|
|
730
|
+
await self._reset_circuit_breaker()
|
|
731
|
+
|
|
732
|
+
# Apply tag filter if provided
|
|
733
|
+
if tag_filter is not None:
|
|
734
|
+
services = {
|
|
735
|
+
name: tags for name, tags in services.items() if tag_filter in tags
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
duration_ms = (time.monotonic() - start_time) * 1000
|
|
739
|
+
|
|
740
|
+
logger.info(
|
|
741
|
+
"Listed all services from Consul catalog",
|
|
742
|
+
extra={
|
|
743
|
+
"service_count": len(services),
|
|
744
|
+
"tag_filter": tag_filter,
|
|
745
|
+
"duration_ms": duration_ms,
|
|
746
|
+
"correlation_id": str(correlation_id),
|
|
747
|
+
},
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
return services
|
|
751
|
+
|
|
752
|
+
except TimeoutError as e:
|
|
753
|
+
async with self._circuit_breaker_lock:
|
|
754
|
+
await self._record_circuit_failure(
|
|
755
|
+
operation="list_all_services",
|
|
756
|
+
correlation_id=correlation_id,
|
|
757
|
+
)
|
|
758
|
+
raise InfraTimeoutError(
|
|
759
|
+
f"Consul catalog query timed out after {self._timeout_seconds}s",
|
|
760
|
+
context=ModelTimeoutErrorContext(
|
|
761
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
762
|
+
operation="list_all_services",
|
|
763
|
+
target_name="consul.discovery",
|
|
764
|
+
correlation_id=correlation_id,
|
|
765
|
+
timeout_seconds=self._timeout_seconds,
|
|
766
|
+
),
|
|
767
|
+
) from e
|
|
768
|
+
|
|
769
|
+
except consul.ConsulException as e:
|
|
770
|
+
async with self._circuit_breaker_lock:
|
|
771
|
+
await self._record_circuit_failure(
|
|
772
|
+
operation="list_all_services",
|
|
773
|
+
correlation_id=correlation_id,
|
|
774
|
+
)
|
|
775
|
+
context = ModelInfraErrorContext(
|
|
776
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
777
|
+
operation="list_all_services",
|
|
778
|
+
target_name="consul.discovery",
|
|
779
|
+
correlation_id=correlation_id,
|
|
780
|
+
)
|
|
781
|
+
raise InfraConnectionError(
|
|
782
|
+
"Consul catalog query failed",
|
|
783
|
+
context=context,
|
|
784
|
+
) from e
|
|
785
|
+
|
|
786
|
+
async def get_all_service_instances(
|
|
787
|
+
self,
|
|
788
|
+
service_name: str,
|
|
789
|
+
include_unhealthy: bool = True,
|
|
790
|
+
correlation_id: UUID | None = None,
|
|
791
|
+
) -> list[ModelServiceInfo]:
|
|
792
|
+
"""Get all instances of a service, optionally including unhealthy ones.
|
|
793
|
+
|
|
794
|
+
Uses Consul health API (/v1/health/service/<name>) to retrieve all
|
|
795
|
+
instances of a service with their health status information.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
service_name: Name of the service to query.
|
|
799
|
+
include_unhealthy: If True, return all instances regardless of health.
|
|
800
|
+
If False, only return healthy (passing) instances.
|
|
801
|
+
correlation_id: Optional correlation ID for tracing.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
List of ModelServiceInfo objects representing service instances,
|
|
805
|
+
including health status, health output, and last check timestamp.
|
|
806
|
+
|
|
807
|
+
Raises:
|
|
808
|
+
InfraConnectionError: If connection to Consul fails.
|
|
809
|
+
InfraTimeoutError: If operation times out.
|
|
810
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
811
|
+
"""
|
|
812
|
+
correlation_id = correlation_id or uuid4()
|
|
813
|
+
start_time = time.monotonic()
|
|
814
|
+
|
|
815
|
+
# Guard against use-after-shutdown
|
|
816
|
+
self._check_not_shutdown("get_all_service_instances", correlation_id)
|
|
817
|
+
|
|
818
|
+
# Check circuit breaker
|
|
819
|
+
async with self._circuit_breaker_lock:
|
|
820
|
+
await self._check_circuit_breaker(
|
|
821
|
+
operation="get_all_service_instances",
|
|
822
|
+
correlation_id=correlation_id,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
try:
|
|
826
|
+
# Query Consul health API for service instances
|
|
827
|
+
client = self._consul_client
|
|
828
|
+
executor = await self._ensure_executor()
|
|
829
|
+
loop = asyncio.get_running_loop()
|
|
830
|
+
|
|
831
|
+
def _query_health() -> tuple[int, list[dict[str, object]]]:
|
|
832
|
+
# NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
|
|
833
|
+
# health.service exists on Optional[Consul] union type.
|
|
834
|
+
# When passing=False, we get ALL instances
|
|
835
|
+
# When passing=True, we only get healthy instances
|
|
836
|
+
result: tuple[int, list[dict[str, object]]] = client.health.service( # type: ignore[union-attr] # NOTE: duck-typed client
|
|
837
|
+
service_name,
|
|
838
|
+
passing=not include_unhealthy,
|
|
839
|
+
)
|
|
840
|
+
return result
|
|
841
|
+
|
|
842
|
+
_, services = await asyncio.wait_for(
|
|
843
|
+
loop.run_in_executor(executor, _query_health),
|
|
844
|
+
timeout=self._timeout_seconds,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
# Reset circuit breaker on success
|
|
848
|
+
async with self._circuit_breaker_lock:
|
|
849
|
+
await self._reset_circuit_breaker()
|
|
850
|
+
|
|
851
|
+
# Convert to ModelServiceInfo list with full health details
|
|
852
|
+
service_infos: list[ModelServiceInfo] = []
|
|
853
|
+
for svc in services:
|
|
854
|
+
# Cast nested dicts for type safety
|
|
855
|
+
svc_data = cast("dict[str, object]", svc.get("Service", {}))
|
|
856
|
+
node_data = cast("dict[str, object]", svc.get("Node", {}))
|
|
857
|
+
checks = cast("list[dict[str, object]]", svc.get("Checks", []))
|
|
858
|
+
|
|
859
|
+
svc_id_str = str(svc_data.get("ID", ""))
|
|
860
|
+
svc_name = svc_data.get("Service", "")
|
|
861
|
+
address = svc_data.get("Address", "") or node_data.get("Address", "")
|
|
862
|
+
port_raw = svc_data.get("Port", 0)
|
|
863
|
+
port = int(port_raw) if isinstance(port_raw, int | float | str) else 0
|
|
864
|
+
svc_tags = cast("list[str]", svc_data.get("Tags", []))
|
|
865
|
+
svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
|
|
866
|
+
|
|
867
|
+
if svc_id_str and svc_name and address and port:
|
|
868
|
+
# Convert Consul service ID string to UUID
|
|
869
|
+
try:
|
|
870
|
+
svc_id = UUID(svc_id_str)
|
|
871
|
+
except ValueError:
|
|
872
|
+
# Non-UUID service ID from Consul - generate deterministic UUID5
|
|
873
|
+
logger.warning(
|
|
874
|
+
"Non-UUID service ID from Consul, using deterministic UUID5 conversion",
|
|
875
|
+
extra={
|
|
876
|
+
"original_id": svc_id_str,
|
|
877
|
+
"correlation_id": str(correlation_id),
|
|
878
|
+
},
|
|
879
|
+
)
|
|
880
|
+
svc_id = uuid5(NAMESPACE_ONEX_SERVICE, svc_id_str)
|
|
881
|
+
|
|
882
|
+
# Determine health status from checks
|
|
883
|
+
health_status = EnumHealthStatus.UNKNOWN
|
|
884
|
+
health_output: str | None = None
|
|
885
|
+
last_check_at: datetime | None = None
|
|
886
|
+
|
|
887
|
+
# Find the service-specific health check
|
|
888
|
+
for check in checks:
|
|
889
|
+
check_service_id = check.get("ServiceID", "")
|
|
890
|
+
if check_service_id == svc_id_str or not check_service_id:
|
|
891
|
+
status_str = str(check.get("Status", "")).lower()
|
|
892
|
+
if status_str == "passing":
|
|
893
|
+
health_status = EnumHealthStatus.HEALTHY
|
|
894
|
+
elif status_str in ("critical", "warning"):
|
|
895
|
+
health_status = EnumHealthStatus.UNHEALTHY
|
|
896
|
+
else:
|
|
897
|
+
health_status = EnumHealthStatus.UNKNOWN
|
|
898
|
+
|
|
899
|
+
health_output = str(check.get("Output", "")) or None
|
|
900
|
+
|
|
901
|
+
# Parse CreateIndex as a proxy for last check time
|
|
902
|
+
# (Consul doesn't directly expose last check time in this API)
|
|
903
|
+
# In production, you might use the check's CreateIndex or ModifyIndex
|
|
904
|
+
break
|
|
905
|
+
|
|
906
|
+
service_infos.append(
|
|
907
|
+
ModelServiceInfo(
|
|
908
|
+
service_id=svc_id,
|
|
909
|
+
service_name=str(svc_name),
|
|
910
|
+
address=str(address),
|
|
911
|
+
port=port,
|
|
912
|
+
tags=tuple(svc_tags or []),
|
|
913
|
+
health_status=health_status,
|
|
914
|
+
health_output=health_output,
|
|
915
|
+
last_check_at=last_check_at,
|
|
916
|
+
metadata=svc_meta or {},
|
|
917
|
+
registered_at=datetime.now(UTC),
|
|
918
|
+
correlation_id=correlation_id,
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
duration_ms = (time.monotonic() - start_time) * 1000
|
|
923
|
+
|
|
924
|
+
logger.info(
|
|
925
|
+
"Retrieved service instances from Consul",
|
|
926
|
+
extra={
|
|
927
|
+
"service_name": service_name,
|
|
928
|
+
"instance_count": len(service_infos),
|
|
929
|
+
"include_unhealthy": include_unhealthy,
|
|
930
|
+
"duration_ms": duration_ms,
|
|
931
|
+
"correlation_id": str(correlation_id),
|
|
932
|
+
},
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
return service_infos
|
|
936
|
+
|
|
937
|
+
except TimeoutError as e:
|
|
938
|
+
async with self._circuit_breaker_lock:
|
|
939
|
+
await self._record_circuit_failure(
|
|
940
|
+
operation="get_all_service_instances",
|
|
941
|
+
correlation_id=correlation_id,
|
|
942
|
+
)
|
|
943
|
+
raise InfraTimeoutError(
|
|
944
|
+
f"Consul health query timed out after {self._timeout_seconds}s",
|
|
945
|
+
context=ModelTimeoutErrorContext(
|
|
946
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
947
|
+
operation="get_all_service_instances",
|
|
948
|
+
target_name="consul.discovery",
|
|
949
|
+
correlation_id=correlation_id,
|
|
950
|
+
timeout_seconds=self._timeout_seconds,
|
|
951
|
+
),
|
|
952
|
+
) from e
|
|
953
|
+
|
|
954
|
+
except consul.ConsulException as e:
|
|
955
|
+
async with self._circuit_breaker_lock:
|
|
956
|
+
await self._record_circuit_failure(
|
|
957
|
+
operation="get_all_service_instances",
|
|
958
|
+
correlation_id=correlation_id,
|
|
959
|
+
)
|
|
960
|
+
context = ModelInfraErrorContext(
|
|
961
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
962
|
+
operation="get_all_service_instances",
|
|
963
|
+
target_name="consul.discovery",
|
|
964
|
+
correlation_id=correlation_id,
|
|
965
|
+
)
|
|
966
|
+
raise InfraConnectionError(
|
|
967
|
+
"Consul health query failed",
|
|
968
|
+
context=context,
|
|
969
|
+
) from e
|
|
970
|
+
|
|
667
971
|
async def health_check(
|
|
668
972
|
self,
|
|
669
973
|
correlation_id: UUID | None = None,
|
|
@@ -45,6 +45,8 @@ class ModelServiceInfo(BaseModel):
|
|
|
45
45
|
health_status: Current health status of the service.
|
|
46
46
|
metadata: Additional key-value metadata for the service.
|
|
47
47
|
health_check_url: Optional URL for health checks (handler extension).
|
|
48
|
+
health_output: Output message from the last health check (handler extension).
|
|
49
|
+
last_check_at: Timestamp of the last health check (handler extension).
|
|
48
50
|
registered_at: Timestamp when the service was registered (handler extension).
|
|
49
51
|
correlation_id: Correlation ID for tracing (handler extension).
|
|
50
52
|
"""
|
|
@@ -86,6 +88,14 @@ class ModelServiceInfo(BaseModel):
|
|
|
86
88
|
default=None,
|
|
87
89
|
description="Optional URL for health checks",
|
|
88
90
|
)
|
|
91
|
+
health_output: str | None = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Output message from the last health check",
|
|
94
|
+
)
|
|
95
|
+
last_check_at: datetime | None = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="Timestamp of the last health check",
|
|
98
|
+
)
|
|
89
99
|
registered_at: datetime | None = Field(
|
|
90
100
|
default=None,
|
|
91
101
|
description="Timestamp when the service was registered",
|
|
@@ -101,6 +101,7 @@ import logging
|
|
|
101
101
|
import time
|
|
102
102
|
from uuid import UUID, uuid4
|
|
103
103
|
|
|
104
|
+
from omnibase_core.types import JsonType
|
|
104
105
|
from omnibase_infra.enums import EnumCircuitState, EnumInfraTransportType
|
|
105
106
|
from omnibase_infra.errors import (
|
|
106
107
|
InfraUnavailableError,
|
|
@@ -580,7 +581,7 @@ class MixinAsyncCircuitBreaker:
|
|
|
580
581
|
self._circuit_breaker_failures = 0
|
|
581
582
|
self._circuit_breaker_open_until = 0.0
|
|
582
583
|
|
|
583
|
-
def _get_circuit_breaker_state(self) -> dict[str,
|
|
584
|
+
def _get_circuit_breaker_state(self) -> dict[str, JsonType]:
|
|
584
585
|
"""Return current circuit breaker state for introspection.
|
|
585
586
|
|
|
586
587
|
This method encapsulates circuit breaker internals for safe access
|
|
@@ -638,7 +639,7 @@ class MixinAsyncCircuitBreaker:
|
|
|
638
639
|
cb_state = "closed"
|
|
639
640
|
seconds_until_half_open = None
|
|
640
641
|
|
|
641
|
-
result: dict[str,
|
|
642
|
+
result: dict[str, JsonType] = {
|
|
642
643
|
"initialized": cb_initialized,
|
|
643
644
|
"state": cb_state,
|
|
644
645
|
"failures": cb_failures,
|