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.
Files changed (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {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
- ... full_topic = f"{environment}.{topic_suffix}"
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
- ... full_topic = f"{environment}.{topic_suffix}"
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. These are suffixes that should
115
- be prefixed with the environment name to form complete topic names.
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"]