"""Bounded retry of a simulated read. No network requests or paid APIs."""
from collections.abc import Callable
from dataclasses import dataclass
import time


class TemporaryFailure(Exception):
    pass


class RetryExhausted(Exception):
    pass


@dataclass(frozen=True)
class RetryPolicy:
    max_attempts: int = 3  # Includes the first attempt.
    max_elapsed: float = 10.0
    base_delay: float = 0.5
    delay_cap: float = 2.0

    def __post_init__(self):
        if self.max_attempts < 1:
            raise ValueError("max_attempts must be at least 1")
        if self.max_elapsed <= 0 or self.base_delay < 0 or self.delay_cap < 0:
            raise ValueError("invalid time limit or delay")


def retry_read(operation: Callable[[], str], policy: RetryPolicy,
               *, clock=time.monotonic, sleep=time.sleep) -> str:
    """Only TemporaryFailure retries; no deadline can interrupt operation()."""
    started = clock()
    for attempt in range(1, policy.max_attempts + 1):
        if clock() - started >= policy.max_elapsed:
            raise RetryExhausted("time budget exhausted before next attempt")
        try:
            return operation()
        except TemporaryFailure as error:
            if attempt == policy.max_attempts:
                raise RetryExhausted(f"stopped after {attempt} attempts") from error
            delay = min(policy.base_delay * (2 ** (attempt - 1)), policy.delay_cap)
            # Do not spend the remaining budget waiting for another attempt.
            if clock() - started + delay >= policy.max_elapsed:
                raise RetryExhausted("insufficient time for next attempt") from error
            sleep(delay)
    raise AssertionError("unreachable")


if __name__ == "__main__":
    calls = 0

    def simulated_read():
        global calls
        calls += 1
        if calls < 3:
            raise TemporaryFailure("simulated upstream unavailable")
        return "draft source loaded"

    result = retry_read(simulated_read, RetryPolicy())
    print(f"{result}; attempts={calls}")
