Skip to content

Latest commit

Β 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌿 tranq

Calm error handling for Python – decorator-based, zero boilerplate.

PyPI GitHub Python 3.9+ Tests License


πŸ“– Table of Contents


🧘 Why tranq?

Writing repetitive try / except blocks clutters your code and hides the business logic. tranq gives you declarative error handling with decorators, context managers, and a rich set of retry strategies – so you can focus on what your code does, not how it recovers from failures.

Feature Description
🧘 Tranquil Clean, readable, and maintainable.
πŸ” Smart retries Exponential, linear, Fibonacci backoff, jitter, and max delay.
🚦 Circuit Breaker Prevent cascading failures (sync & async).
πŸ§ͺ Conditional retry On specific exceptions or result values.
πŸ“¦ Retry groups All-or-nothing execution for multiple functions.
πŸ“Š Built‑in metrics & profiling Monitor performance and error rates.
πŸ“ Pluggable reporters Send errors to files (JSON), Sentry, Slack, or custom destinations.
🧩 Context manager API Use tranq.retry(...) with all decorator features.
πŸ”§ Stateful retry Persist attempt count across calls (thread/async safe).
🎭 Mock error injection Test your error handling with ease.
πŸ’‰ Dependency injection Inject dependencies into decorated functions.
🌐 Global policy Set defaults once, override per function.

πŸ“¦ Installation

pip install tranq

Requires Python 3.9 or later.

For rich logging output:

pip install tranq[rich]

For development:

pip install tranq[dev]

⚑ Quick Start

Decorator (@handle)

import tranq

@tranq.handle(on=ValueError, retry=3, delay=0.5, backoff=2.0)
def risky():
    ...

Async (@handle_async)

@tranq.handle_async(on=ConnectionError, retry=2, fallback=lambda: "offline")
async def fetch_data():
    ...

Circuit Breaker

cb = tranq.CircuitBreaker(failure_threshold=5, timeout=60)

@tranq.handle(circuit_breaker=cb)
def call_unstable_service():
    ...

Context Manager (full feature parity)

with tranq.retry(on=ValueError, retry=2, retry_if=lambda e: "503" in str(e)) as ctx:
    result = ctx.run(my_function, arg1, arg2)

Retry Group (all‑or‑nothing)

group = tranq.retry_group(step1, step2, step3, on=Exception, retry=1)
results = group.run()  # if any step fails, all are retried together

πŸ” Features in Depth

1. Retry with Backoff

Choose from exponential, linear, or Fibonacci backoff. Add jitter to avoid thundering herds.

@tranq.handle(
    on=TimeoutError,
    retry=5,
    delay=0.1,
    backoff=2.0,
    backoff_strategy="exponential",  # "linear", "fibonacci", or custom callable
    max_delay=10.0,
    jitter=True,
)
def fetch():
    ...

2. Conditional Retry

  • retry_if – retry only when the exception matches a condition.
  • retry_on_result – retry if the result is unacceptable (e.g., None).
@tranq.handle(
    on=requests.RequestException,
    retry_if=lambda e: e.response.status_code == 429,  # rate‑limit
    retry=3,
)
def call_api():
    ...

@tranq.handle(
    retry_on_result=lambda result: result is None,
    retry=2,
)
def get_data():
    ...

3. Error Handlers (on_error)

Run different callbacks for different exception types.

def log_warning(e):
    print(f"Warning: {e}")

@tranq.handle(
    on=(ValueError, ConnectionError),
    on_error={ValueError: log_warning},
)
def process():
    ...

4. Circuit Breaker (Sync & Async)

from tranq import CircuitBreaker, AsyncCircuitBreaker

cb = CircuitBreaker(failure_threshold=3, timeout=30, half_open_requests=1)

@tranq.handle(circuit_breaker=cb)
def sync_call():
    ...

acb = AsyncCircuitBreaker(failure_threshold=3, timeout=30)

@tranq.handle_async(circuit_breaker=acb)
async def async_call():
    ...

5. Stateful Retry (thread‑safe)

Uses contextvars to isolate counters, safe for async and threaded code.

@tranq.handle(on=ValueError, retry=3, stateful=True)
def process_item(item):
    ...

