Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</a>
</p>

A production-grade, asynchronous HTTP client for Python engineered to tolerate downstream service outages, network instability, and latency spikes. It implements proven resilience patterns including **Circuit Breakers**, **Retry Policies**, **Fallback Mechanisms**, and a **Distributed Failure Store** to enable reliable service-to-service communication.
Asynchronous HTTP client for Python engineered to tolerate downstream service outages, network instability, and latency spikes. It implements proven resilience patterns including **Circuit Breakers**, **Retry Policies**, **Fallback Mechanisms**, and a **Distributed Failure Store** to enable reliable service-to-service communication.

Built on top of `httpx`, the library is designed for modern distributed systems where resilience is a first-class requirement.

Expand Down Expand Up @@ -131,7 +131,13 @@ async def main():
},
)

print(response)
# The request returns a raw httpx.Response object on success
if hasattr(response, "json"):
print("Status:", response.status_code)
print("Response Data:", response.json())
else:
# Fallback values returned as a dict
print("Fallback Response:", response)

if __name__ == "__main__":
asyncio.run(main())
Expand Down Expand Up @@ -311,4 +317,4 @@ Available examples:
* Event-driven systems
* Containerized applications
* Kubernetes deployments
* Any environment where downstream dependencies may become unavailable
* Any environment where downstream dependencies may become unavailable
2 changes: 2 additions & 0 deletions examples/fastapi_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ async def create_charge(http: ResilientHttpClient = Depends(http_client)):
},
)

if hasattr(response, "json"):
return response.json()
return response
5 changes: 4 additions & 1 deletion examples/slack_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ async def main():
},
)

print(response)
if hasattr(response, "json"):
print(response.json())
else:
print(response)


if __name__ == "__main__":
Expand Down
5 changes: 4 additions & 1 deletion examples/stripe_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ async def main():
},
)

print(response)
if hasattr(response, "json"):
print(response.json())
else:
print(response)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ad-tech-inc-resilient-http"
version = "0.1.1"
version = "0.2.0"
description = "A production-grade, asynchronous HTTP client for Python engineered to tolerate downstream service outages, network instability, and latency spikes."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
6 changes: 4 additions & 2 deletions src/resilient_http_client/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
import httpx
from .http import HttpExecutor
from .circuit_breaker import CircuitBreaker
from .retry import RetryPolicy
from .failure_store import FailureStore
from .fallback import FallbackHandler
from .config import ResilienceConfig


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -40,7 +42,7 @@ async def __aexit__(self, exc_type, exc, tb):
async def close(self):
await self.http.close()

async def request(self, method: str, url: str, **kwargs):
async def request(self, method: str, url: str, **kwargs) -> httpx.Response:
# 1. Check Circuit Breaker
if not await self.circuit.allow_request():
logger.warning(f"Request blocked by Circuit Breaker for {self.service}")
Expand All @@ -57,7 +59,7 @@ async def request(self, method: str, url: str, **kwargs):

if response.is_success:
await self.circuit.on_success()
return response.data
return response

# Non-success response (e.g., 5xx)
logger.error(f"Request failed with status {response.status_code}")
Expand Down
23 changes: 2 additions & 21 deletions src/resilient_http_client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ async def get_client(self) -> httpx.AsyncClient:
self._client = httpx.AsyncClient(timeout=self.timeout)
return self._client

async def send(self, method: str, url: str, **kwargs):
async def send(self, method: str, url: str, **kwargs) -> httpx.Response:
client = await self.get_client()
try:
response = await client.request(method, url, **kwargs)
return HttpResponse(response)
return response
except httpx.RequestError as exc:
logger.error(f"An error occurred while requesting {exc.request.url!r}.")
raise
Expand All @@ -27,22 +27,3 @@ async def close(self):
if self._client and not self._client.is_closed:
await self._client.aclose()


class HttpResponse:
def __init__(self, response: httpx.Response):
self._response = response

@property
def is_success(self):
return 200 <= self._response.status_code < 300

@property
def status_code(self):
return self._response.status_code

@property
def data(self):
try:
return self._response.json()
except Exception:
return self._response.text
7 changes: 5 additions & 2 deletions tests/test_flaky_resilience.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ class MockResponse:
def __init__(self, is_success, status_code, data):
self.is_success = is_success
self.status_code = status_code
self.data = data
self._data = data

def json(self):
return self._data


class FlakyHttpExecutor:
Expand Down Expand Up @@ -107,7 +110,7 @@ async def worker(worker_id: int):
assert len(results) == 30

# successes should exist
successes = [r for r in results if isinstance(r, dict) and r.get("ok") is True]
successes = [r for r in results if not isinstance(r, dict) and hasattr(r, "json") and r.json().get("ok") is True]
assert len(successes) > 0

# failures should exist due to upstream instability (returned as degraded responses)
Expand Down
7 changes: 5 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ class FakeResponse:
def __init__(self, status_code=200):
self.status_code = status_code
self.is_success = 200 <= status_code < 300
self.data = {"ok": True} if self.is_success else {"error": "failed"}
self._data = {"ok": True} if self.is_success else {"error": "failed"}

def json(self):
return self._data


@pytest.mark.asyncio
Expand All @@ -60,7 +63,7 @@ async def test_successful_request():

result = await client.request("GET", "https://api.example.com")

assert result["ok"] is True
assert result.json()["ok"] is True


@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading