Skip to main content

Resilient decorators that return Result types instead of throwing exceptions

Project description

resilient-result

PyPI version Python 3.8+ License: MIT Tests

Beautiful resilient decorators that return Result types instead of throwing exceptions.

from resilient_result import resilient, Ok, Err

@resilient(retries=3, timeout=5)
async def call_api(url: str) -> str:
    return await http.get(url)  # Exceptions become Result[str, Exception]

result = await call_api("https://api.example.com")
if result.success:
    print(result.data)  # The API response
else:
    print(f"Failed: {result.error}")  # The exception that occurred

What just happened? Function ran up to 3 times with exponential backoff, timed out after 5s, and returned Result[str, Exception] instead of throwing.

Installation

pip install resilient-result

Core Patterns

Basic Resilience

# Simple retry with defaults
@resilient()
async def might_fail():
    if random.random() < 0.3:
        raise Exception("Oops!")
    return "success"

# Returns Ok("success") or Err(Exception("Oops!"))
result = await might_fail()

Built-in Patterns

# Network calls with smart retry logic
@resilient.network(retries=2)
async def fetch_data(url: str):
    return await httpx.get(url)

# JSON parsing with error recovery
@resilient.parsing()
async def parse_response(text: str):
    return json.loads(text)

# Circuit breaker protection
@resilient.circuit(failures=5, window=60)
async def external_service():
    return await service.call()

# Rate limiting with token bucket
@resilient.rate_limit(rps=10.0, burst=5)
async def api_call():
    return await external_api()

Result Type System

Type-Safe Error Handling

from resilient_result import Result, Ok, Err

# Functions return Result[T, E] instead of throwing
def divide(a: int, b: int) -> Result[int, str]:
    if b == 0:
        return Err("Division by zero")
    return Ok(a // b)

# Pattern matching for elegant handling
result = divide(10, 2)
match result:
    case Ok(value):
        print(f"Result: {value}")
    case Err(error):
        print(f"Error: {error}")

Smart Result Detection

# Already returns Result? Passes through unchanged
@resilient(retries=2)
async def already_result() -> Result[str, ValueError]:
    return Ok("data")  # Unchanged: Ok("data")

# Regular return? Auto-wrapped in Ok()
@resilient(retries=2) 
async def regular_return() -> str:
    return "data"  # Becomes: Ok("data")

# Exception raised? Becomes Err()
@resilient(retries=2)
async def might_throw() -> str:
    raise ValueError("oops")  # Becomes: Err(ValueError("oops"))

Extensibility - Registry System

Creating Custom Patterns

from resilient_result import resilient, decorator

# Define domain-specific handler
async def llm_handler(error):
    error_str = str(error).lower()
    if "rate_limit" in error_str:
        await asyncio.sleep(60)  # Wait for rate limit reset
        return None  # Trigger retry
    if "context_length" in error_str:
        return False  # Don't retry context errors
    return None  # Retry other errors

# Create pattern factory
def llm_pattern(retries=3, **kwargs):
    return decorator(handler=llm_handler, retries=retries, **kwargs)

# Register with resilient-result
resilient.register("llm", llm_pattern)

# Beautiful usage
@resilient.llm(retries=5, timeout=30)
async def call_openai(prompt: str):
    return await openai.create(prompt=prompt)

Real-World Extension: AI Agent Patterns

# Cogency extends resilient-result for AI-specific resilience
from cogency.resilience import safe  # Built on resilient-result

@safe.reasoning(retries=2)  # Fallback: deep → fast mode
async def llm_reasoning(state):
    return await llm.generate(state.prompt)

@safe.memory()  # Graceful memory degradation
async def store_context(data):  
    return await vector_db.store(data)

# Both @safe.reasoning() and @resilient.reasoning() work identically
# Proving the extensibility architecture works beautifully

Performance & Architecture

Performance Characteristics

  • Overhead: ~0.1ms per decorated call
  • Memory: ~200 bytes per Result object
  • Concurrency: Thread-safe, async-first design
  • Test suite: Comprehensive coverage, <2s runtime

v0.2.0 Status: Foundation Ready

Proven extensible architecture - Registry system enables domain-specific patterns
Beautiful decorator API - Clean @resilient.pattern() syntax
Type-safe Result system - Ok/Err prevents ignored errors
Real-world proven - Successfully integrated with production AI systems

Current patterns provide solid foundation with basic implementations suitable for development and basic production use.

Production-grade pattern enhancements planned for v0.3.0 - see roadmap

When to Use

Perfect for:

  • API clients and external service calls
  • Data processing pipelines
  • AI/LLM applications with retry logic
  • Microservices with resilience requirements
  • Any async operations that might fail

Not ideal for:

  • High-frequency inner loops (0.1ms overhead)
  • Simple scripts (adds complexity)
  • Teams preferring exception-based patterns

Advanced Examples

Composing Multiple Patterns

# Stack decorators for layered resilience
@resilient.rate_limit(rps=5)
@resilient.circuit(failures=3)
@resilient.network(retries=2)
async def robust_api_call(endpoint: str):
    return await http.get(f"https://api.service.com/{endpoint}")

Custom Error Types

class APIError(Exception):
    pass

@resilient(retries=3, error_type=APIError, timeout=10)
async def typed_api_call(data: dict):
    response = await http.post("/api/endpoint", json=data)
    return response.json()

# Returns Result[dict, APIError] - type-safe!

Sync Function Support

@resilient(retries=3)
def sync_operation(data: str) -> str:
    if random.random() < 0.3:
        raise Exception("Sync failure")
    return f"processed: {data}"

# Also returns Result[str, Exception]
result = sync_operation("test")

Testing Made Easy

# No more exception mocking - just check Result values
async def test_api_call():
    result = await call_api("https://fake-url")
    
    assert isinstance(result, Result)
    if result.success:
        assert "data" in result.data
    else:
        assert "network" in str(result.error).lower()

License

MIT - Build amazing resilient systems! 🚀

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

resilient_result-0.2.0.tar.gz (8.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

resilient_result-0.2.0-py3-none-any.whl (10.7 kB view details)

Uploaded Python 3

File details

Details for the file resilient_result-0.2.0.tar.gz.

File metadata

  • Download URL: resilient_result-0.2.0.tar.gz
  • Upload date:
  • Size: 8.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.11.12 Darwin/24.5.0

File hashes

Hashes for resilient_result-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d79b3c3e32d889690a8c0d6a8c9619c50f0e390dcdca703a18a00a77ebecfae6
MD5 9a7ad935dbb2016cfea70561e1e1ab40
BLAKE2b-256 c36c9f7b143aa8e61988309c4588b10b9b1cb408b4ba928854670331078a1446

See more details on using hashes here.

File details

Details for the file resilient_result-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: resilient_result-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.11.12 Darwin/24.5.0

File hashes

Hashes for resilient_result-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3e8db1b8008d335b76ab472b4b923092c37c7489a2662e1ce8cfa4b64b238352
MD5 1b775b9f7ac5150754edde44132bc38c
BLAKE2b-256 f1427d4b14e83f65fb288f3add61f9a9ca2c352e9611658035c46444b1aa4a01

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page