Source code for julee.domain.use_cases.decorators
"""
Decorators for use case step error handling and logging.
This module provides decorators that implement consistent error handling
and logging patterns across all use cases in the julee domain,
following the patterns established in the sample use cases.
"""
import logging
from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any, TypeVar
[docs]
logger = logging.getLogger(__name__)
[docs]
F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
[docs]
def try_use_case_step(
step_name: str,
extra_context: dict[str, Any] | None = None,
) -> Callable[[F], F]:
"""
Decorator that wraps use case steps with consistent error handling and
logging.
This decorator provides the same error handling and logging pattern used
in the sample use cases, eliminating boilerplate and ensuring consistency
across all use case implementations.
Args:
step_name: Name of the step (e.g., "assembly_id_generation")
extra_context: Optional additional context to include in all log
messages
Returns:
Decorated function with consistent error handling and logging
Example:
>>> @try_use_case_step(
... "assembly_id_generation", {"document_id": "doc-123"}
... )
>>> async def generate_assembly_id(self) -> str:
... return await self.assembly_repo.generate_id()
"""
def decorator(func: F) -> F:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
# Build base logging context
log_context = {
"debug_step": f"before_{step_name}",
}
if extra_context:
log_context.update(extra_context)
# Pre-step logging
logger.debug(
f"About to {step_name}",
extra=log_context,
)
try:
# Execute the wrapped function
result = await func(*args, **kwargs)
# Success logging with result information
success_context = {
"debug_step": f"{step_name}_success",
}
if extra_context:
success_context.update(extra_context)
# Add result to logging context if it's a simple type
if isinstance(result, str):
success_context["result"] = result
elif hasattr(result, "id"):
success_context["result_id"] = result.id
elif hasattr(result, "__dict__"):
# For complex objects, just log the type
success_context["result_type"] = type(result).__name__
logger.debug(
f"{step_name} completed successfully",
extra=success_context,
)
return result
except Exception as e:
# Error logging
error_context = {
"error": str(e),
"error_type": type(e).__name__,
"debug_step": f"{step_name}_failed",
}
if extra_context:
error_context.update(extra_context)
logger.error(
f"Failed to {step_name}",
extra=error_context,
)
raise
return wrapper # type: ignore[return-value]
return decorator