"""Small helpers worth having in every project."""
import logging
import time
from contextlib import contextmanager
from functools import lru_cache, wraps

log = logging.getLogger(__name__)

TIMINGS = {}

__all__ = ["timer", "retry", "cached", "managed", "TIMINGS"]


def timer(func):
    """Record each call's duration in TIMINGS[func.__name__]."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        started = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            TIMINGS.setdefault(func.__name__, []).append(time.perf_counter() - started)
    return wrapper


def retry(attempts=3, exceptions=(Exception,), sleep=time.sleep):
    """Retry on `exceptions`, backing off 1s, 2s, 4s. Re-raises on the last try."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as err:
                    if attempt == attempts:
                        log.error("%s failed after %d attempts: %s",
                                  func.__name__, attempts, err)
                        raise
                    log.warning("%s failed (%s); retrying", func.__name__, err)
                    if sleep:
                        sleep(2 ** (attempt - 1))
        return wrapper
    return decorator


def cached(maxsize=128):
    """lru_cache, named for what it is. Only ever put this on a pure function."""
    return lru_cache(maxsize=maxsize)


@contextmanager
def managed(resource, close="close"):
    """Yield `resource`, then call its close method whatever happens."""
    try:
        yield resource
    finally:
        closer = getattr(resource, close, None)
        if callable(closer):
            closer()

