diff --git a/src/omnibase_core/container/service_registry.py b/src/omnibase_core/container/service_registry.py index 88d6252110..1268f116b6 100644 --- a/src/omnibase_core/container/service_registry.py +++ b/src/omnibase_core/container/service_registry.py @@ -2,25 +2,37 @@ import time from datetime import datetime -from typing import Any, Literal, TypeVar, cast +from typing import Literal, TypeVar, cast from uuid import UUID, uuid4 from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode from omnibase_core.enums.enum_log_level import EnumLogLevel from omnibase_core.logging.structured import emit_log_event_sync as emit_log_event +from omnibase_core.models.container.model_injection_context import ModelInjectionContext from omnibase_core.models.container.model_registry_config import ( ModelServiceRegistryConfig, ) from omnibase_core.models.container.model_registry_status import ( ModelServiceRegistryStatus, ) +from omnibase_core.models.container.model_service_dependency_graph import ( + ModelServiceDependencyGraph, +) +from omnibase_core.models.container.model_service_health_validation_result import ( + ModelServiceHealthValidationResult, +) from omnibase_core.models.container.model_service_instance import ModelServiceInstance from omnibase_core.models.container.model_service_metadata import ModelServiceMetadata from omnibase_core.models.container.model_service_registration import ( ModelServiceRegistration, ) from omnibase_core.models.errors.model_onex_error import ModelOnexError -from omnibase_core.protocols import LiteralInjectionScope, LiteralServiceLifecycle +from omnibase_core.protocols import ( + LiteralInjectionScope, + LiteralServiceLifecycle, + ProtocolServiceFactory, + ProtocolServiceValidator, +) from omnibase_core.types.type_serializable_value import SerializedDict from omnibase_core.types.typed_dict_resolution_context import TypedDictResolutionContext @@ -104,12 +116,12 @@ def config(self) -> ModelServiceRegistryConfig: return self._config @property - def validator(self) -> Any | None: + def validator(self) -> ProtocolServiceValidator | None: """Get service validator (not implemented in v1.0).""" return None @property - def factory(self) -> Any | None: + def factory(self) -> ProtocolServiceFactory | None: """Get service factory (not implemented in v1.0).""" return None @@ -313,7 +325,7 @@ async def register_instance( async def register_factory( self, interface: type[TInterface], - factory: Any, + factory: ProtocolServiceFactory, lifecycle: LiteralServiceLifecycle = "transient", scope: LiteralInjectionScope = "global", ) -> UUID: @@ -322,7 +334,7 @@ async def register_factory( Args: interface: Interface protocol type - factory: Service factory + factory: Service factory implementing ProtocolServiceFactory lifecycle: Lifecycle pattern scope: Injection scope @@ -549,7 +561,7 @@ async def resolve_all_services( instance = await self._resolve_by_lifecycle( registration_id, registration, scope or registration.scope, None ) - instances.append(instance) + instances.append(cast(TInterface, instance)) return instances @@ -703,17 +715,39 @@ async def detect_circular_dependencies( # Not implemented in v1.0 - no dependency tracking yet return [] - async def get_dependency_graph(self, service_id: UUID) -> Any | None: + async def get_dependency_graph( + self, service_id: UUID + ) -> ModelServiceDependencyGraph | None: """ - Get dependency graph (not implemented in v1.0). + Get dependency graph for a service. + + Returns the dependency graph for the specified service, including + its dependencies, dependents, and resolution order. + + Note: Dependency tracking is not fully implemented in v1.0. + This method returns a minimal graph with only basic information. Args: - service_id: Service ID + service_id: Service registration ID Returns: - None (not implemented in v1.0) + ModelServiceDependencyGraph with dependency information, + or None if service not found """ - return None + # Check if service exists + if service_id not in self._registrations: + return None + + # In v1.0, we don't track dependencies, so return a minimal graph + return ModelServiceDependencyGraph( + service_id=service_id, + dependencies=[], + dependents=[], + depth_level=0, + circular_references=[], + resolution_order=[service_id], + metadata={"note": "Dependency tracking not implemented in v1.0"}, + ) async def get_registry_status(self) -> ModelServiceRegistryStatus: """ @@ -775,21 +809,78 @@ async def get_registry_status(self) -> ModelServiceRegistryStatus: last_updated=datetime.now(), ) - async def validate_service_health(self, registration_id: UUID) -> Any: + async def validate_service_health( + self, registration_id: UUID + ) -> ModelServiceHealthValidationResult: """ - Validate service health (not fully implemented in v1.0). + Validate service health. + + Performs health validation on the specified service registration, + checking instance availability and basic health metrics. Args: - registration_id: Registration ID + registration_id: Registration ID to validate Returns: - Validation result + ModelServiceHealthValidationResult with health information + + Raises: + ModelOnexError: If registration not found """ - msg = "Service health validation not yet fully implemented (planned for v2.0)" - raise ModelOnexError( - message=msg, - error_code=EnumCoreErrorCode.METHOD_NOT_IMPLEMENTED, + start_time = time.perf_counter() + + # Check if registration exists + if registration_id not in self._registrations: + return ModelServiceHealthValidationResult.unhealthy( + registration_id=registration_id, + error_message=f"Registration not found: {registration_id}", + diagnostics={"available_registrations": len(self._registrations)}, + ) + + registration = self._registrations[registration_id] + instances = self._instances.get(registration_id, []) + active_instances = [inst for inst in instances if inst.is_active()] + + end_time = time.perf_counter() + response_time_ms = (end_time - start_time) * 1000 + + # Check for issues + warnings: list[str] = [] + + if not active_instances and registration.lifecycle == "singleton": + # Singleton with no instances might indicate a problem + warnings.append("Singleton service has no active instances") + + # Check last access time + last_access: datetime | None = None + if active_instances: + last_access = max(inst.last_accessed for inst in active_instances) + + # Determine health status + if registration.health_status == "unhealthy": + return ModelServiceHealthValidationResult.unhealthy( + registration_id=registration_id, + error_message="Service marked as unhealthy", + diagnostics={ + "registration_status": registration.registration_status, + "lifecycle": registration.lifecycle, + }, + ) + + if warnings: + return ModelServiceHealthValidationResult.degraded( + registration_id=registration_id, + warnings=warnings, + instance_count=len(active_instances), + ) + + result = ModelServiceHealthValidationResult.healthy( + registration_id=registration_id, + instance_count=len(active_instances), + response_time_ms=response_time_ms, ) + result.last_access_time = last_access + return result async def update_service_configuration( self, registration_id: UUID, configuration: SerializedDict @@ -854,16 +945,26 @@ async def dispose_injection_scope(self, scope_id: UUID) -> int: error_code=EnumCoreErrorCode.METHOD_NOT_IMPLEMENTED, ) - async def get_injection_context(self, context_id: UUID) -> Any | None: + async def get_injection_context( + self, context_id: UUID + ) -> ModelInjectionContext | None: """ - Get injection context (not implemented in v1.0). + Get injection context by ID. + + Returns the injection context for the specified context ID, + including resolution status and dependency path information. + + Note: Context tracking is not fully implemented in v1.0. + This method currently returns None as contexts are not persisted. Args: - context_id: Context ID + context_id: Context ID to look up Returns: - None (not implemented in v1.0) + ModelInjectionContext if found, None otherwise """ + # In v1.0, we don't persist injection contexts + # Full context tracking is planned for v2.0 return None # Private helper methods @@ -871,7 +972,7 @@ async def get_injection_context(self, context_id: UUID) -> Any | None: async def _store_instance( self, registration_id: UUID, - instance: Any, + instance: object, lifecycle: LiteralServiceLifecycle, scope: LiteralInjectionScope, ) -> ModelServiceInstance: @@ -903,7 +1004,7 @@ async def _resolve_by_lifecycle( registration: ModelServiceRegistration, scope: LiteralInjectionScope, context: TypedDictResolutionContext | None, - ) -> Any: + ) -> object: """Resolve service based on lifecycle pattern.""" lifecycle = registration.lifecycle diff --git a/src/omnibase_core/models/container/__init__.py b/src/omnibase_core/models/container/__init__.py index 49b1d14220..81362f6255 100644 --- a/src/omnibase_core/models/container/__init__.py +++ b/src/omnibase_core/models/container/__init__.py @@ -6,6 +6,7 @@ from omnibase_core.models.container.model_base_model_onex_container import ( _BaseModelONEXContainer, ) +from omnibase_core.models.container.model_injection_context import ModelInjectionContext from omnibase_core.models.container.model_onex_container import ( ModelONEXContainer, create_model_onex_container, @@ -18,6 +19,12 @@ from omnibase_core.models.container.model_registry_status import ( ModelServiceRegistryStatus, ) +from omnibase_core.models.container.model_service_dependency_graph import ( + ModelServiceDependencyGraph, +) +from omnibase_core.models.container.model_service_health_validation_result import ( + ModelServiceHealthValidationResult, +) from omnibase_core.models.container.model_service_instance import ModelServiceInstance from omnibase_core.models.container.model_service_metadata import ModelServiceMetadata from omnibase_core.models.container.model_service_registration import ( @@ -26,12 +33,15 @@ __all__ = [ "_BaseModelONEXContainer", + "ModelInjectionContext", "ModelONEXContainer", - "ModelServiceRegistryConfig", - "ModelServiceRegistryStatus", + "ModelServiceDependencyGraph", + "ModelServiceHealthValidationResult", "ModelServiceInstance", "ModelServiceMetadata", "ModelServiceRegistration", + "ModelServiceRegistryConfig", + "ModelServiceRegistryStatus", "create_model_onex_container", "get_model_onex_container", "get_model_onex_container_sync", diff --git a/src/omnibase_core/models/container/model_injection_context.py b/src/omnibase_core/models/container/model_injection_context.py new file mode 100644 index 0000000000..b371678008 --- /dev/null +++ b/src/omnibase_core/models/container/model_injection_context.py @@ -0,0 +1,161 @@ +"""Injection Context Model. + +Implements ProtocolInjectionContext for dependency injection tracking. + +This model provides context information for dependency injection operations, +including: +- Resolution status and error tracking +- Dependency path tracking for circular detection +- Scope and timing information + +Used by the service registry to track injection operations and +diagnose resolution failures. +""" + +from datetime import UTC, datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from omnibase_core.protocols import ( + LiteralInjectionScope, + LiteralServiceResolutionStatus, +) +from omnibase_core.types.type_serializable_value import SerializableValue + + +class ModelInjectionContext(BaseModel): + """ + Context for dependency injection operations. + + Implements ProtocolInjectionContext. + Tracks the state of an injection operation including resolution + status, resolved dependencies, and error information. + + Attributes: + context_id: Unique identifier for this injection context + target_service_id: ID of the service being resolved + scope: Injection scope (global, request, session, etc.) + resolved_dependencies: Map of resolved dependency values + injection_time: When the injection was initiated + resolution_status: Current status of resolution + error_details: Error message if resolution failed + resolution_path: Path of service IDs for circular detection + metadata: Additional context metadata + + Example: + ```python + context = ModelInjectionContext( + context_id=uuid4(), + target_service_id=UUID("12345678-..."), + scope="global", + resolution_status="resolved", + ) + ``` + + Thread Safety: + This context is designed for single-threaded use during + service resolution. Each resolution operation should use + its own context instance. Do not share contexts across + concurrent resolution operations. + """ + + model_config = ConfigDict( + extra="ignore", + frozen=False, + validate_assignment=True, + ) + + context_id: UUID = Field(description="Unique identifier for this injection context") + target_service_id: UUID = Field(description="ID of the service being resolved") + scope: LiteralInjectionScope = Field( + default="global", + description="Injection scope", + ) + resolved_dependencies: dict[str, SerializableValue] = Field( + default_factory=dict, + description="Map of resolved dependency values", + ) + injection_time: datetime = Field( + default_factory=lambda: datetime.now(UTC), + description="When the injection was initiated", + ) + resolution_status: LiteralServiceResolutionStatus = Field( + description="Current status of resolution", + ) + error_details: str | None = Field( + default=None, + description="Error message if resolution failed", + ) + resolution_path: list[UUID] = Field( + default_factory=list, + description="Path of service IDs for circular detection", + ) + metadata: dict[str, SerializableValue] = Field( + default_factory=dict, + description="Additional context metadata", + ) + + @property + def is_resolved(self) -> bool: + """Check if the service was successfully resolved.""" + return self.resolution_status == "resolved" + + @property + def has_error(self) -> bool: + """Check if resolution encountered an error.""" + return self.resolution_status == "failed" or self.error_details is not None + + @property + def has_circular_dependency(self) -> bool: + """Check if resolution failed due to circular dependency.""" + return self.resolution_status == "circular_dependency" + + @property + def has_missing_dependency(self) -> bool: + """Check if resolution failed due to missing dependency.""" + return self.resolution_status == "missing_dependency" + + def mark_resolved(self) -> None: + """Mark the injection as successfully resolved.""" + self.resolution_status = "resolved" + self.error_details = None + + def mark_failed(self, error_message: str) -> None: + """Mark the injection as failed with an error.""" + self.resolution_status = "failed" + self.error_details = error_message + + def mark_circular_dependency(self, error_message: str) -> None: + """Mark the injection as failed due to circular dependency.""" + self.resolution_status = "circular_dependency" + self.error_details = error_message + + def mark_missing_dependency(self, error_message: str) -> None: + """Mark the injection as failed due to missing dependency.""" + self.resolution_status = "missing_dependency" + self.error_details = error_message + + def mark_type_mismatch(self, error_message: str) -> None: + """Mark the injection as failed due to type mismatch.""" + self.resolution_status = "type_mismatch" + self.error_details = error_message + + def add_to_path(self, service_id: UUID) -> bool: + """ + Add a service to the resolution path. + + Returns: + True if added successfully, False if already in path (circular) + """ + if service_id in self.resolution_path: + return False + self.resolution_path.append(service_id) + return True + + def add_resolved_dependency(self, name: str, value: SerializableValue) -> None: + """Add a resolved dependency value.""" + self.resolved_dependencies[name] = value + + +__all__ = ["ModelInjectionContext"] diff --git a/src/omnibase_core/models/container/model_service_dependency_graph.py b/src/omnibase_core/models/container/model_service_dependency_graph.py new file mode 100644 index 0000000000..63f40c66fe --- /dev/null +++ b/src/omnibase_core/models/container/model_service_dependency_graph.py @@ -0,0 +1,118 @@ +"""Service Dependency Graph Model. + +Implements ProtocolDependencyGraph for service registry dependency analysis. + +This model provides dependency graph information for services registered +in the service registry, including: +- Direct dependencies and dependents +- Circular reference detection +- Resolution ordering for dependency injection + +Note: This is distinct from ModelDependencyGraph in workflow/execution which +is used for workflow step ordering. This model is specific to DI containers. +""" + +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from omnibase_core.types.type_serializable_value import SerializableValue + + +class ModelServiceDependencyGraph(BaseModel): + """ + Dependency graph for a service in the registry. + + Implements ProtocolDependencyGraph. + Tracks service dependencies for proper initialization ordering + and circular dependency detection. + + Attributes: + service_id: ID of the service this graph describes + dependencies: List of service IDs this service depends on + dependents: List of service IDs that depend on this service + depth_level: Depth in the dependency tree (0 = root, no dependencies) + circular_references: List of service IDs forming circular dependencies + resolution_order: Ordered list of service IDs for proper initialization + metadata: Additional graph metadata + + Example: + ```python + graph = ModelServiceDependencyGraph( + service_id=UUID("12345678-1234-5678-1234-567812345678"), + dependencies=[UUID("aaaa-...")], + dependents=[UUID("bbbb-...")], + depth_level=1, + circular_references=[], + resolution_order=[UUID("aaaa-..."), UUID("12345678-...")], + ) + ``` + """ + + model_config = ConfigDict( + extra="ignore", + frozen=False, + validate_assignment=True, + ) + + service_id: UUID = Field(description="ID of the service this graph describes") + dependencies: list[UUID] = Field( + default_factory=list, + description="Service IDs this service depends on", + ) + dependents: list[UUID] = Field( + default_factory=list, + description="Service IDs that depend on this service", + ) + depth_level: int = Field( + default=0, + ge=0, + description="Depth in dependency tree (0 = root with no dependencies)", + ) + circular_references: list[UUID] = Field( + default_factory=list, + description="Service IDs forming circular dependencies", + ) + resolution_order: list[UUID] = Field( + default_factory=list, + description="Ordered service IDs for proper initialization", + ) + metadata: dict[str, SerializableValue] = Field( + default_factory=dict, + description="Additional graph metadata", + ) + + @property + def has_circular_dependencies(self) -> bool: + """Check if this service has circular dependencies.""" + return len(self.circular_references) > 0 + + @property + def has_dependencies(self) -> bool: + """Check if this service has any dependencies.""" + return len(self.dependencies) > 0 + + @property + def has_dependents(self) -> bool: + """Check if any services depend on this one.""" + return len(self.dependents) > 0 + + def add_dependency(self, dependency_id: UUID) -> None: + """Add a dependency to this service.""" + if dependency_id == self.service_id: + return # Ignore self-reference + if dependency_id not in self.dependencies: + self.dependencies.append(dependency_id) + + def add_dependent(self, service_id: UUID) -> None: + """Add a service that depends on this one.""" + if service_id not in self.dependents: + self.dependents.append(service_id) + + def mark_circular_reference(self, service_id: UUID) -> None: + """Mark a circular reference with another service.""" + if service_id not in self.circular_references: + self.circular_references.append(service_id) + + +__all__ = ["ModelServiceDependencyGraph"] diff --git a/src/omnibase_core/models/container/model_service_health_validation_result.py b/src/omnibase_core/models/container/model_service_health_validation_result.py new file mode 100644 index 0000000000..fec2027f83 --- /dev/null +++ b/src/omnibase_core/models/container/model_service_health_validation_result.py @@ -0,0 +1,170 @@ +"""Service Health Validation Result Model. + +Result model for service health validation in the service registry. + +This model provides detailed information about service health validation +including validation status, health metrics, and diagnostic information. +Used by the validate_service_health() method in ServiceRegistry. +""" + +from datetime import UTC, datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from omnibase_core.protocols import LiteralHealthStatus +from omnibase_core.types.type_serializable_value import SerializedDict + + +class ModelServiceHealthValidationResult(BaseModel): + """ + Result of service health validation. + + Provides comprehensive health validation results for a registered service, + including health status, metrics, and diagnostic information. + + Attributes: + registration_id: ID of the service registration validated + is_healthy: Overall health status + health_status: Detailed health status + validation_time: When the validation was performed + response_time_ms: Time taken to validate in milliseconds + instance_count: Number of active instances + last_access_time: Last time the service was accessed + error_message: Error message if validation failed + warnings: List of warning messages + diagnostics: Additional diagnostic information + + Example: + ```python + result = ModelServiceHealthValidationResult( + registration_id=UUID("12345678-..."), + is_healthy=True, + health_status="healthy", + response_time_ms=5.2, + instance_count=1, + ) + ``` + """ + + model_config = ConfigDict( + extra="ignore", + frozen=False, + validate_assignment=True, + ) + + registration_id: UUID = Field( + description="ID of the service registration validated" + ) + is_healthy: bool = Field( + default=True, + description="Overall health status", + ) + health_status: LiteralHealthStatus = Field( + default="healthy", + description="Detailed health status", + ) + validation_time: datetime = Field( + default_factory=lambda: datetime.now(UTC), + description="When the validation was performed", + ) + response_time_ms: float | None = Field( + default=None, + description="Time taken to validate in milliseconds", + ) + instance_count: int = Field( + default=0, + ge=0, + description="Number of active instances", + ) + last_access_time: datetime | None = Field( + default=None, + description="Last time the service was accessed", + ) + error_message: str | None = Field( + default=None, + description="Error message if validation failed", + ) + warnings: list[str] = Field( + default_factory=list, + description="List of warning messages", + ) + diagnostics: SerializedDict = Field( + default_factory=dict, + description="Additional diagnostic information", + ) + + @property + def has_warnings(self) -> bool: + """Check if validation produced warnings.""" + return len(self.warnings) > 0 + + @property + def has_error(self) -> bool: + """Check if validation produced an error.""" + return self.error_message is not None + + def add_warning(self, warning: str) -> None: + """Add a warning message.""" + self.warnings.append(warning) + + def set_error(self, error: str) -> None: + """Set the error message and mark as unhealthy.""" + self.error_message = error + self.is_healthy = False + self.health_status = "unhealthy" + + def add_diagnostic(self, key: str, value: str | int | float | bool | None) -> None: + """Add diagnostic information.""" + self.diagnostics[key] = value + + @classmethod + def healthy( + cls, + registration_id: UUID, + instance_count: int = 1, + response_time_ms: float | None = None, + ) -> "ModelServiceHealthValidationResult": + """Create a healthy validation result.""" + return cls( + registration_id=registration_id, + is_healthy=True, + health_status="healthy", + instance_count=instance_count, + response_time_ms=response_time_ms, + ) + + @classmethod + def unhealthy( + cls, + registration_id: UUID, + error_message: str, + diagnostics: SerializedDict | None = None, + ) -> "ModelServiceHealthValidationResult": + """Create an unhealthy validation result.""" + return cls( + registration_id=registration_id, + is_healthy=False, + health_status="unhealthy", + error_message=error_message, + diagnostics=diagnostics or {}, + ) + + @classmethod + def degraded( + cls, + registration_id: UUID, + warnings: list[str], + instance_count: int = 1, + ) -> "ModelServiceHealthValidationResult": + """Create a degraded validation result.""" + return cls( + registration_id=registration_id, + is_healthy=True, + health_status="degraded", + instance_count=instance_count, + warnings=warnings, + ) + + +__all__ = ["ModelServiceHealthValidationResult"] diff --git a/src/omnibase_core/protocols/__init__.py b/src/omnibase_core/protocols/__init__.py index 00c7ee261f..295b93edc9 100644 --- a/src/omnibase_core/protocols/__init__.py +++ b/src/omnibase_core/protocols/__init__.py @@ -158,8 +158,11 @@ ) # ============================================================================= -# Logger Module Exports +# Logging Protocol Exports # ============================================================================= +from omnibase_core.protocols.protocol_context_aware_output_handler import ( + ProtocolContextAwareOutputHandler, +) from omnibase_core.protocols.protocol_logger_like import ProtocolLoggerLike # ============================================================================= @@ -167,6 +170,24 @@ # ============================================================================= from omnibase_core.protocols.protocol_from_event import ProtocolFromEvent +# ============================================================================= +# Generation Protocol Exports +# ============================================================================= +from omnibase_core.protocols.protocol_generation_config import ProtocolGenerationConfig +from omnibase_core.protocols.protocol_import_tracker import ProtocolImportTracker + +# ============================================================================= +# Data Protocol Exports +# ============================================================================= +from omnibase_core.protocols.protocol_payload_data import ( + PayloadValue, + ProtocolPayloadData, +) +from omnibase_core.protocols.protocol_smart_log_formatter import ( + LogDataValue, + ProtocolSmartLogFormatter, +) + # ============================================================================= # Resolution Module Exports (OMN-1123) # ============================================================================= @@ -324,14 +345,27 @@ # ========================================================================== "ProtocolCanonicalSerializer", # ========================================================================== - # Logger Module + # Data Protocols + # ========================================================================== + "ProtocolPayloadData", + "PayloadValue", # ========================================================================== + # Logging Protocols + # ========================================================================== + "ProtocolSmartLogFormatter", + "ProtocolContextAwareOutputHandler", "ProtocolLoggerLike", + "LogDataValue", # ========================================================================== # Event Construction Protocol # ========================================================================== "ProtocolFromEvent", # ========================================================================== + # Generation Protocols + # ========================================================================== + "ProtocolGenerationConfig", + "ProtocolImportTracker", + # ========================================================================== # Compute Module # ========================================================================== "ProtocolAsyncCircuitBreaker", diff --git a/src/omnibase_core/protocols/compute/__init__.py b/src/omnibase_core/protocols/compute/__init__.py index 438ed3d0d5..6b7201af37 100644 --- a/src/omnibase_core/protocols/compute/__init__.py +++ b/src/omnibase_core/protocols/compute/__init__.py @@ -23,6 +23,10 @@ from omnibase_core.protocols.compute.protocol_parallel_executor import ( ProtocolParallelExecutor, ) +from omnibase_core.protocols.compute.protocol_payload_data import ( + ProtocolComputePayloadData, + ProtocolDictLike, +) from omnibase_core.protocols.compute.protocol_performance_monitor import ( ProtocolPerformanceMonitor, ) @@ -34,6 +38,8 @@ "ProtocolAsyncCircuitBreaker", "ProtocolCircuitBreaker", "ProtocolComputeCache", + "ProtocolComputePayloadData", + "ProtocolDictLike", "ProtocolParallelExecutor", "ProtocolPerformanceMonitor", "ProtocolTimingService", diff --git a/src/omnibase_core/protocols/compute/protocol_payload_data.py b/src/omnibase_core/protocols/compute/protocol_payload_data.py new file mode 100644 index 0000000000..23e1b11e69 --- /dev/null +++ b/src/omnibase_core/protocols/compute/protocol_payload_data.py @@ -0,0 +1,130 @@ +""" +ProtocolComputePayloadData - Protocol for polymorphic data in compute pipelines. + +This protocol defines the interface for data objects that can be traversed +in compute pipelines. It supports dict-like access, Pydantic models, and +regular Python objects with attributes. + +Note: + This is distinct from ProtocolPayloadData in omnibase_core.protocols which + provides a full dict-like interface (get, keys, values, items, etc.). + ProtocolComputePayloadData is a marker protocol with no required methods, + designed for maximum flexibility in compute pipeline data types. + +Design: + This protocol uses structural subtyping (duck typing) to allow any object + that supports the required access patterns to be used in pipeline operations. + The protocol is intentionally minimal to maximize compatibility. + +Usage: + .. code-block:: python + + from omnibase_core.protocols.compute import ProtocolComputePayloadData + + # Dict-like access + data: ProtocolComputePayloadData = {"key": "value"} + + # Pydantic model + class UserModel(BaseModel): + name: str + age: int + + user: ProtocolComputePayloadData = UserModel(name="Alice", age=30) + + # Regular object with attributes + @dataclass + class Config: + debug: bool = False + + config: ProtocolComputePayloadData = Config() + +Related: + - compute_executor.py: Uses this for pipeline data flows + - compute_transformations.py: Uses this for transformation inputs + - compute_path_resolver.py: Uses this for path traversal + +.. versionadded:: 0.4.0 +""" + +from __future__ import annotations + +__all__ = ["ProtocolComputePayloadData", "ProtocolDictLike"] + +from collections.abc import Iterator +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ProtocolDictLike(Protocol): + """ + Protocol for objects that support dictionary-like access. + + This protocol defines the minimal interface for dict-like objects + used in path resolution and data traversal. Objects implementing + this protocol can be navigated using key-based access. + + Note: + This protocol is satisfied by: + - dict instances + - Mapping implementations + - Objects with __getitem__ and keys methods + """ + + def __getitem__(self, key: str) -> object: + """Get a value by key.""" + ... + + def keys(self) -> Iterator[str]: + """Return the keys of the container.""" + ... + + def __contains__(self, key: object) -> bool: + """Check if a key exists in the container.""" + ... + + +@runtime_checkable +class ProtocolComputePayloadData(Protocol): + """ + Protocol for polymorphic payload data in compute pipelines. + + This protocol is satisfied by any object that can be used as + data in compute pipeline operations: + - Dictionaries (dict[str, object]) + - Pydantic BaseModel instances (via model_dump) + - Regular objects with attributes (via getattr) + + The protocol is minimal by design - it doesn't require specific + methods since different data types are accessed differently: + - Dicts: via __getitem__ + - Objects: via getattr + + This protocol is primarily used as a type marker to indicate + that a parameter accepts polymorphic data. Actual data access + is handled by the path resolution utilities which check for + dict vs object at runtime. + + Note: + This is distinct from ProtocolPayloadData in omnibase_core.protocols + which provides a full dict-like interface with get(), keys(), values(), + items(), __getitem__(), and __contains__() methods. + + Thread Safety: + This protocol makes no thread safety guarantees. The underlying + data objects should be treated as read-only during pipeline execution. + + Example: + .. code-block:: python + + def process_data(data: ProtocolComputePayloadData) -> str: + # Data can be dict, Pydantic model, or object + if isinstance(data, dict): + return str(data.get("value", "")) + return str(getattr(data, "value", "")) + + .. versionadded:: 0.4.0 + """ + + # Marker protocol - no required methods + # This allows any object to satisfy the protocol + # Actual access patterns are determined at runtime diff --git a/src/omnibase_core/protocols/container/protocol_service_factory.py b/src/omnibase_core/protocols/container/protocol_service_factory.py index 6d7d432e7c..8acbd3faee 100644 --- a/src/omnibase_core/protocols/container/protocol_service_factory.py +++ b/src/omnibase_core/protocols/container/protocol_service_factory.py @@ -4,11 +4,17 @@ This module provides the ProtocolServiceFactory protocol which defines the interface for service instance creation with dependency injection support and lifecycle management. + +Design Principles: +- Use typing.Protocol with @runtime_checkable for duck typing support +- Keep interfaces minimal - only define what Core actually needs +- Provide complete type hints for mypy strict mode compliance +- NO Any types - use object for maximum flexibility where needed """ from __future__ import annotations -from typing import Any, Protocol, TypeVar, runtime_checkable +from typing import Protocol, TypeVar, runtime_checkable from omnibase_core.protocols.base import ContextValue @@ -22,16 +28,50 @@ class ProtocolServiceFactory(Protocol): Defines the interface for service instance creation with dependency injection support and lifecycle management. + + The factory is responsible for: + - Creating new service instances with proper dependency injection + - Managing the lifecycle of created instances + - Disposing of instances when no longer needed + + Example: + class MyServiceFactory: + async def create_instance( + self, interface: type[T], context: dict[str, ContextValue] + ) -> T: + # Create and return instance + return instance + + async def dispose_instance(self, instance: object) -> None: + # Clean up instance resources + pass """ async def create_instance( self, interface: type[T], context: dict[str, ContextValue] ) -> T: - """Create a new service instance with dependency injection.""" + """ + Create a new service instance with dependency injection. + + Args: + interface: The interface type to create an instance for + context: Injection context with configuration values + + Returns: + A new instance implementing the specified interface + """ ... - async def dispose_instance(self, instance: Any) -> None: - """Dispose of a service instance.""" + async def dispose_instance(self, instance: object) -> None: + """ + Dispose of a service instance. + + Cleans up any resources held by the instance and removes it + from the container's management. + + Args: + instance: The service instance to dispose + """ ... diff --git a/src/omnibase_core/protocols/container/protocol_service_validator.py b/src/omnibase_core/protocols/container/protocol_service_validator.py index c4e5b9739e..20f8471406 100644 --- a/src/omnibase_core/protocols/container/protocol_service_validator.py +++ b/src/omnibase_core/protocols/container/protocol_service_validator.py @@ -4,11 +4,17 @@ This module provides the ProtocolServiceValidator protocol which defines the interface for comprehensive service validation including interface compliance checking and dependency validation. + +Design Principles: +- Use typing.Protocol with @runtime_checkable for duck typing support +- Keep interfaces minimal - only define what Core actually needs +- Provide complete type hints for mypy strict mode compliance +- NO Any types - use object for maximum flexibility where needed """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: from omnibase_core.protocols.container.protocol_service_dependency import ( @@ -26,18 +32,54 @@ class ProtocolServiceValidator(Protocol): Defines the interface for comprehensive service validation including interface compliance checking and dependency validation. + + The validator is responsible for: + - Checking that service implementations conform to their interfaces + - Validating that all declared dependencies can be satisfied + - Producing detailed validation results with error information + + Example: + class MyServiceValidator: + async def validate_service( + self, service: object, interface: type[object] + ) -> ProtocolValidationResult: + # Check service conforms to interface + return validation_result + + async def validate_dependencies( + self, dependencies: list[ProtocolServiceDependency] + ) -> ProtocolValidationResult: + # Check all dependencies can be resolved + return validation_result """ async def validate_service( - self, service: Any, interface: type[Any] + self, service: object, interface: type[object] ) -> ProtocolValidationResult: - """Validate that a service implementation conforms to its interface.""" + """ + Validate that a service implementation conforms to its interface. + + Args: + service: The service instance to validate + interface: The interface type the service should conform to + + Returns: + Validation result indicating success or failure with details + """ ... async def validate_dependencies( self, dependencies: list[ProtocolServiceDependency] ) -> ProtocolValidationResult: - """Validate that all dependencies can be satisfied.""" + """ + Validate that all dependencies can be satisfied. + + Args: + dependencies: List of service dependencies to validate + + Returns: + Validation result indicating if all dependencies can be resolved + """ ... diff --git a/src/omnibase_core/protocols/logging/__init__.py b/src/omnibase_core/protocols/logging/__init__.py new file mode 100644 index 0000000000..248564c357 --- /dev/null +++ b/src/omnibase_core/protocols/logging/__init__.py @@ -0,0 +1,26 @@ +""" +Logging protocols for ONEX logging infrastructure. + +This module provides protocol definitions for logging formatters and output handlers +used by the ONEX logging system. +""" + +from omnibase_core.protocols.logging.protocol_minimal_logger import ( + ProtocolMinimalLogger, +) +from omnibase_core.protocols.logging.protocol_registry_node import ( + ProtocolRegistryNode, +) +from omnibase_core.protocols.logging.protocol_smart_log_formatter import ( + ProtocolSmartLogFormatter, +) +from omnibase_core.protocols.protocol_context_aware_output_handler import ( + ProtocolContextAwareOutputHandler, +) + +__all__ = [ + "ProtocolContextAwareOutputHandler", + "ProtocolMinimalLogger", + "ProtocolRegistryNode", + "ProtocolSmartLogFormatter", +] diff --git a/src/omnibase_core/protocols/logging/protocol_minimal_logger.py b/src/omnibase_core/protocols/logging/protocol_minimal_logger.py new file mode 100644 index 0000000000..4c39b04e76 --- /dev/null +++ b/src/omnibase_core/protocols/logging/protocol_minimal_logger.py @@ -0,0 +1,94 @@ +""" +ProtocolMinimalLogger - Protocol for minimal bootstrap logging. + +This module provides the protocol definition for minimal loggers +used during bootstrap phase when full logging infrastructure is unavailable. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Protocol, runtime_checkable + +from omnibase_core.enums.enum_log_level import EnumLogLevel + + +@runtime_checkable +class ProtocolMinimalLogger(Protocol): + """ + Protocol for minimal bootstrap loggers. + + Provides basic logging interface for bootstrap scenarios + when full logging infrastructure is unavailable. + """ + + def emit_log_event( + self, + level: EnumLogLevel, + event_type: str, + message: str, + **kwargs: object, + ) -> None: + """Emit a structured log event.""" + ... + + def emit_log_event_sync( + self, + level: EnumLogLevel, + message: str, + event_type: str = ..., + **kwargs: object, + ) -> None: + """ + Synchronous log event emission. + + Args: + level: Log level for the event. + message: Primary log message. + event_type: Type of event. Defaults to "generic". + **kwargs: Additional structured data. + """ + ... + + async def emit_log_event_async( + self, + level: EnumLogLevel, + message: str, + event_type: str = ..., + **kwargs: object, + ) -> None: + """ + Asynchronous log event emission. + + Args: + level: Log level for the event. + message: Primary log message. + event_type: Type of event. Defaults to "generic". + **kwargs: Additional structured data. + """ + ... + + def trace_function_lifecycle[F: Callable[..., object]](self, func: F) -> F: + """Decorator to trace function lifecycle.""" + ... + + def tool_logger_performance_metrics( + self, + _threshold_ms: int = ..., + ) -> Callable[[Callable[..., object]], Callable[..., object]]: + """ + Decorator for performance metrics logging. + + Args: + _threshold_ms: Performance threshold in milliseconds. + Logs warning if execution exceeds this threshold. + Defaults to 1000. + + Returns: + Decorator function that wraps the target function with + performance metrics logging. + """ + ... + + +__all__ = ["ProtocolMinimalLogger"] diff --git a/src/omnibase_core/protocols/logging/protocol_registry_node.py b/src/omnibase_core/protocols/logging/protocol_registry_node.py new file mode 100644 index 0000000000..04a1466b7d --- /dev/null +++ b/src/omnibase_core/protocols/logging/protocol_registry_node.py @@ -0,0 +1,57 @@ +""" +ProtocolRegistryNode - Protocol for bootstrap registry node. + +This module provides the protocol definition for registry nodes +used in bootstrap service discovery. +""" + +from __future__ import annotations + +from typing import Protocol, TypeVar, runtime_checkable + +T = TypeVar("T") + + +@runtime_checkable +class ProtocolRegistryNode(Protocol): + """ + Protocol for registry nodes in bootstrap service discovery. + + Provides service and protocol resolution for bootstrap scenarios. + """ + + def get_service(self, protocol_type: type[T]) -> T | None: + """ + Get a service implementation for the given protocol type. + + Args: + protocol_type: The protocol interface to resolve + + Returns: + Service implementation or None if not found + """ + ... + + def get_protocol(self, name: str) -> object | None: + """ + Get a protocol implementation by name. + + Args: + name: The protocol name to resolve + + Returns: + Protocol implementation or None if not found + """ + ... + + def list_services(self) -> list[str]: + """ + List available services. + + Returns: + List of available service names + """ + ... + + +__all__ = ["ProtocolRegistryNode"] diff --git a/src/omnibase_core/protocols/logging/protocol_smart_log_formatter.py b/src/omnibase_core/protocols/logging/protocol_smart_log_formatter.py new file mode 100644 index 0000000000..ec288e3346 --- /dev/null +++ b/src/omnibase_core/protocols/logging/protocol_smart_log_formatter.py @@ -0,0 +1,56 @@ +""" +ProtocolSmartLogFormatter - Protocol for smart log event formatting. + +This module provides the protocol definition for log formatters that +transform structured log events into formatted output strings. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +if TYPE_CHECKING: + from omnibase_core.enums.enum_log_level import EnumLogLevel + from omnibase_core.models.core.model_log_context import ModelLogContext + +# Type alias for log data values - JSON-compatible types +# Consistent with omnibase_core.protocols.protocol_smart_log_formatter.LogDataValue +LogDataValue = str | int | float | bool | None + + +@runtime_checkable +class ProtocolSmartLogFormatter(Protocol): + """ + Protocol for smart log event formatters. + + Transforms structured log events into formatted output strings + suitable for display or storage. + """ + + def format_log_event( + self, + level: EnumLogLevel, + event_type: str, + message: str, + context: ModelLogContext, + data: dict[str, LogDataValue], + correlation_id: str, + ) -> str: + """ + Format a log event into a string representation. + + Args: + level: Log level (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL) + event_type: Type of event (function_entry, node_execution_start, etc.) + message: Primary log message + context: Log context with calling function, module, and line info + data: Additional structured data + correlation_id: Correlation ID for tracing + + Returns: + Formatted log string + """ + ... + + +__all__ = ["ProtocolSmartLogFormatter", "LogDataValue"] diff --git a/src/omnibase_core/protocols/protocol_context_aware_output_handler.py b/src/omnibase_core/protocols/protocol_context_aware_output_handler.py new file mode 100644 index 0000000000..eb86a5f2fe --- /dev/null +++ b/src/omnibase_core/protocols/protocol_context_aware_output_handler.py @@ -0,0 +1,62 @@ +""" +Protocol for context-aware output handling. + +This module provides the ProtocolContextAwareOutputHandler protocol which +defines the interface for output handlers used in the ONEX logging infrastructure. + +Design Principles: +- Use typing.Protocol with @runtime_checkable for duck typing support +- Keep interfaces minimal - only define what Core actually needs +- Provide complete type hints for mypy strict mode compliance +- NO Any types - use structured types for output data +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ProtocolContextAwareOutputHandler(Protocol): + """ + Protocol for context-aware output handling. + + Defines the interface for output handlers that route formatted log + entries to appropriate destinations based on context. Used by the + ONEX logging infrastructure for flexible log output routing. + + The output handler is responsible for: + - Receiving formatted log entries + - Routing to appropriate output destinations (console, file, event bus) + - Handling output based on log level + - Managing output context and buffering + + Example: + class MyOutputHandler: + def output_log_entry( + self, + formatted_log: str, + level_name: str, + ) -> None: + if level_name in ("ERROR", "CRITICAL"): + print(formatted_log, file=sys.stderr) + else: + print(formatted_log) + """ + + def output_log_entry( + self, + formatted_log: str, + level_name: str, + ) -> None: + """ + Output a formatted log entry. + + Args: + formatted_log: Pre-formatted log string ready for output + level_name: Name of the log level (e.g., "DEBUG", "INFO", "ERROR") + """ + ... + + +__all__ = ["ProtocolContextAwareOutputHandler"] diff --git a/src/omnibase_core/protocols/protocol_generation_config.py b/src/omnibase_core/protocols/protocol_generation_config.py new file mode 100644 index 0000000000..162a0e7876 --- /dev/null +++ b/src/omnibase_core/protocols/protocol_generation_config.py @@ -0,0 +1,28 @@ +""" +Protocol for generation configuration objects. + +Defines the interface for config objects passed to UtilityReferenceResolver. +""" + +from typing import Protocol, runtime_checkable + +__all__ = ["ProtocolGenerationConfig"] + + +@runtime_checkable +class ProtocolGenerationConfig(Protocol): + """ + Protocol for generation configuration objects. + + Defines the interface for config objects passed to UtilityReferenceResolver. + """ + + @property + def subcontract_import_map(self) -> dict[str, dict[str, str]] | None: + """Map of subcontract paths to import configuration.""" + ... + + @property + def use_imports_for_subcontracts(self) -> bool: + """Whether to use imports for subcontract references.""" + ... diff --git a/src/omnibase_core/protocols/protocol_import_tracker.py b/src/omnibase_core/protocols/protocol_import_tracker.py new file mode 100644 index 0000000000..f3e484b6f4 --- /dev/null +++ b/src/omnibase_core/protocols/protocol_import_tracker.py @@ -0,0 +1,36 @@ +""" +Protocol for import tracking objects. + +Defines the interface for import trackers passed to UtilityReferenceResolver. +""" + +from typing import Protocol, runtime_checkable + +__all__ = ["ProtocolImportTracker"] + + +@runtime_checkable +class ProtocolImportTracker(Protocol): + """ + Protocol for import tracking objects. + + Defines the interface for import trackers passed to UtilityReferenceResolver. + """ + + def add_subcontract_model( + self, + subcontract_path: str, + model_name: str, + package_name: str, + import_path: str, + ) -> None: + """ + Track a subcontract model for import generation. + + Args: + subcontract_path: Path to the subcontract file + model_name: Resolved model name + package_name: Package name for the model + import_path: Python import path + """ + ... diff --git a/src/omnibase_core/protocols/protocol_payload_data.py b/src/omnibase_core/protocols/protocol_payload_data.py new file mode 100644 index 0000000000..bf8847d81b --- /dev/null +++ b/src/omnibase_core/protocols/protocol_payload_data.py @@ -0,0 +1,128 @@ +""" +Protocol for payload data with dict-like access. + +This module provides the ProtocolPayloadData protocol which defines +the interface for compute pipeline data that needs dict-like access. +Compatible with dict, Pydantic BaseModel, and objects with __dict__. + +Design Principles: +- Use typing.Protocol with @runtime_checkable for duck typing support +- Keep interfaces minimal - only define what Core actually needs +- Provide complete type hints for mypy strict mode compliance +- NO Any types - use object with proper constraints +""" + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Protocol, TypeVar, overload, runtime_checkable + +# Type variable for payload values - intentionally permissive for data payloads +PayloadValue = str | int | float | bool | None | list[object] | dict[str, object] + +# Type variable for default values in get() method +_T = TypeVar("_T") + + +@runtime_checkable +class ProtocolPayloadData(Protocol): + """ + Protocol for payload data with dict-like access. + + Defines the interface for compute pipeline data that needs dict-like access. + Compatible with: + - Python dict + - Pydantic BaseModel (via model_dump) + - Objects with __dict__ attribute + + This protocol enables type-safe access to payload data without using Any. + + Example: + def process_payload(data: ProtocolPayloadData) -> None: + if "user_id" in data: + user_id = data["user_id"] + for key in data.keys(): + print(f"{key}: {data.get(key)}") + """ + + @overload + def get(self, key: str) -> PayloadValue | None: + """Get value by key, returning None if not found.""" + ... + + @overload + def get(self, key: str, default: _T) -> PayloadValue | _T: + """Get value by key, returning default if not found.""" + ... + + def get( + self, key: str, default: PayloadValue | _T | None = None + ) -> PayloadValue | _T | None: + """ + Get a value by key with optional default. + + Args: + key: The key to look up + default: Value to return if key not found + + Returns: + The value associated with the key, or default if not found + """ + ... + + def keys(self) -> Iterator[str]: + """ + Return an iterator over the keys. + + Returns: + Iterator of string keys + """ + ... + + def values(self) -> Iterator[PayloadValue]: + """ + Return an iterator over the values. + + Returns: + Iterator of payload values + """ + ... + + def items(self) -> Iterator[tuple[str, PayloadValue]]: + """ + Return an iterator over (key, value) pairs. + + Returns: + Iterator of (key, value) tuples + """ + ... + + def __getitem__(self, key: str) -> PayloadValue: + """ + Get value by key using bracket notation. + + Args: + key: The key to look up + + Returns: + The value associated with the key + + Raises: + KeyError: If key is not found + """ + ... + + def __contains__(self, key: object) -> bool: + """ + Check if key exists in the payload. + + Args: + key: The key to check + + Returns: + True if key exists, False otherwise + """ + ... + + +__all__ = ["ProtocolPayloadData", "PayloadValue"] diff --git a/src/omnibase_core/protocols/protocol_smart_log_formatter.py b/src/omnibase_core/protocols/protocol_smart_log_formatter.py new file mode 100644 index 0000000000..afa885713e --- /dev/null +++ b/src/omnibase_core/protocols/protocol_smart_log_formatter.py @@ -0,0 +1,81 @@ +""" +Protocol for smart log formatting. + +This module provides the ProtocolSmartLogFormatter protocol which defines +the interface for logging formatters used in the ONEX logging infrastructure. + +Design Principles: +- Use typing.Protocol with @runtime_checkable for duck typing support +- Keep interfaces minimal - only define what Core actually needs +- Provide complete type hints for mypy strict mode compliance +- NO Any types - use structured types for log data +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +if TYPE_CHECKING: + from omnibase_core.enums.enum_log_level import EnumLogLevel + from omnibase_core.models.core.model_log_context import ModelLogContext + +# Type alias for log data values - JSON-compatible types +LogDataValue = str | int | float | bool | None + + +@runtime_checkable +class ProtocolSmartLogFormatter(Protocol): + """ + Protocol for smart log formatting. + + Defines the interface for logging formatters that convert structured + log events into formatted output strings. Used by the ONEX logging + infrastructure for consistent log formatting across the platform. + + The formatter is responsible for: + - Converting log events to human-readable format + - Including context information (module, function, line) + - Formatting structured data as JSON or key-value pairs + - Applying appropriate formatting based on log level + + Example: + class MyLogFormatter: + def format_log_event( + self, + level: EnumLogLevel, + event_type: str, + message: str, + context: ModelLogContext, + data: dict[str, LogDataValue], + correlation_id: str, + ) -> str: + return f"[{level.name}] {correlation_id}: {message}" + """ + + def format_log_event( + self, + level: EnumLogLevel, + event_type: str, + message: str, + context: ModelLogContext, + data: dict[str, LogDataValue], + correlation_id: str, + ) -> str: + """ + Format a structured log event into a string. + + Args: + level: Log level (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL) + event_type: Type of event (function_entry, node_execution_start, etc.) + message: Primary log message + context: Log context with calling module, function, line, timestamp + data: Additional structured data as key-value pairs + correlation_id: Correlation ID for tracing (as string) + + Returns: + Formatted log string ready for output + """ + ... + + +__all__ = ["ProtocolSmartLogFormatter", "LogDataValue"] diff --git a/src/omnibase_core/runtime/envelope_router.py b/src/omnibase_core/runtime/envelope_router.py index 68d5e4e1f4..e1335c0aba 100644 --- a/src/omnibase_core/runtime/envelope_router.py +++ b/src/omnibase_core/runtime/envelope_router.py @@ -44,7 +44,7 @@ import logging import threading import time -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING from omnibase_core.decorators.error_handling import standard_error_handling from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode @@ -953,11 +953,10 @@ async def execute_with_handler( # Validate handler return type to harden against misbehaving handlers. # Handlers MUST return ModelOnexEnvelope per ProtocolHandler contract. - # Note: We cast to Any for runtime validation because mypy knows - # ProtocolHandler.execute() is typed to return ModelOnexEnvelope. - # However, runtime validation is essential for defensive programming - # against misbehaving handler implementations that violate the contract. - response_unchecked: Any = cast(Any, response) + # We use object typing for runtime validation: object is a supertype of + # all types, allowing isinstance checks while avoiding Any. + # This defensive programming catches misbehaving handler implementations. + response_unchecked: object = response if not isinstance(response_unchecked, ModelOnexEnvelope): logger.warning( "Handler returned invalid type %s instead of ModelOnexEnvelope " diff --git a/src/omnibase_core/runtime/message_dispatch_engine.py b/src/omnibase_core/runtime/message_dispatch_engine.py index 323941e093..f805544737 100644 --- a/src/omnibase_core/runtime/message_dispatch_engine.py +++ b/src/omnibase_core/runtime/message_dispatch_engine.py @@ -81,7 +81,7 @@ import time from collections.abc import Awaitable, Callable, Sequence from datetime import UTC, datetime -from typing import Any, Literal +from typing import Literal, cast from uuid import uuid4 from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode @@ -126,11 +126,24 @@ "category_mismatch_count", ] +# Type alias for handler return types (without Awaitable wrapper) +HandlerReturnType = ModelHandlerOutput[object] | str | list[object] + +# Type alias for sync handler functions (used for run_in_executor narrowing) +SyncHandlerFunc = Callable[ + [ModelEventEnvelope[object], ProtocolHandlerContext], + HandlerReturnType, +] + # Type alias for handler functions # Handlers take an envelope and context, can be sync or async +# Return type accommodates: +# - ModelHandlerOutput[object] (new pattern) +# - str (legacy topic strings) +# - list[object] (legacy list of topic strings) HandlerFunc = Callable[ - [ModelEventEnvelope[Any], ProtocolHandlerContext], - Any | Awaitable[Any], + [ModelEventEnvelope[object], ProtocolHandlerContext], + HandlerReturnType | Awaitable[HandlerReturnType], ] @@ -368,7 +381,7 @@ def register_handler( Args: handler_id: Unique identifier for this handler handler: Callable that processes messages. Can be sync or async. - Signature: (envelope: ModelEventEnvelope[Any], context: ProtocolHandlerContext) -> Any + Signature: (envelope: ModelEventEnvelope[object], context: ProtocolHandlerContext) -> ModelHandlerOutput[object] category: Message category this handler processes node_kind: The architectural node kind for this handler. Determines which context type the handler receives: @@ -421,7 +434,7 @@ def register_handler( message=( f"Handler for '{handler_id}' must be callable (function or async function). " f"Got {type(handler).__name__}. " - f"Expected signature: (envelope: ModelEventEnvelope[Any]) -> Any" + f"Expected signature: (envelope: ModelEventEnvelope[object], context: ProtocolHandlerContext) -> ModelHandlerOutput[object]" ), error_code=EnumCoreErrorCode.INVALID_PARAMETER, ) @@ -756,7 +769,7 @@ def _build_log_context( async def dispatch( self, topic: str, - envelope: ModelEventEnvelope[Any], + envelope: ModelEventEnvelope[object], ) -> ModelDispatchResult: """ Dispatch a message to matching handlers. @@ -1017,7 +1030,7 @@ async def dispatch( # Step 5: Execute handlers and collect outputs outputs: list[str] = [] - handler_outputs: list[ModelHandlerOutput[Any]] = [] + handler_outputs: list[ModelHandlerOutput[object]] = [] handler_errors: list[str] = [] executed_handler_ids: list[str] = [] @@ -1303,7 +1316,7 @@ def _find_matching_handlers( def _build_handler_context( self, entry: MessageDispatchEngine._HandlerEntry, - envelope: ModelEventEnvelope[Any], + envelope: ModelEventEnvelope[object], ) -> ProtocolHandlerContext: """ Build the appropriate context model for a handler based on its node_kind. @@ -1418,8 +1431,8 @@ def _build_handler_context( async def _execute_handler( self, entry: MessageDispatchEngine._HandlerEntry, - envelope: ModelEventEnvelope[Any], - ) -> Any: + envelope: ModelEventEnvelope[object], + ) -> ModelHandlerOutput[object] | str | list[object]: """ Execute a handler (sync or async) with context injection. @@ -1446,12 +1459,15 @@ async def _execute_handler( return await handler(envelope, context) else: # Sync handler - run in executor to avoid blocking + # Cast to SyncHandlerFunc for type narrowing (runtime check above + # guarantees this is not an async function) + sync_handler = cast(SyncHandlerFunc, handler) loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, handler, envelope, context) + return await loop.run_in_executor(None, sync_handler, envelope, context) async def _publish_outputs_in_order( self, - handler_outputs: Sequence[ModelHandlerOutput[Any]], + handler_outputs: Sequence[ModelHandlerOutput[object]], event_bus: ProtocolEventBus | None = None, publish_intents: bool = False, ) -> int: @@ -1525,15 +1541,17 @@ async def _publish_outputs_in_order( # 1. Collect all events FIRST (facts that happened) # Preserves handler-returned order within category - all_events: list[Any] = [e for output in handler_outputs for e in output.events] + all_events: list[object] = [ + e for output in handler_outputs for e in output.events + ] # 2. Collect all projections (derived state) - all_projections: list[Any] = [ + all_projections: list[object] = [ p for output in handler_outputs for p in output.projections ] # 3. Collect all intents LAST (desired future effects) - all_intents: list[Any] = [ + all_intents: list[object] = [ i for output in handler_outputs for i in output.intents ] diff --git a/src/omnibase_core/types/__init__.py b/src/omnibase_core/types/__init__.py index 4676f6ce3b..f29b0debfb 100644 --- a/src/omnibase_core/types/__init__.py +++ b/src/omnibase_core/types/__init__.py @@ -23,7 +23,8 @@ Solution: Use TYPE_CHECKING and __getattr__ for lazy loading, similar to ModelBaseCollection. """ -# All constraint imports are now lazy-loaded via __getattr__ to prevent circular imports +# Constraint imports are direct at module level for IDE support and import performance. +# The __getattr__ fallback at the bottom provides lazy-loading as a backup mechanism. # Core types for breaking circular dependencies # Converter functions from .constraints import ( @@ -82,6 +83,15 @@ ToolParameterValue, ) +# Compute pipeline type aliases (for pipeline data flows) +from .type_compute_pipeline import ( + PathResolvedValue, + PipelineData, + PipelineDataDict, + StepResultMapping, + TransformInputT, +) + # Effect result type aliases (centralized to avoid primitive soup unions) from .type_effect_result import DbParamType, EffectResultType @@ -368,6 +378,7 @@ from .typed_dict_workflow_outputs import TypedDictWorkflowOutputsDict from .typed_dict_workflow_state import TypedDictWorkflowState from .typed_dict_yaml_dump_kwargs import TypedDictYamlDumpKwargs +from .typed_dict_yaml_dump_options import TypedDictYamlDumpOptions # Utility functions from .util_datetime_parser import parse_datetime @@ -391,6 +402,12 @@ # Schema type aliases "SchemaDict", "StepOutputs", + # Compute pipeline type aliases + "PathResolvedValue", + "PipelineData", + "PipelineDataDict", + "StepResultMapping", + "TransformInputT", # JSON type aliases "JsonPrimitive", "JsonType", @@ -613,6 +630,7 @@ "MappingResultDict", "TypedDictPathResolutionContext", "TypedDictYamlDumpKwargs", + "TypedDictYamlDumpOptions", # Metadata tool collection types "TypedDictCollectionMetadata", "TypedDictCollectionValidation", diff --git a/src/omnibase_core/types/type_compute_pipeline.py b/src/omnibase_core/types/type_compute_pipeline.py new file mode 100644 index 0000000000..601039b437 --- /dev/null +++ b/src/omnibase_core/types/type_compute_pipeline.py @@ -0,0 +1,65 @@ +""" +Compute pipeline type definitions. + +This module provides type definitions for compute pipeline operations, +including type aliases for pipeline data flows and transformations. + +These types replace the use of `Any` with more specific type annotations +that better document the expected data shapes while maintaining the +necessary flexibility for polymorphic data handling. + +Type Aliases: + PipelineData: Type for data flowing through compute pipelines + TransformInput: Type for transformation function inputs + TransformOutput: Type for transformation function outputs + +Design Principles: + - Use `object` for truly polymorphic data (JSON-like values) + - Use union types for known type variants + - Use TypeVar for type-preserving operations (like identity) + +.. versionadded:: 0.4.0 +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TypeVar + +from pydantic import BaseModel + +__all__ = [ + "PipelineData", + "PipelineDataDict", + "TransformInputT", + "PathResolvedValue", + "StepResultMapping", +] + + +# Type alias for data flowing through compute pipelines. +# Pipeline data can be: +# - A dictionary with string keys and arbitrary values +# - A Pydantic BaseModel instance +# +# Note: The `| object` was intentionally removed as it would cause +# the union to collapse to just `object` (all types inherit from object), +# defeating type narrowing. If truly arbitrary objects are needed, +# use `object` directly or a protocol for duck typing. +PipelineData = dict[str, object] | BaseModel + +# Type alias for dictionary-based pipeline data +# Used when the data is known to be a dict structure +PipelineDataDict = dict[str, object] + +# TypeVar for type-preserving transformations +# Used for operations like identity where output type matches input type +TransformInputT = TypeVar("TransformInputT") + +# Type for values resolved from path expressions +# Path resolution can return any JSON-compatible value or object attribute +PathResolvedValue = object + +# Type alias for step results mapping +# Maps step names to their result objects +StepResultMapping = Mapping[str, object] diff --git a/src/omnibase_core/types/typed_dict_yaml_dump_options.py b/src/omnibase_core/types/typed_dict_yaml_dump_options.py new file mode 100644 index 0000000000..3807119500 --- /dev/null +++ b/src/omnibase_core/types/typed_dict_yaml_dump_options.py @@ -0,0 +1,26 @@ +""" +TypedDict for YAML dump options. + +Provides type-safe options for yaml.dump() function calls. +""" + +from typing import TypedDict + + +class TypedDictYamlDumpOptions(TypedDict, total=False): + """Type-safe options for yaml.dump(). + + All fields are optional with sensible defaults applied in _dump_yaml_content. + These map directly to the corresponding yaml.dump() parameters. + """ + + sort_keys: bool + default_flow_style: bool + allow_unicode: bool + explicit_start: bool + explicit_end: bool + indent: int + width: int + + +__all__ = ["TypedDictYamlDumpOptions"] diff --git a/src/omnibase_core/utils/compute_executor.py b/src/omnibase_core/utils/compute_executor.py index fa1ad296b7..9434bf34d9 100644 --- a/src/omnibase_core/utils/compute_executor.py +++ b/src/omnibase_core/utils/compute_executor.py @@ -50,12 +50,20 @@ from collections.abc import Mapping from concurrent.futures import ThreadPoolExecutor from concurrent.futures import TimeoutError as FuturesTimeoutError -from typing import Any + +from pydantic import BaseModel from omnibase_core.types.typed_dict_mapping_result import MappingResultDict logger = logging.getLogger(__name__) +# Type alias for pipeline data - can be dict, Pydantic model, or arbitrary object +# Note: `| object` is intentionally kept here (unlike PipelineData in types module) +# because execute_transformation() can return any type (e.g., JSON_PATH extracts +# arbitrary values from nested structures). This local type differs from the +# stricter PipelineData type alias used for documentation purposes. +PipelineDataType = dict[str, object] | BaseModel | object + from omnibase_core.enums.enum_compute_step_type import EnumComputeStepType from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode from omnibase_core.models.compute.model_compute_execution_context import ( @@ -119,9 +127,9 @@ def _get_error_type(error: ModelOnexError) -> str: def resolve_mapping_path( path: str, - input_data: Any, # Any: accepts dict, Pydantic models, or other objects with attributes + input_data: PipelineDataType, step_results: dict[str, ModelComputeStepResult], -) -> Any: # Any: return type depends on the path being resolved +) -> object: """ Resolve a v1.0 mapping path expression to its value. @@ -187,11 +195,9 @@ def resolve_mapping_path( def execute_mapping_step( step: ModelComputePipelineStep, - input_data: Any, # Any: accepts dict, Pydantic models, or other objects with attributes + input_data: PipelineDataType, step_results: dict[str, ModelComputeStepResult], -) -> ( - MappingResultDict -): # Returns dict with field mappings; values are path-resolved objects +) -> MappingResultDict: """ Execute a mapping step, building output from path expressions. @@ -233,13 +239,13 @@ def execute_mapping_step( return result -def execute_validation_step( +def execute_validation_step[ValidationT]( step: ModelComputePipelineStep, - data: Any, # Any: validation accepts any data type for schema checking + data: ValidationT, schema_registry: ( Mapping[str, object] | None ) = None, # schema definitions vary; reserved for v1.1 -) -> Any: # Any: returns input unchanged (v1.0 pass-through) +) -> ValidationT: """ Execute a validation step against a schema. @@ -302,12 +308,10 @@ def execute_validation_step( def execute_pipeline_step( step: ModelComputePipelineStep, - current_data: Any, # Any: data flows through pipeline with varying types per step - input_data: Any, # Any: original input preserved for mapping step references + current_data: PipelineDataType, + input_data: PipelineDataType, step_results: dict[str, ModelComputeStepResult], -) -> ( - Any -): # Any: output type depends on step_type (transformation, mapping, or validation) +) -> PipelineDataType | MappingResultDict: """ Execute a single pipeline step and return the result. @@ -366,7 +370,7 @@ def execute_pipeline_step( def _execute_pipeline_steps( contract: ModelComputeSubcontract, - input_data: Any, # Any: pipeline input can be any JSON-compatible or Pydantic model + input_data: PipelineDataType, context: ModelComputeExecutionContext, start_time: float, ) -> ModelComputePipelineResult: @@ -570,7 +574,7 @@ def _execute_pipeline_steps( def execute_compute_pipeline( contract: ModelComputeSubcontract, - input_data: Any, # Any: pipeline input can be any JSON-compatible or Pydantic model + input_data: PipelineDataType, context: ModelComputeExecutionContext, ) -> ModelComputePipelineResult: """ diff --git a/src/omnibase_core/utils/compute_path_resolver.py b/src/omnibase_core/utils/compute_path_resolver.py index f7f9459152..ff5baa402b 100644 --- a/src/omnibase_core/utils/compute_path_resolver.py +++ b/src/omnibase_core/utils/compute_path_resolver.py @@ -90,7 +90,8 @@ """ from collections.abc import Mapping -from typing import Any + +from pydantic import BaseModel from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode from omnibase_core.models.errors.model_onex_error import ModelOnexError @@ -98,6 +99,10 @@ TypedDictPathResolutionContext, ) +# Type alias for data that can be traversed during path resolution +# Includes dictionaries, Pydantic models, and arbitrary objects with attributes +TraversableData = dict[str, object] | BaseModel | object + class PathResolutionError(ModelOnexError): """ @@ -205,12 +210,12 @@ def _validate_private_attribute(part: str, path: str) -> None: def _traverse_path_segments( - current: Any, + current: TraversableData, parts: list[str], path: str, *, check_private: bool = True, -) -> Any: +) -> object: """ Traverse through path segments to resolve nested values. @@ -275,10 +280,10 @@ def _traverse_path_segments( def resolve_path( path: str, - data: Any, + data: TraversableData, *, check_private: bool = True, -) -> Any: +) -> object: """ Resolve a simple dot-notation path to its value. @@ -335,8 +340,8 @@ def resolve_path( def resolve_input_path( path: str, - input_data: Any, -) -> Any: + input_data: TraversableData, +) -> object: """ Resolve a pipeline input path expression ($.input prefix). @@ -490,7 +495,7 @@ def resolve_step_path( def resolve_pipeline_path( path: str, - input_data: Any, # Any: input can be dict, Pydantic model, or arbitrary object + input_data: TraversableData, step_results: Mapping[str, object], ) -> object: """ diff --git a/src/omnibase_core/utils/compute_transformations.py b/src/omnibase_core/utils/compute_transformations.py index 47103bde71..ff3ec27edd 100644 --- a/src/omnibase_core/utils/compute_transformations.py +++ b/src/omnibase_core/utils/compute_transformations.py @@ -37,7 +37,6 @@ import re import unicodedata from collections.abc import Callable -from typing import Any from omnibase_core.enums.enum_case_mode import EnumCaseMode from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode @@ -63,7 +62,7 @@ from omnibase_core.models.transformations.model_types import ModelTransformationConfig -def _validate_string_input(value: Any, transform_name: str) -> str: +def _validate_string_input(value: object, transform_name: str) -> str: """ Validate that input is a string type for string transformation functions. @@ -112,12 +111,12 @@ def _validate_string_input(value: Any, transform_name: str) -> str: # See: docs/architecture/NODECOMPUTE_VERSIONING_ROADMAP.md -def transform_identity( - data: Any, # Any: intentionally polymorphic - accepts any input type unchanged +def transform_identity[T]( + data: T, config: ( ModelTransformationConfig | None ), # Aligned with other handlers for uniform registry usage -) -> Any: # Any: output type mirrors input type +) -> T: """ Identity transformation - returns data unchanged. @@ -360,9 +359,9 @@ def transform_unicode(data: str, config: ModelTransformUnicodeConfig) -> str: # def transform_json_path(data, config): return resolve_path(config.path, data) # See: compute_path_resolver.py for unified path resolution logic with EBNF grammar docs def transform_json_path( - data: Any, # Any: accepts dict, Pydantic models, or other objects with attributes + data: dict[str, object] | object, config: ModelTransformJsonPathConfig, -) -> Any: # Any: return type depends on the path being resolved +) -> object: """ Extract data using simple JSONPath-like path notation. @@ -468,8 +467,11 @@ def transform_json_path( # Transformation registry mapping type to handler -# Callable[..., Any] used because handlers have varying signatures -TRANSFORMATION_REGISTRY: dict[EnumTransformationType, Callable[..., Any]] = { +# Handlers take (data, config) and return transformed data +# Using Callable[..., object] because handlers have heterogeneous signatures +# that share the (data, config) pattern but with specific types per transformation. +# The ellipsis indicates unspecified argument types (distinct from Any). +TRANSFORMATION_REGISTRY: dict[EnumTransformationType, Callable[..., object]] = { EnumTransformationType.IDENTITY: transform_identity, EnumTransformationType.REGEX: transform_regex, EnumTransformationType.CASE_CONVERSION: transform_case, @@ -480,10 +482,10 @@ def transform_json_path( def execute_transformation( - data: Any, # Any: input varies based on transformation_type (str for regex/case, any for json_path) + data: str | dict[str, object] | object, transformation_type: EnumTransformationType, config: ModelTransformationConfig | None, -) -> Any: # Any: output type depends on transformation_type +) -> str | dict[str, object] | object: """ Execute a single transformation on input data. diff --git a/src/omnibase_core/utils/fsm_executor.py b/src/omnibase_core/utils/fsm_executor.py index d7833f3fb3..ada79d5608 100644 --- a/src/omnibase_core/utils/fsm_executor.py +++ b/src/omnibase_core/utils/fsm_executor.py @@ -5,12 +5,12 @@ No side effects - returns results and intents. Typing: Strongly typed with FSMContextType for runtime context flexibility. -Context dictionaries use FSMContextType (dict[str, Any]) to allow dynamic execution data +Context dictionaries use FSMContextType (dict[str, object]) to allow dynamic execution data while maintaining type clarity for FSM-specific usage. """ from datetime import UTC, datetime -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Literal, cast if TYPE_CHECKING: from typing import SupportsFloat @@ -420,7 +420,7 @@ async def _evaluate_conditions( return True -def _get_nested_field_value(context: FSMContextType, field_path: str) -> Any: +def _get_nested_field_value(context: FSMContextType, field_path: str) -> object: """ Get value from nested dict using dot notation path. @@ -453,7 +453,7 @@ def _get_nested_field_value(context: FSMContextType, field_path: str) -> Any: # Split and traverse for nested paths segments = field_path.split(".") - current: Any = context + current: object = context for segment in segments: if not isinstance(current, dict): diff --git a/src/omnibase_core/utils/fsm_operators.py b/src/omnibase_core/utils/fsm_operators.py index 5b4f68e1fa..8825adc17d 100644 --- a/src/omnibase_core/utils/fsm_operators.py +++ b/src/omnibase_core/utils/fsm_operators.py @@ -40,13 +40,11 @@ - ModelFSMTransitionCondition: Defines condition expressions """ -from typing import Any - from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode from omnibase_core.models.errors.model_onex_error import ModelOnexError -def evaluate_equals(lhs: Any, rhs: Any) -> bool: +def evaluate_equals(lhs: object, rhs: object) -> bool: """ Evaluate equals operator with string coercion. @@ -90,7 +88,7 @@ def evaluate_equals(lhs: Any, rhs: Any) -> bool: return str(lhs) == str(rhs) -def evaluate_not_equals(lhs: Any, rhs: Any) -> bool: +def evaluate_not_equals(lhs: object, rhs: object) -> bool: """ Evaluate not_equals operator with string coercion. @@ -161,8 +159,8 @@ def is_string_operator(operator: str) -> bool: def evaluate_string_operator( operator: str, - lhs: Any, - rhs: Any, + lhs: object, + rhs: object, ) -> bool: """ Evaluate a string comparison operator. diff --git a/src/omnibase_core/utils/generation/utility_reference_resolver.py b/src/omnibase_core/utils/generation/utility_reference_resolver.py index 5667938af4..dbdedd7957 100644 --- a/src/omnibase_core/utils/generation/utility_reference_resolver.py +++ b/src/omnibase_core/utils/generation/utility_reference_resolver.py @@ -7,11 +7,12 @@ import re from pathlib import Path -from typing import Any from omnibase_core.enums.enum_log_level import EnumLogLevel as LogLevel from omnibase_core.logging.structured import emit_log_event_sync as emit_log_event from omnibase_core.models.utils.generation.model_ref_info import ModelRefInfo +from omnibase_core.protocols.protocol_generation_config import ProtocolGenerationConfig +from omnibase_core.protocols.protocol_import_tracker import ProtocolImportTracker class UtilityReferenceResolver: @@ -52,8 +53,8 @@ class UtilityReferenceResolver: def __init__( self, - config: Any | None = None, - import_tracker: Any | None = None, + config: ProtocolGenerationConfig | None = None, + import_tracker: ProtocolImportTracker | None = None, ): """ Initialize the reference resolver. @@ -65,11 +66,11 @@ def __init__( self.config = config self.import_tracker = import_tracker - def set_config(self, config: Any) -> None: + def set_config(self, config: ProtocolGenerationConfig | None) -> None: """Set the generation config after initialization.""" self.config = config - def set_import_tracker(self, import_tracker: Any) -> None: + def set_import_tracker(self, import_tracker: ProtocolImportTracker | None) -> None: """Set the import tracker after initialization.""" self.import_tracker = import_tracker diff --git a/src/omnibase_core/utils/util_bootstrap.py b/src/omnibase_core/utils/util_bootstrap.py index b95a086222..4a08c9bff3 100644 --- a/src/omnibase_core/utils/util_bootstrap.py +++ b/src/omnibase_core/utils/util_bootstrap.py @@ -9,9 +9,11 @@ registry-centric architecture pattern. """ -from typing import Any, cast +from collections.abc import Callable +from typing import cast from omnibase_core.enums.enum_log_level import EnumLogLevel as LogLevel +from omnibase_core.protocols.logging import ProtocolRegistryNode class _BootstrapMinimalLogger: @@ -22,7 +24,7 @@ def emit_log_event( # stub-ok: Intentional no-op for minimal bootstrap logger level: LogLevel, event_type: str, message: str, - **kwargs: Any, + **kwargs: object, ) -> None: """No-op emit_log_event for bootstrap fallback.""" @@ -31,7 +33,7 @@ def emit_log_event_sync( # stub-ok: Intentional no-op for minimal bootstrap log level: LogLevel, message: str, event_type: str = "generic", - **kwargs: Any, + **kwargs: object, ) -> None: """No-op emit_log_event_sync for bootstrap fallback.""" @@ -40,20 +42,22 @@ async def emit_log_event_async( # stub-ok: Intentional no-op for minimal bootst level: LogLevel, message: str, event_type: str = "generic", - **kwargs: Any, + **kwargs: object, ) -> None: """No-op emit_log_event_async for bootstrap fallback.""" @staticmethod - def trace_function_lifecycle(func: Any) -> Any: + def trace_function_lifecycle[F: Callable[..., object]](func: F) -> F: """No-op decorator for bootstrap.""" return func @staticmethod - def tool_logger_performance_metrics(_threshold_ms: int = 1000) -> Any: + def tool_logger_performance_metrics[F: Callable[..., object]]( + _threshold_ms: int = 1000, + ) -> Callable[[F], F]: """Minimal tool logger performance metrics decorator.""" - def decorator(func: Any) -> Any: + def decorator(func: F) -> F: return func return decorator @@ -90,7 +94,7 @@ def get_service[T](protocol_type: type[T]) -> T | None: return _get_fallback_service(protocol_type) -def get_logging_service() -> Any: +def get_logging_service() -> object: """ Get the logging service with special bootstrap handling. @@ -115,7 +119,7 @@ def emit_log_event( level: LogLevel, event_type: str, message: str, - **kwargs: Any, + **kwargs: object, ) -> None: """ Bootstrap emit_log_event function. @@ -141,7 +145,7 @@ def emit_log_event_sync( level: LogLevel, message: str, event_type: str = "generic", - **kwargs: Any, + **kwargs: object, ) -> None: """ Bootstrap emit_log_event_sync function. @@ -170,7 +174,7 @@ def emit_log_event_sync( # Private helper functions -def _get_registry_node() -> Any | None: +def _get_registry_node() -> ProtocolRegistryNode | None: """ Attempt to find and return the registry node. @@ -208,7 +212,7 @@ def _get_fallback_service[T](protocol_type: type[T]) -> T | None: return None -def _get_minimal_logging_service() -> Any: +def _get_minimal_logging_service() -> _BootstrapMinimalLogger: """ Get minimal logging service for bootstrap scenarios. @@ -240,7 +244,7 @@ def get_available_services() -> list[str]: """ try: registry = _get_registry_node() - if registry and hasattr(registry, "list_services"): + if registry: services = registry.list_services() # Type narrowing: ensure list[str] return list(services) if services else [] diff --git a/src/omnibase_core/utils/util_cli_result_formatter.py b/src/omnibase_core/utils/util_cli_result_formatter.py index 5006e4d579..be9239d304 100644 --- a/src/omnibase_core/utils/util_cli_result_formatter.py +++ b/src/omnibase_core/utils/util_cli_result_formatter.py @@ -6,7 +6,6 @@ """ import json -from typing import Any from omnibase_core.models.cli.model_cli_output_data import ModelCliOutputData @@ -79,7 +78,7 @@ def format_output(output_text: str, output_data: ModelCliOutputData | None) -> s def format_error( error_message: str | None, error_details: str | None = None, - validation_errors: list[Any] | None = None, + validation_errors: list[object] | None = None, ) -> str: """ Format error information for display. diff --git a/src/omnibase_core/utils/util_conflict_resolver.py b/src/omnibase_core/utils/util_conflict_resolver.py index 26af25cd90..6445c1adba 100644 --- a/src/omnibase_core/utils/util_conflict_resolver.py +++ b/src/omnibase_core/utils/util_conflict_resolver.py @@ -41,7 +41,6 @@ """ from collections.abc import Callable -from typing import Any from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode from omnibase_core.enums.enum_reducer_types import EnumConflictResolution @@ -80,25 +79,38 @@ class UtilConflictResolver: def __init__( self, strategy: EnumConflictResolution, - custom_resolver: Callable[..., Any] | None = None, + custom_resolver: Callable[[object, object, str | None], object] | None = None, ): """ Initialize conflict resolver. Args: strategy: Conflict resolution strategy to use - custom_resolver: Optional custom resolution function for CUSTOM strategy + custom_resolver: Optional custom resolution function for CUSTOM strategy. + Signature: (existing_value, new_value, key) -> resolved_value + + Raises: + ModelOnexError: If CUSTOM strategy is specified without a custom_resolver """ + if strategy == EnumConflictResolution.CUSTOM and custom_resolver is None: + raise ModelOnexError( + error_code=EnumCoreErrorCode.VALIDATION_ERROR, + message="CUSTOM conflict resolution strategy requires a custom_resolver function", + context={ + "strategy": strategy.value, + "custom_resolver": "None", + }, + ) self.strategy = strategy self.custom_resolver = custom_resolver self.conflicts_count = 0 def resolve( self, - existing_value: Any, - new_value: Any, + existing_value: object, + new_value: object, key: str | None = None, - ) -> Any: + ) -> object: """ Resolve conflict between existing and new values. @@ -136,7 +148,7 @@ def resolve( # Default to last wins return new_value - def _merge_values(self, existing: Any, new: Any) -> Any: + def _merge_values(self, existing: object, new: object) -> object: """ Attempt to merge two values intelligently. @@ -169,7 +181,7 @@ def _merge_values(self, existing: Any, new: Any) -> Any: return new -def __getattr__(name: str) -> Any: +def __getattr__(name: str) -> type[UtilConflictResolver]: """ Lazy loading for deprecated aliases per OMN-1071 renaming. diff --git a/src/omnibase_core/utils/util_hash.py b/src/omnibase_core/utils/util_hash.py index 4a255c383f..d9217174b8 100644 --- a/src/omnibase_core/utils/util_hash.py +++ b/src/omnibase_core/utils/util_hash.py @@ -15,7 +15,6 @@ """ import hashlib -from typing import Any from uuid import UUID @@ -56,7 +55,7 @@ def deterministic_hash_int(value: str) -> int: return int(hashlib.sha256(value.encode()).hexdigest(), 16) -def deterministic_cache_key(*args: Any, **kwargs: Any) -> str: +def deterministic_cache_key(*args: object, **kwargs: object) -> str: """Generate a deterministic cache key from arguments. Creates a stable cache key by hashing the string representation diff --git a/src/omnibase_core/utils/util_logger_cache.py b/src/omnibase_core/utils/util_logger_cache.py index 1520d3b3d2..40cb3867bb 100644 --- a/src/omnibase_core/utils/util_logger_cache.py +++ b/src/omnibase_core/utils/util_logger_cache.py @@ -7,17 +7,16 @@ """ import threading -from typing import Any class _LoggerCache: """Thread-safe logger cache holder.""" - _instance: Any = None + _instance: object | None = None _lock = threading.Lock() @classmethod - def get(cls) -> Any | None: + def get(cls) -> object | None: """Get cached logger instance. Thread-safe access to the cached logger. @@ -26,7 +25,7 @@ def get(cls) -> Any | None: return cls._instance @classmethod - def set(cls, logger: Any) -> None: + def set(cls, logger: object) -> None: """Set cached logger instance. Thread-safe modification of the cached logger. diff --git a/src/omnibase_core/utils/util_payload_migration.py b/src/omnibase_core/utils/util_payload_migration.py index d01d275763..0b8b5cfb7a 100644 --- a/src/omnibase_core/utils/util_payload_migration.py +++ b/src/omnibase_core/utils/util_payload_migration.py @@ -22,14 +22,13 @@ - ModelEventPayloadUnion for the complete list of typed payloads """ -from typing import Any from uuid import UUID from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode -# Type alias for legacy dict payloads - intentionally accepts Any values for migration -# ONEX_EXCLUDE: dict_str_any - migration utility requires untyped dict input -LegacyDictPayload = dict[str, Any] +# Type alias for legacy dict payloads - accepts object values for migration +# This migration utility handles untyped dict input from legacy code +LegacyDictPayload = dict[str, object] from omnibase_core.enums.enum_node_kind import EnumNodeKind from omnibase_core.models.errors.model_onex_error import ModelOnexError from omnibase_core.models.events.payloads.model_event_payload_union import ( @@ -268,9 +267,10 @@ def _prepare_dict_for_conversion( # Convert string UUIDs to UUID objects for common UUID fields uuid_fields = ["node_id", "runtime_id", "graph_id", "subscription_id", "event_id"] for field in uuid_fields: - if field in data and isinstance(data[field], str): + field_value = data.get(field) + if isinstance(field_value, str): try: - data[field] = UUID(data[field]) + data[field] = UUID(field_value) except ValueError: # Leave as-is; Pydantic will handle the validation error pass diff --git a/src/omnibase_core/utils/util_protocol_cache_holder.py b/src/omnibase_core/utils/util_protocol_cache_holder.py index 8bfd775988..ab467d98f9 100644 --- a/src/omnibase_core/utils/util_protocol_cache_holder.py +++ b/src/omnibase_core/utils/util_protocol_cache_holder.py @@ -7,19 +7,16 @@ Thread Safety: All methods use internal locking for thread-safe access. - -BOUNDARY_LAYER_EXCEPTION: - Uses Any for formatter/output_handler types because the actual protocol - types (ProtocolSmartLogFormatter, ProtocolContextAwareOutputHandler) are - planned infrastructure not yet implemented. The logging system gracefully - handles their absence with fallback behavior. """ import threading import time -from typing import Any from omnibase_core.constants import DEFAULT_CACHE_TTL_SECONDS +from omnibase_core.protocols.logging import ( + ProtocolContextAwareOutputHandler, + ProtocolSmartLogFormatter, +) class _ProtocolCacheHolder: @@ -31,20 +28,16 @@ class _ProtocolCacheHolder: Thread Safety: All public methods are thread-safe using internal locking. - - BOUNDARY_LAYER_EXCEPTION: - Uses Any for cached services because protocol types are not yet defined. """ - # BOUNDARY_LAYER_EXCEPTION: Any required - protocol types not yet implemented - _formatter: Any = None - _output_handler: Any = None + _formatter: ProtocolSmartLogFormatter | None = None + _output_handler: ProtocolContextAwareOutputHandler | None = None _timestamp: float = 0.0 _ttl: float = float(DEFAULT_CACHE_TTL_SECONDS) # 5 minutes TTL _lock: threading.Lock = threading.Lock() @classmethod - def get_formatter(cls) -> Any: + def get_formatter(cls) -> ProtocolSmartLogFormatter | None: """Get cached formatter (thread-safe).""" with cls._lock: if cls._is_expired(): @@ -52,14 +45,14 @@ def get_formatter(cls) -> Any: return cls._formatter @classmethod - def set_formatter(cls, formatter: Any) -> None: + def set_formatter(cls, formatter: ProtocolSmartLogFormatter | None) -> None: """Set cached formatter (thread-safe).""" with cls._lock: cls._formatter = formatter cls._timestamp = time.time() @classmethod - def get_output_handler(cls) -> Any: + def get_output_handler(cls) -> ProtocolContextAwareOutputHandler | None: """Get cached output handler (thread-safe).""" with cls._lock: if cls._is_expired(): @@ -67,7 +60,9 @@ def get_output_handler(cls) -> Any: return cls._output_handler @classmethod - def set_output_handler(cls, handler: Any) -> None: + def set_output_handler( + cls, handler: ProtocolContextAwareOutputHandler | None + ) -> None: """Set cached output handler (thread-safe).""" with cls._lock: cls._output_handler = handler diff --git a/src/omnibase_core/utils/util_safe_yaml_loader.py b/src/omnibase_core/utils/util_safe_yaml_loader.py index 471b4adadc..181c53a1d1 100644 --- a/src/omnibase_core/utils/util_safe_yaml_loader.py +++ b/src/omnibase_core/utils/util_safe_yaml_loader.py @@ -8,7 +8,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, cast +from typing import Unpack, cast import yaml from pydantic import BaseModel, ValidationError @@ -19,7 +19,8 @@ from omnibase_core.models.core.model_custom_properties import ModelCustomProperties from omnibase_core.models.errors.model_onex_error import ModelOnexError from omnibase_core.models.examples.model_schema_example import ModelSchemaExample -from omnibase_core.models.utils import ModelYamlOption, ModelYamlValue +from omnibase_core.models.utils import ModelYamlValue +from omnibase_core.types.typed_dict_yaml_dump_options import TypedDictYamlDumpOptions # ModelYamlWithExamples import removed - using direct YAML parsing @@ -184,20 +185,20 @@ def load_yaml_content_as_model[T: BaseModel](content: str, model_cls: type[T]) - def _dump_yaml_content( data: object, - sort_keys: bool = False, - default_flow_style: bool = False, - allow_unicode: bool = True, - explicit_start: bool = False, - explicit_end: bool = False, - indent: int = 2, - width: int = 120, - **kwargs: Any, # Any: required for yaml.dump() external API compatibility + **kwargs: Unpack[TypedDictYamlDumpOptions], ) -> str: """ Internal function to dump data to YAML format with security restrictions. This is the only place where yaml.dump should be used in the codebase. All other code should use this function through proper Pydantic model serialization. + + Args: + data: Data to serialize to YAML + **kwargs: Type-safe YAML dump options (see TypedDictYamlDumpOptions) + + Returns: + YAML string representation of the data """ try: # Convert ModelYamlValue to serializable data @@ -205,14 +206,17 @@ def _dump_yaml_content( data.to_serializable() if isinstance(data, ModelYamlValue) else data ) - # Convert ModelYamlOption values to Python values - # ONEX_EXCLUDE: dict_str_any - required for yaml.dump() external API compatibility - yaml_kwargs: dict[str, Any] = { - k: v.to_value() if isinstance(v, ModelYamlOption) else v - for k, v in kwargs.items() - } - - yaml_str = yaml.dump( + # Extract options with defaults + sort_keys = kwargs.get("sort_keys", False) + default_flow_style = kwargs.get("default_flow_style", False) + allow_unicode = kwargs.get("allow_unicode", True) + explicit_start = kwargs.get("explicit_start", False) + explicit_end = kwargs.get("explicit_end", False) + indent = kwargs.get("indent", 2) + width = kwargs.get("width", 120) + + # Call yaml.dump with explicit parameters for type safety + yaml_str: str = yaml.dump( serializable_data, sort_keys=sort_keys, default_flow_style=default_flow_style, @@ -221,7 +225,6 @@ def _dump_yaml_content( explicit_end=explicit_end, indent=indent, width=width, - **yaml_kwargs, ) # Normalize line endings and Unicode characters yaml_str = yaml_str.replace("\xa0", " ") @@ -263,7 +266,7 @@ def _dump_yaml_content( def serialize_pydantic_model_to_yaml( model: BaseModel, comment_prefix: str = "", - **yaml_options: Any, # Any: required for yaml.dump() external API compatibility + **yaml_options: Unpack[TypedDictYamlDumpOptions], ) -> str: """ Serialize a Pydantic model to YAML format through the centralized dumper. @@ -271,7 +274,7 @@ def serialize_pydantic_model_to_yaml( Args: model: Pydantic model instance to serialize comment_prefix: Optional prefix for each line (for comment blocks) - **yaml_options: Additional options to pass to YAML dumper + **yaml_options: Type-safe YAML dump options (see TypedDictYamlDumpOptions) Returns: YAML string representation of the model @@ -315,7 +318,7 @@ def serialize_pydantic_model_to_yaml( def serialize_data_to_yaml( data: object, comment_prefix: str = "", - **yaml_options: Any, # Any: required for yaml.dump() external API compatibility + **yaml_options: Unpack[TypedDictYamlDumpOptions], ) -> str: """ Serialize arbitrary data to YAML format through the centralized dumper. @@ -326,7 +329,7 @@ def serialize_data_to_yaml( Args: data: Data to serialize (dict, list, or other YAML-serializable types) comment_prefix: Optional prefix for each line (for comment blocks) - **yaml_options: Additional options to pass to YAML dumper + **yaml_options: Type-safe YAML dump options (see TypedDictYamlDumpOptions) Returns: YAML string representation of the data @@ -416,7 +419,7 @@ def extract_example_from_schema( if not isinstance(example, dict): raise ModelOnexError( error_code=EnumCoreErrorCode.VALIDATION_ERROR, - message=f"Example at index {example_index} is not a dict[str, Any]in schema: {schema_path}", + message=f"Example at index {example_index} is not a dict[str, Any] in schema: {schema_path}", details=ModelErrorContext.with_context( { "operation": ModelSchemaValue.from_value( @@ -431,7 +434,7 @@ def extract_example_from_schema( ), ) - # Convert example dict[str, Any]to ModelCustomProperties + # Convert example dict[str, Any] to ModelCustomProperties custom_props = ModelCustomProperties() if isinstance(example, dict): for key, value in example.items(): diff --git a/src/omnibase_core/utils/util_security.py b/src/omnibase_core/utils/util_security.py index 02186bd82e..7cfbf55c78 100644 --- a/src/omnibase_core/utils/util_security.py +++ b/src/omnibase_core/utils/util_security.py @@ -2,7 +2,6 @@ import re from collections.abc import Mapping, Sequence -from typing import Any from omnibase_core.enums.enum_credential_strength import EnumCredentialStrength from omnibase_core.models.security.model_credential_audit_report import ( @@ -99,7 +98,7 @@ def mask_credential( @staticmethod def mask_dict_credentials( - data: Mapping[str, Any], + data: Mapping[str, object], sensitive_patterns: set[str] | None = None, recursive: bool = True, ) -> dict[str, object]: @@ -148,7 +147,7 @@ def mask_dict_credentials( @staticmethod def _mask_list_credentials( - data: Sequence[Any], + data: Sequence[object], sensitive_patterns: set[str], ) -> list[object]: """ @@ -338,7 +337,7 @@ def create_secure_mask_config( @staticmethod def audit_credential_usage( - data: Mapping[str, Any], + data: Mapping[str, object], config: ModelSecureMaskConfig | None = None, ) -> ModelCredentialAuditReport: """ @@ -360,7 +359,7 @@ def audit_credential_usage( audit_report = ModelCredentialAuditReport() - def _audit_recursive(obj: Any, path: str = "") -> None: + def _audit_recursive(obj: object, path: str = "") -> None: """Recursively audit object for credentials.""" if isinstance(obj, Mapping): for key, value in obj.items(): diff --git a/src/omnibase_core/utils/util_simple_fallback_logger.py b/src/omnibase_core/utils/util_simple_fallback_logger.py deleted file mode 100644 index 1396a8f08c..0000000000 --- a/src/omnibase_core/utils/util_simple_fallback_logger.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Simple Fallback Logger. - -A simple fallback logger that just prints to stdout/stderr, -used when the full logging infrastructure is not available -during bootstrap or circular dependency scenarios. -""" - -from typing import Any - - -class _SimpleFallbackLogger: - """Simple fallback logger that just prints to stdout.""" - - def emit(self, level: Any, message: str, correlation_id: Any) -> None: - """Emit log message to stdout.""" - import sys - - # Import LogLevel here to avoid circular imports - from omnibase_core.enums.enum_log_level import EnumLogLevel as LogLevel - - # ERROR and CRITICAL levels go to stderr, others to stdout - is_error = level in (LogLevel.ERROR, LogLevel.CRITICAL, LogLevel.FATAL) - # print-ok: fallback logger intentionally uses print when logging infra unavailable - print( - f"[{level.name}] {correlation_id}: {message}", - file=sys.stderr if is_error else sys.stdout, - ) diff --git a/src/omnibase_core/utils/util_streaming_window.py b/src/omnibase_core/utils/util_streaming_window.py index 262e4661b3..4552648ed8 100644 --- a/src/omnibase_core/utils/util_streaming_window.py +++ b/src/omnibase_core/utils/util_streaming_window.py @@ -43,7 +43,6 @@ from collections import deque from datetime import datetime, timedelta -from typing import Any class UtilStreamingWindow: @@ -90,10 +89,10 @@ def __init__(self, window_size_ms: int, overlap_ms: int = 0): """ self.window_size_ms = window_size_ms self.overlap_ms = overlap_ms - self.buffer: deque[tuple[Any, datetime]] = deque() + self.buffer: deque[tuple[object, datetime]] = deque() self.window_start = datetime.now() - def add_item(self, item: Any) -> bool: + def add_item(self, item: object) -> bool: """ Add item to window. @@ -110,7 +109,7 @@ def add_item(self, item: Any) -> bool: window_duration = (current_time - self.window_start).total_seconds() * 1000 return window_duration >= self.window_size_ms - def get_window_items(self) -> list[Any]: + def get_window_items(self) -> list[object]: """ Get all items in current window. @@ -140,7 +139,7 @@ def advance_window(self) -> None: self.window_start = datetime.now() -def __getattr__(name: str) -> Any: +def __getattr__(name: str) -> object: """ Lazy loading for deprecated aliases per OMN-1071 renaming. diff --git a/src/omnibase_core/utils/util_tool_logger_code_block.py b/src/omnibase_core/utils/util_tool_logger_code_block.py index f31ed9acea..2c8f496abc 100644 --- a/src/omnibase_core/utils/util_tool_logger_code_block.py +++ b/src/omnibase_core/utils/util_tool_logger_code_block.py @@ -1,5 +1,7 @@ from __future__ import annotations +from types import TracebackType + """ ToolLoggerCodeBlock @@ -14,20 +16,22 @@ """ -from typing import Any - - class ToolLoggerCodeBlock: """Tool logger code block for performance tracking.""" def __init__( # stub-ok: Minimal logging service provides pass-through implementation - self, *args: Any, **kwargs: Any + self, *args: object, **kwargs: object ) -> None: """Initialize tool logger code block.""" - def __enter__(self) -> Any: + def __enter__(self) -> ToolLoggerCodeBlock: """Enter context manager.""" return self - def __exit__(self, *args: Any) -> None: + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: """Exit context manager.""" diff --git a/tests/unit/container/test_service_registry_extended.py b/tests/unit/container/test_service_registry_extended.py index f338d4fd29..1c37974175 100644 --- a/tests/unit/container/test_service_registry_extended.py +++ b/tests/unit/container/test_service_registry_extended.py @@ -445,21 +445,73 @@ async def test_resolve_service_tracks_performance_metrics( assert status.average_resolution_time_ms is not None assert status.average_resolution_time_ms >= 0.0 - # ===== validate_service_health Not Implemented (Lines 733-747) ===== + # ===== validate_service_health Tests (Lines 812-883) ===== @pytest.mark.asyncio - async def test_validate_service_health_not_implemented( + async def test_validate_service_health_not_found( self, registry: ServiceRegistry ) -> None: - """Test validate_service_health raises not implemented (Lines 743-747).""" + """Test validate_service_health returns unhealthy for non-existent registration.""" fake_id = uuid4() - with pytest.raises(ModelOnexError) as exc_info: - await registry.validate_service_health(fake_id) + result = await registry.validate_service_health(fake_id) + + assert result.is_healthy is False + assert result.health_status == "unhealthy" + assert result.registration_id == fake_id + assert result.error_message is not None + assert "Registration not found" in result.error_message + assert "available_registrations" in result.diagnostics - assert "Service health validation not yet fully implemented" in str( - exc_info.value + @pytest.mark.asyncio + async def test_validate_service_health_healthy( + self, registry: ServiceRegistry + ) -> None: + """Test validate_service_health returns healthy for valid registration with instance.""" + # Register and store an instance + service_instance = MockServiceImplementation(name="health_test") + registration_id = await registry.register_instance( + interface=ITestService, + instance=service_instance, + scope="global", ) + # Validate health + result = await registry.validate_service_health(registration_id) + + assert result.is_healthy is True + assert result.health_status == "healthy" + assert result.registration_id == registration_id + assert result.error_message is None + assert result.instance_count >= 1 + assert result.response_time_ms is not None + assert result.response_time_ms >= 0.0 + + @pytest.mark.asyncio + async def test_validate_service_health_unhealthy_status( + self, registry: ServiceRegistry + ) -> None: + """Test validate_service_health returns unhealthy when registration is marked unhealthy.""" + # Register an instance + service_instance = MockServiceImplementation(name="unhealthy_test") + registration_id = await registry.register_instance( + interface=ITestService, + instance=service_instance, + scope="global", + ) + + # Mark registration as unhealthy + registration = await registry.get_registration(registration_id) + assert registration is not None + registration.health_status = "unhealthy" + + # Validate health + result = await registry.validate_service_health(registration_id) + + assert result.is_healthy is False + assert result.health_status == "unhealthy" + assert result.error_message is not None + assert "Service marked as unhealthy" in result.error_message + # ===== update_service_configuration Not Found (Lines 762-763) ===== @pytest.mark.asyncio