6. Reporters (JSON file, Sentry, Slack)

from tranq import FileReporter, SentryReporter, SlackReporter

reporters = [
    FileReporter("/var/log/tranq_errors.json"),
    SentryReporter(dsn="..."),
    SlackReporter(webhook_url="..."),
]

@tranq.handle(on=Exception, reporters=reporters)
def critical_task():
    ...

FileReporter writes JSON lines for easier parsing.

7. Metrics & Profiling

@tranq.handle(metrics=True, metric_prefix="myapp")
def expensive_op():
    ...

from tranq import get_metrics, profile, get_profile

@profile
def heavy_computation():
    ...

print(get_metrics())
print(get_profile("heavy_computation"))

8. Mock Error Injection

from tranq import mock_errors

with mock_errors(ValueError, probability=0.8):
    result = my_function()

9. Dependency Injection

@tranq.handle(inject={"logger": logging.getLogger("app")})
def do_work(logger=None):
    logger.info("Working...")

10. Global Policy

tranq.set_global_policy(tranq.Policy(
    retry=3,
    delay=0.5,
    backoff=2.0,
    reraise=False,
))

# All @handle calls now inherit these defaults
@tranq.handle(on=ValueError)
def my_func():
    ...

πŸš€ Advanced Example

cb = CircuitBreaker(failure_threshold=3, timeout=60)

@tranq.handle(
    on=requests.RequestException,
    retry=5,
    backoff_strategy="fibonacci",
    max_delay=30,
    jitter=True,
    retry_if=lambda e: e.response.status_code in (429, 503),
    circuit_breaker=cb,
    metrics=True,
    metric_prefix="api",
    reporters=[FileReporter("api_errors.json")],
    fallback=lambda: {"status": "fallback"},
)
def fetch_from_external_api():
    ...

πŸ“š API Reference

@tranq.handle(...) / @tranq.handle_async(...)

Parameter Type Default Description
on type | tuple Exception Exception type(s) to catch
retry int 0 Number of retries (0 = no retry)
delay float 0.0 Base delay between retries (seconds)
backoff float 1.0 Backoff multiplier
backoff_strategy str | callable "exponential" "exponential", "linear", "fibonacci", or custom callable
max_delay float | None None Cap on delay between retries
jitter bool False Add Β±25% randomness to delays
fallback callable | None None Function to call when all retries fail
reraise bool True Re-raise exception after exhaustion
log_level int logging.ERROR Logging level for retry messages
message str | None None Custom log format string
policy Policy | None None Explicit policy object
retry_if callable | None None Condition to decide if retry should happen
retry_on_result callable | None None Retry if result matches condition
on_error dict | None None Exception-type β†’ handler mapping
metrics bool False Collect metrics for this function
metric_prefix str "" Prefix for metric keys
circuit_breaker CircuitBreaker | None None Circuit breaker instance
stateful bool False Persist attempt count across calls
reporters list | None None List of reporter instances
inject dict | None None Dependencies to inject

tranq.retry(...) β€” Context Manager

Same parameters as @handle. Usage:

with tranq.retry(on=ValueError, retry=3) as ctx:
    result = ctx.run(my_function, arg1, kwarg1=val)

tranq.retry_group(...) / tranq.async_retry_group(...)

group = tranq.retry_group(func1, func2, func3, on=Exception, retry=2)
results = group.run()

tranq.CircuitBreaker(failure_threshold, timeout, half_open_requests)

tranq.AsyncCircuitBreaker(failure_threshold, timeout, half_open_requests)

tranq.mock_errors(exception, probability, seed)

tranq.profile / tranq.async_profile

tranq.get_metrics() / tranq.reset_metrics()

tranq.get_profile(name=None)

tranq.set_global_policy(policy) / tranq.get_global_policy()

Exceptions

Exception Description
TranqError Base exception for all tranq errors
RetryExhaustedError All retries exhausted
CircuitBreakerError Circuit breaker is open
ResultNotAcceptedError Result rejected by retry_on_result
RetryGroupError Error in a retry group member

πŸ“ Examples

The examples/ directory contains 20 complete, runnable examples covering every feature:

