Source code for wraquant.core.decorators

"""Decorators for optional dependency gating, caching, and validation."""

from __future__ import annotations

import functools
import hashlib
import time
from typing import Any, Callable, TypeVar

from wraquant._lazy import check_extra
from wraquant.core.exceptions import MissingDependencyError

F = TypeVar("F", bound=Callable[..., Any])


[docs] def requires_extra(group: str) -> Callable[[F], F]: """Decorator that raises MissingDependencyError if an optional group is missing. Parameters: group: The PDM optional dependency group name (e.g., 'market-data'). Returns: Decorated function that checks for the dependency before execution. Example: >>> @requires_extra('market-data') ... def fetch_yahoo(symbol: str) -> pd.DataFrame: ... import yfinance ... return yfinance.download(symbol) """ def decorator(func: F) -> F: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: if not check_extra(group): raise MissingDependencyError( package=group, extra_group=group, ) return func(*args, **kwargs) return wrapper # type: ignore[return-value] return decorator
[docs] def cache_result(ttl: int = 3600) -> Callable[[F], F]: """Simple in-memory cache with TTL for expensive computations. Parameters: ttl: Time-to-live in seconds. Defaults to 1 hour. Returns: Decorated function with caching behavior. """ cache: dict[str, tuple[float, Any]] = {} def decorator(func: F) -> F: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: key = hashlib.md5( f"{func.__name__}:{args}:{sorted(kwargs.items())}".encode(), usedforsecurity=False, ).hexdigest() now = time.monotonic() if key in cache: timestamp, result = cache[key] if now - timestamp < ttl: return result result = func(*args, **kwargs) cache[key] = (now, result) return result wrapper.cache_clear = cache.clear # type: ignore[attr-defined] return wrapper # type: ignore[return-value] return decorator
[docs] def validate_input(func: F) -> F: """Decorator that validates function inputs using type hints. Currently a pass-through that documents the intent for pydantic validation integration in the future. """ @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) return wrapper # type: ignore[return-value]