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
|
@@ -243,6 +243,9 @@ from omnibase_infra.runtime.event_bus_subcontract_wiring import (
|
|
|
243
243
|
load_event_bus_subcontract,
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
+
# Request-response wiring (OMN-1742)
|
|
247
|
+
from omnibase_infra.runtime.request_response_wiring import RequestResponseWiring
|
|
248
|
+
|
|
246
249
|
# Runtime contract config loader (OMN-1519)
|
|
247
250
|
from omnibase_infra.runtime.runtime_contract_config_loader import (
|
|
248
251
|
RuntimeContractConfigLoader,
|
|
@@ -279,6 +282,11 @@ from omnibase_infra.runtime.baseline_subscriptions import (
|
|
|
279
282
|
get_baseline_topics,
|
|
280
283
|
)
|
|
281
284
|
|
|
285
|
+
# Contract dependency resolver (OMN-1732)
|
|
286
|
+
from omnibase_infra.runtime.contract_dependency_resolver import (
|
|
287
|
+
ContractDependencyResolver,
|
|
288
|
+
)
|
|
289
|
+
|
|
282
290
|
# Chain-aware dispatch (OMN-951) - must be imported LAST to avoid circular import
|
|
283
291
|
from omnibase_infra.runtime.chain_aware_dispatch import (
|
|
284
292
|
ChainAwareDispatcher,
|
|
@@ -429,6 +437,8 @@ __all__: list[str] = [
|
|
|
429
437
|
# Event bus subcontract wiring (OMN-1621)
|
|
430
438
|
"EventBusSubcontractWiring",
|
|
431
439
|
"load_event_bus_subcontract",
|
|
440
|
+
# Request-response wiring (OMN-1742)
|
|
441
|
+
"RequestResponseWiring",
|
|
432
442
|
# Runtime contract config loader (OMN-1519)
|
|
433
443
|
"RuntimeContractConfigLoader",
|
|
434
444
|
# Security constants and configuration (OMN-1519)
|
|
@@ -453,4 +463,6 @@ __all__: list[str] = [
|
|
|
453
463
|
"BASELINE_CONTRACT_TOPICS",
|
|
454
464
|
"BASELINE_PLATFORM_TOPICS",
|
|
455
465
|
"get_baseline_topics",
|
|
466
|
+
# Contract dependency resolver (OMN-1732)
|
|
467
|
+
"ContractDependencyResolver",
|
|
456
468
|
]
|
|
@@ -72,10 +72,13 @@ This subset excludes heartbeat topics and is appropriate when:
|
|
|
72
72
|
- Heartbeat processing is handled separately
|
|
73
73
|
- You want to minimize subscription overhead
|
|
74
74
|
|
|
75
|
+
Note:
|
|
76
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
77
|
+
envelope identity, not topic naming. Subscribe directly to the topic suffix.
|
|
78
|
+
|
|
75
79
|
Example:
|
|
76
80
|
>>> for topic_suffix in BASELINE_CONTRACT_TOPICS:
|
|
77
|
-
...
|
|
78
|
-
... subscribe(full_topic)
|
|
81
|
+
... subscribe(topic_suffix) # No environment prefix needed
|
|
79
82
|
"""
|
|
80
83
|
|
|
81
84
|
# All platform baseline topics including heartbeat.
|
|
@@ -91,10 +94,13 @@ Includes:
|
|
|
91
94
|
- Contract deregistered events
|
|
92
95
|
- Node heartbeat events
|
|
93
96
|
|
|
97
|
+
Note:
|
|
98
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
99
|
+
envelope identity, not topic naming. Subscribe directly to the topic suffix.
|
|
100
|
+
|
|
94
101
|
Example:
|
|
95
102
|
>>> for topic_suffix in BASELINE_PLATFORM_TOPICS:
|
|
96
|
-
...
|
|
97
|
-
... subscribe(full_topic)
|
|
103
|
+
... subscribe(topic_suffix) # No environment prefix needed
|
|
98
104
|
"""
|
|
99
105
|
|
|
100
106
|
|
|
@@ -111,8 +117,9 @@ def get_baseline_topics(*, include_heartbeat: bool = True) -> frozenset[str]:
|
|
|
111
117
|
registration/deregistration topics.
|
|
112
118
|
|
|
113
119
|
Returns:
|
|
114
|
-
A frozenset of topic suffix strings.
|
|
115
|
-
|
|
120
|
+
A frozenset of topic suffix strings. Topics are realm-agnostic in ONEX;
|
|
121
|
+
subscribe directly to these suffixes without environment prefix.
|
|
122
|
+
The environment/realm is enforced via envelope identity, not topic naming.
|
|
116
123
|
|
|
117
124
|
Example:
|
|
118
125
|
>>> # For full platform observability
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Contract dependency resolver for ONEX nodes.
|
|
4
|
+
|
|
5
|
+
This module provides ContractDependencyResolver, which reads protocol
|
|
6
|
+
dependencies from a node's contract.yaml and resolves them from the
|
|
7
|
+
container's service_registry.
|
|
8
|
+
|
|
9
|
+
Part of OMN-1732: Runtime dependency injection for zero-code nodes.
|
|
10
|
+
|
|
11
|
+
Architecture:
|
|
12
|
+
- Container: service provider (owns protocol instances)
|
|
13
|
+
- Runtime (this resolver): wiring authority (resolves + validates)
|
|
14
|
+
- Node: consumer (receives resolved deps via constructor)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
28
|
+
from omnibase_infra.errors import (
|
|
29
|
+
ModelInfraErrorContext,
|
|
30
|
+
ProtocolConfigurationError,
|
|
31
|
+
ProtocolDependencyResolutionError,
|
|
32
|
+
)
|
|
33
|
+
from omnibase_infra.models.runtime.model_resolved_dependencies import (
|
|
34
|
+
ModelResolvedDependencies,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from omnibase_core.container import ModelONEXContainer
|
|
39
|
+
from omnibase_core.models.contracts import ModelContractBase
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ContractDependencyResolver:
|
|
45
|
+
"""Resolves protocol dependencies from contracts using container.
|
|
46
|
+
|
|
47
|
+
Reads the `dependencies` field from a node's contract, filters for
|
|
48
|
+
protocol-type dependencies, imports the protocol classes, and resolves
|
|
49
|
+
instances from the container's service_registry.
|
|
50
|
+
|
|
51
|
+
This resolver implements the "fail-fast" principle: if any required
|
|
52
|
+
protocol cannot be resolved, it raises ProtocolDependencyResolutionError
|
|
53
|
+
immediately rather than allowing node creation with missing dependencies.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
57
|
+
>>>
|
|
58
|
+
>>> container = ModelONEXContainer()
|
|
59
|
+
>>> # ... register protocols in container ...
|
|
60
|
+
>>>
|
|
61
|
+
>>> resolver = ContractDependencyResolver(container)
|
|
62
|
+
>>> resolved = await resolver.resolve(node_contract)
|
|
63
|
+
>>>
|
|
64
|
+
>>> # Use resolved dependencies for node creation
|
|
65
|
+
>>> node = NodeRegistry.create(container, dependencies=resolved)
|
|
66
|
+
|
|
67
|
+
.. versionadded:: 0.x.x
|
|
68
|
+
Part of OMN-1732 runtime dependency injection.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
72
|
+
"""Initialize the resolver with a container.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
container: ONEX container with service_registry for protocol resolution.
|
|
76
|
+
"""
|
|
77
|
+
self._container = container
|
|
78
|
+
|
|
79
|
+
async def resolve(
|
|
80
|
+
self,
|
|
81
|
+
contract: ModelContractBase,
|
|
82
|
+
*,
|
|
83
|
+
allow_missing: bool = False,
|
|
84
|
+
) -> ModelResolvedDependencies:
|
|
85
|
+
"""Resolve protocol dependencies from a contract.
|
|
86
|
+
|
|
87
|
+
Reads contract.dependencies, filters for protocol-type entries,
|
|
88
|
+
and resolves each from the container's service_registry.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
contract: The node contract containing dependency declarations.
|
|
92
|
+
allow_missing: If True, skip missing protocols instead of raising.
|
|
93
|
+
Default False (fail-fast behavior).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ModelResolvedDependencies containing resolved protocol instances.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ProtocolDependencyResolutionError: If any required protocol cannot
|
|
100
|
+
be resolved and allow_missing is False.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> resolved = await resolver.resolve(contract)
|
|
104
|
+
>>> adapter = resolved.get("ProtocolPostgresAdapter")
|
|
105
|
+
"""
|
|
106
|
+
# Extract protocol dependencies from contract
|
|
107
|
+
protocol_deps = self._extract_protocol_dependencies(contract)
|
|
108
|
+
|
|
109
|
+
if not protocol_deps:
|
|
110
|
+
logger.debug(
|
|
111
|
+
"No protocol dependencies declared in contract",
|
|
112
|
+
extra={"contract_name": getattr(contract, "name", "unknown")},
|
|
113
|
+
)
|
|
114
|
+
return ModelResolvedDependencies()
|
|
115
|
+
|
|
116
|
+
# Resolve each protocol from container
|
|
117
|
+
# ONEX_EXCLUDE: any_type - dict holds heterogeneous protocol instances resolved at runtime
|
|
118
|
+
resolved: dict[str, Any] = {}
|
|
119
|
+
missing: list[str] = []
|
|
120
|
+
resolution_errors: dict[str, str] = {}
|
|
121
|
+
|
|
122
|
+
for dep in protocol_deps:
|
|
123
|
+
class_name = dep.get("class_name")
|
|
124
|
+
module_path = dep.get("module")
|
|
125
|
+
|
|
126
|
+
if not class_name:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"Protocol dependency missing class_name, skipping",
|
|
129
|
+
extra={"dependency": dep},
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Import the protocol class
|
|
135
|
+
protocol_class = self._import_protocol_class(class_name, module_path)
|
|
136
|
+
|
|
137
|
+
# Resolve from container
|
|
138
|
+
instance = await self._resolve_from_container(protocol_class)
|
|
139
|
+
resolved[class_name] = instance
|
|
140
|
+
|
|
141
|
+
logger.debug(
|
|
142
|
+
"Resolved protocol dependency",
|
|
143
|
+
extra={
|
|
144
|
+
"protocol": class_name,
|
|
145
|
+
"module_path": module_path,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
error_msg = str(e)
|
|
151
|
+
resolution_errors[class_name] = error_msg
|
|
152
|
+
missing.append(class_name)
|
|
153
|
+
|
|
154
|
+
logger.warning(
|
|
155
|
+
"Failed to resolve protocol dependency",
|
|
156
|
+
extra={
|
|
157
|
+
"protocol": class_name,
|
|
158
|
+
"module_path": module_path,
|
|
159
|
+
"error": error_msg,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Fail-fast if any protocols are missing
|
|
164
|
+
if missing and not allow_missing:
|
|
165
|
+
contract_name = getattr(contract, "name", "unknown")
|
|
166
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
167
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
168
|
+
operation="resolve_dependencies",
|
|
169
|
+
target_name=contract_name,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
error_details = "\n".join(
|
|
173
|
+
f" - {proto}: {resolution_errors.get(proto, 'unknown error')}"
|
|
174
|
+
for proto in missing
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
raise ProtocolDependencyResolutionError(
|
|
178
|
+
f"Cannot create node '{contract_name}': missing required protocols.\n"
|
|
179
|
+
f"The following protocols are declared in contract.yaml but could not "
|
|
180
|
+
f"be resolved from the container:\n{error_details}\n\n"
|
|
181
|
+
f"Ensure these protocols are registered in the container before "
|
|
182
|
+
f"node creation via container.service_registry.register_instance().",
|
|
183
|
+
context=context,
|
|
184
|
+
missing_protocols=missing,
|
|
185
|
+
node_name=contract_name,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return ModelResolvedDependencies(protocols=resolved)
|
|
189
|
+
|
|
190
|
+
def _extract_protocol_dependencies(
|
|
191
|
+
self,
|
|
192
|
+
contract: ModelContractBase,
|
|
193
|
+
) -> list[dict[str, str]]:
|
|
194
|
+
"""Extract protocol-type dependencies from contract.
|
|
195
|
+
|
|
196
|
+
Supports multiple detection patterns:
|
|
197
|
+
1. `type` field = "protocol" (YAML contract style)
|
|
198
|
+
2. `is_protocol()` method returns True
|
|
199
|
+
3. `dependency_type` attribute with value "PROTOCOL"
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
contract: The contract to extract dependencies from.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of dependency dicts with keys: name, type, class_name, module
|
|
206
|
+
"""
|
|
207
|
+
dependencies: list[dict[str, str]] = []
|
|
208
|
+
|
|
209
|
+
# Get dependencies attribute if present
|
|
210
|
+
contract_deps = getattr(contract, "dependencies", None)
|
|
211
|
+
if not contract_deps:
|
|
212
|
+
return dependencies
|
|
213
|
+
|
|
214
|
+
for dep in contract_deps:
|
|
215
|
+
is_protocol = self._is_protocol_dependency(dep)
|
|
216
|
+
|
|
217
|
+
if is_protocol:
|
|
218
|
+
dep_dict: dict[str, str] = {}
|
|
219
|
+
|
|
220
|
+
# Extract fields - handle both attribute and dict access
|
|
221
|
+
if hasattr(dep, "name"):
|
|
222
|
+
dep_dict["name"] = str(dep.name) if dep.name else ""
|
|
223
|
+
if hasattr(dep, "class_name"):
|
|
224
|
+
dep_dict["class_name"] = (
|
|
225
|
+
str(dep.class_name) if dep.class_name else ""
|
|
226
|
+
)
|
|
227
|
+
if hasattr(dep, "module"):
|
|
228
|
+
dep_dict["module"] = str(dep.module) if dep.module else ""
|
|
229
|
+
|
|
230
|
+
# Only add if we have a class_name
|
|
231
|
+
if dep_dict.get("class_name"):
|
|
232
|
+
dependencies.append(dep_dict)
|
|
233
|
+
|
|
234
|
+
return dependencies
|
|
235
|
+
|
|
236
|
+
# ONEX_EXCLUDE: any_type - dep can be ModelContractDependency or dict from various sources
|
|
237
|
+
def _is_protocol_dependency(self, dep: Any) -> bool:
|
|
238
|
+
"""Check if a dependency is a protocol dependency.
|
|
239
|
+
|
|
240
|
+
Supports multiple detection patterns:
|
|
241
|
+
1. `type` field = "protocol" (YAML contract style)
|
|
242
|
+
2. `is_protocol()` method returns True
|
|
243
|
+
3. `dependency_type` attribute with value "PROTOCOL"
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
dep: The dependency object to check.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if this is a protocol dependency, False otherwise.
|
|
250
|
+
"""
|
|
251
|
+
# Check is_protocol() method first (most explicit)
|
|
252
|
+
if hasattr(dep, "is_protocol") and callable(dep.is_protocol):
|
|
253
|
+
if dep.is_protocol():
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
# Check dependency_type attribute (enum or string)
|
|
257
|
+
if hasattr(dep, "dependency_type"):
|
|
258
|
+
dep_type_val = dep.dependency_type
|
|
259
|
+
if hasattr(dep_type_val, "value"):
|
|
260
|
+
# Enum with .value
|
|
261
|
+
if str(dep_type_val.value).upper() == "PROTOCOL":
|
|
262
|
+
return True
|
|
263
|
+
elif str(dep_type_val).upper() == "PROTOCOL":
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
# Check type field (YAML contract style: type: "protocol")
|
|
267
|
+
if hasattr(dep, "type"):
|
|
268
|
+
type_val = getattr(dep, "type", "")
|
|
269
|
+
if str(type_val).lower() == "protocol":
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def _import_protocol_class(
|
|
275
|
+
self,
|
|
276
|
+
class_name: str,
|
|
277
|
+
module_path: str | None,
|
|
278
|
+
) -> type:
|
|
279
|
+
"""Import a protocol class by name and module.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
class_name: The class name to import
|
|
283
|
+
module_path: The module path (required for import)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The imported class type.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ImportError: If the class cannot be imported.
|
|
290
|
+
"""
|
|
291
|
+
if not module_path:
|
|
292
|
+
raise ImportError(
|
|
293
|
+
f"Protocol '{class_name}' has no module path specified in contract. "
|
|
294
|
+
f"Cannot import without module path."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
module = importlib.import_module(module_path)
|
|
299
|
+
protocol_class = getattr(module, class_name)
|
|
300
|
+
return cast("type", protocol_class)
|
|
301
|
+
except ModuleNotFoundError as e:
|
|
302
|
+
raise ImportError(
|
|
303
|
+
f"Module '{module_path}' not found for protocol '{class_name}': {e}"
|
|
304
|
+
) from e
|
|
305
|
+
except AttributeError as e:
|
|
306
|
+
raise ImportError(
|
|
307
|
+
f"Class '{class_name}' not found in module '{module_path}': {e}"
|
|
308
|
+
) from e
|
|
309
|
+
|
|
310
|
+
# ONEX_EXCLUDE: any_type - returns protocol instance, type varies by resolved protocol class
|
|
311
|
+
async def _resolve_from_container(self, protocol_class: type) -> Any:
|
|
312
|
+
"""Resolve a protocol instance from the container.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
protocol_class: The protocol class type to resolve.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The resolved protocol instance.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
RuntimeError: If container.service_registry is None.
|
|
322
|
+
Exception: If resolution fails (propagated from container).
|
|
323
|
+
"""
|
|
324
|
+
if self._container.service_registry is None:
|
|
325
|
+
raise RuntimeError(
|
|
326
|
+
"Container service_registry is None. "
|
|
327
|
+
"Ensure container is properly initialized with service_registry enabled."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return await self._container.service_registry.resolve_service(protocol_class)
|
|
331
|
+
|
|
332
|
+
async def resolve_from_path(
|
|
333
|
+
self,
|
|
334
|
+
contract_path: Path,
|
|
335
|
+
*,
|
|
336
|
+
allow_missing: bool = False,
|
|
337
|
+
) -> ModelResolvedDependencies:
|
|
338
|
+
"""Resolve protocol dependencies from a contract.yaml file path.
|
|
339
|
+
|
|
340
|
+
Loads the contract YAML, extracts the dependencies section, and resolves
|
|
341
|
+
each protocol from the container's service_registry. This is a convenience
|
|
342
|
+
method that combines file loading with dependency resolution.
|
|
343
|
+
|
|
344
|
+
Part of OMN-1903: Runtime dependency injection integration.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
contract_path: Path object to the contract.yaml file.
|
|
348
|
+
allow_missing: If True, skip missing protocols instead of raising.
|
|
349
|
+
Default False (fail-fast behavior).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
ModelResolvedDependencies containing resolved protocol instances.
|
|
353
|
+
Returns empty ModelResolvedDependencies if contract has no dependencies.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
ProtocolConfigurationError: If contract file cannot be loaded or parsed.
|
|
357
|
+
ProtocolDependencyResolutionError: If any required protocol cannot
|
|
358
|
+
be resolved and allow_missing is False.
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> resolver = ContractDependencyResolver(container)
|
|
362
|
+
>>> resolved = await resolver.resolve_from_path(
|
|
363
|
+
... Path("src/nodes/my_node/contract.yaml")
|
|
364
|
+
... )
|
|
365
|
+
>>> adapter = resolved.get("ProtocolPostgresAdapter")
|
|
366
|
+
|
|
367
|
+
.. versionadded:: 0.x.x
|
|
368
|
+
Part of OMN-1903 runtime dependency injection integration.
|
|
369
|
+
"""
|
|
370
|
+
path = contract_path
|
|
371
|
+
|
|
372
|
+
# Load and parse the contract YAML
|
|
373
|
+
contract_data = self._load_contract_yaml(path)
|
|
374
|
+
|
|
375
|
+
# Check if contract has dependencies section
|
|
376
|
+
if "dependencies" not in contract_data or not contract_data["dependencies"]:
|
|
377
|
+
logger.debug(
|
|
378
|
+
"No dependencies section in contract, skipping resolution",
|
|
379
|
+
extra={"contract_path": str(path)},
|
|
380
|
+
)
|
|
381
|
+
return ModelResolvedDependencies()
|
|
382
|
+
|
|
383
|
+
# Create a lightweight contract object for the resolver
|
|
384
|
+
# Uses SimpleNamespace for duck-typing compatibility with resolve()
|
|
385
|
+
contract_name = contract_data.get("name", path.stem)
|
|
386
|
+
dependencies = [SimpleNamespace(**dep) for dep in contract_data["dependencies"]]
|
|
387
|
+
contract = SimpleNamespace(name=contract_name, dependencies=dependencies)
|
|
388
|
+
|
|
389
|
+
logger.debug(
|
|
390
|
+
"Resolving dependencies from contract path",
|
|
391
|
+
extra={
|
|
392
|
+
"contract_path": str(path),
|
|
393
|
+
"contract_name": contract_name,
|
|
394
|
+
"dependency_count": len(dependencies),
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Type ignore: contract is a SimpleNamespace duck-typed to match ModelContractBase
|
|
399
|
+
# The resolver uses getattr() internally so any object with name/dependencies works
|
|
400
|
+
return await self.resolve(contract, allow_missing=allow_missing) # type: ignore[arg-type]
|
|
401
|
+
|
|
402
|
+
# ONEX_EXCLUDE: any_type - yaml.safe_load returns heterogeneous dict, values vary by contract schema
|
|
403
|
+
def _load_contract_yaml(self, path: Path) -> dict[str, Any]:
|
|
404
|
+
"""Load and parse a contract.yaml file.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
path: Path to the contract YAML file.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Parsed YAML content as a dictionary.
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
ProtocolConfigurationError: If file doesn't exist or cannot be parsed.
|
|
414
|
+
"""
|
|
415
|
+
if not path.exists():
|
|
416
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
417
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
418
|
+
operation="load_contract_yaml",
|
|
419
|
+
target_name=str(path),
|
|
420
|
+
)
|
|
421
|
+
raise ProtocolConfigurationError(
|
|
422
|
+
f"Contract file not found: {path}",
|
|
423
|
+
context=context,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
with open(path, encoding="utf-8") as f:
|
|
428
|
+
data = yaml.safe_load(f)
|
|
429
|
+
if data is None:
|
|
430
|
+
data = {}
|
|
431
|
+
# ONEX_EXCLUDE: any_type - yaml.safe_load returns Any, we validate structure elsewhere
|
|
432
|
+
return cast("dict[str, Any]", data)
|
|
433
|
+
except yaml.YAMLError as e:
|
|
434
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
435
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
436
|
+
operation="load_contract_yaml",
|
|
437
|
+
target_name=str(path),
|
|
438
|
+
)
|
|
439
|
+
raise ProtocolConfigurationError(
|
|
440
|
+
f"Failed to parse contract YAML at {path}: {e}",
|
|
441
|
+
context=context,
|
|
442
|
+
) from e
|
|
443
|
+
except OSError as e:
|
|
444
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
445
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
446
|
+
operation="load_contract_yaml",
|
|
447
|
+
target_name=str(path),
|
|
448
|
+
)
|
|
449
|
+
raise ProtocolConfigurationError(
|
|
450
|
+
f"Failed to read contract file at {path}: {e}",
|
|
451
|
+
context=context,
|
|
452
|
+
) from e
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
__all__ = ["ContractDependencyResolver"]
|