File Topic
01_basic_decorator.py Basic @handle usage
02_retry_and_backoff.py All backoff strategies
03_conditional_retry.py retry_if
04_retry_on_result.py Retry on return value
05_error_handlers.py Multiple on_error handlers
06_fallback.py Fallback values/functions
07_circuit_breaker.py Sync circuit breaker
08_async_circuit_breaker.py Async circuit breaker
09_context_manager.py with tranq.retry(...)
10_retry_group.py Sync retry group
11_async_retry_group.py Async retry group
12_metrics.py Metrics collection
13_profiling.py Function profiling
14_reporters.py File/custom reporters
15_mock_errors.py Mock error injection
16_dependency_injection.py inject parameter
17_stateful_retry.py Stateful retry
18_global_policy.py Global policy
19_async_decorator.py @handle_async
20_combined_advanced.py Everything combined

Run all examples:

python examples/run_all.py

Run a single example:

python examples/07_circuit_breaker.py

πŸ§ͺ Testing

The tests/ directory contains a comprehensive test suite with 120+ tests covering all features:

  • Circuit breaker state transitions (sync & async)
  • Half-open request limits
  • All backoff strategies (exponential, linear, fibonacci, custom)
  • Jitter and max_delay
  • Conditional retry (retry_if, retry_on_result)
  • Error handlers, fallback, dependency injection
  • Stateful retry with thread isolation
  • Retry groups (sync & async, mixed sync/async)
  • Reporters (FileReporter JSON output)
  • Metrics and profiling
  • Mock error injection
  • Global policy

Run all tests:

pip install pytest pytest-asyncio
pytest tests/ -v

Run a specific test file:

pytest tests/test_circuit_breaker.py -v

βš–οΈ Comparison

Feature tranq tenacity backoff
Decorator βœ… βœ… βœ…
Async support βœ… βœ… βœ…
Circuit Breaker βœ… ❌ ❌
Context Manager βœ… ❌ ❌
Retry Groups βœ… ❌ ❌
Metrics βœ… ❌ ❌
Profiling βœ… ❌ ❌
Reporters βœ… ❌ ❌
Mock Errors βœ… ❌ ❌
Dependency Injection βœ… ❌ ❌
Global Policy βœ… ❌ ❌
Stateful Retry βœ… ❌ ❌

πŸ—‚οΈ Project Structure

tranq/
β”œβ”€β”€ examples/                    # 20 runnable examples
β”‚   β”œβ”€β”€ 01_basic_decorator.py
β”‚   β”œβ”€β”€ ...
β”‚   β”œβ”€β”€ 20_combined_advanced.py
β”‚   └── run_all.py
β”œβ”€β”€ src/tranq/
β”‚   β”œβ”€β”€ __init__.py              # Public API
β”‚   β”œβ”€β”€ decorators.py            # @handle / @handle_async
β”‚   β”œβ”€β”€ context.py               # retry() context manager
β”‚   β”œβ”€β”€ circuit_breaker.py       # Sync circuit breaker
β”‚   β”œβ”€β”€ async_circuit_breaker.py # Async circuit breaker
β”‚   β”œβ”€β”€ retry_group.py           # Retry groups (sync/async)
β”‚   β”œβ”€β”€ policies.py              # Policy dataclass
β”‚   β”œβ”€β”€ exceptions.py            # Custom exceptions
β”‚   β”œβ”€β”€ metrics.py               # Metrics collection
β”‚   β”œβ”€β”€ profiling.py             # Function profiling
β”‚   β”œβ”€β”€ reporters.py             # Error reporters
β”‚   β”œβ”€β”€ mock.py                  # Mock error injection
β”‚   └── utils.py                 # Backoff, jitter, logging
β”œβ”€β”€ tests/                       # 120+ tests
β”‚   β”œβ”€β”€ test_circuit_breaker.py
β”‚   β”œβ”€β”€ test_decorators_sync.py
β”‚   β”œβ”€β”€ test_decorators_async.py
β”‚   β”œβ”€β”€ ...
β”‚   └── test_thread_safety.py
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ pytest.ini
β”œβ”€β”€ README.md
β”œβ”€β”€ CONTRIBUTING.md
└── LICENSE

🀝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md.


πŸ“„ License

MIT Β© RaptorVampire

Releases

Contributors

Languages