Metadata-Version: 2.4
Name: tryr
Version: 0.1.0
Summary: Retries that get out of your way. Decorators and helpers for sync and async retry with backoff, jitter, and circuit breaker.
Project-URL: Homepage, https://github.com/wowori/tryr
Project-URL: Issues, https://github.com/wowori/tryr/issues
Project-URL: Source, https://github.com/wowori/tryr
Author-email: wowori <wowori@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: async,backoff,circuit-breaker,resilience,retry
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: hypothesis>=6.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Provides-Extra: test
Requires-Dist: hypothesis>=6.0; extra == 'test'
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# tryr

> Retries that get out of your way.

A small, focused retry library for Python. Decorators and context managers for
sync and async code, with backoff strategies, jitter, hooks, and an optional
circuit breaker. Zero required dependencies.

## Why

- `tenacity` is mature but has 40+ parameters.
- `backoff` hasn't been updated since 2019 and async support is awkward.
- You just want a clean decorator that retries on the right exceptions, sleeps
  with a sensible backoff, and gets out of your way.

That's `tryr`.

## Install

```bash
pip install tryr
```

## Quick start

```python
from tryr import retry

@retry(max_attempts=5, retry_on=(ConnectionError, TimeoutError))
def fetch_user(user_id: int) -> dict:
    return requests.get(f"https://api.example.com/users/{user_id}").json()
```

Async version:

```python
from tryr import aretry

@aretry(max_attempts=3, backoff="exponential", jitter="full")
async def fetch_async(url: str) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as r:
            return await r.json()
```

## Backoff strategies

```python
@retry(backoff="fixed", initial_delay=1.0)               # 1, 1, 1, ...
@retry(backoff="linear", initial_delay=0.5, max_delay=10) # 0.5, 1.0, 1.5, ...
@retry(backoff="exponential", initial_delay=0.5, max_delay=30)  # 0.5, 1, 2, 4, 8, 16, 30, ...
```

Add jitter to avoid thundering herd:

```python
@retry(backoff="exponential", jitter="full")   # random in [0, delay]
@retry(backoff="exponential", jitter="equal")  # delay/2 + random(0, delay/2)
```

## Exception classification

By default, `tryr` retries on `Exception` if you don't specify `retry_on`. Be
explicit in real code:

```python
@retry(
    retry_on=(requests.RequestException, TimeoutError),
    give_up_on=(requests.HTTPError,),  # 4xx won't be retried
)
def fetch(): ...
```

## Hooks

```python
@retry(
    max_attempts=5,
    on_retry=lambda attempt, exc, delay: logger.warning(
        "attempt %d failed: %s (sleeping %.2fs)", attempt, exc, delay
    ),
    on_give_up=lambda attempts, exc: metrics.counter("retries.give_up").inc(),
)
def f(): ...
```

## Circuit breaker

Stop hammering a dead service:

```python
from tryr import circuit_breaker

@circuit_breaker(failure_threshold=5, reset_timeout=30.0)
@retry(max_attempts=3)
def call_external_api(): ...
```

After 5 consecutive failures the breaker opens for 30 seconds, during which
calls fail fast with `CircuitOpenError`. Then it half-opens for a probe call.

## Context manager

For blocks that aren't functions:

```python
from tryr import Retrier

with Retrier(max_attempts=3, backoff="exponential") as r:
    while r.should_retry():
        try:
            do_thing()
            break
        except TransientError as e:
            r.record_failure(e)
```

## Development

```bash
git clone https://github.com/wowori/tryr
cd tryr
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
pytest
```

## License

MIT
