Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..

README.md


SmooAI Logo

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-fetch (Python)

Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.

PyPI VersionPyPI DownloadsPyPI Last Update

GitHub LicenseGitHub Actions Workflow StatusGitHub Repo stars

Python Package

This is the Python port of @smooai/fetch, built with idiomatic async/await patterns using httpx and Pydantic. It provides the same resilient HTTP client capabilities — retries, timeouts, rate limiting, circuit breaking, and request lifecycle hooks — in a Pythonic API.

Why smooai-fetch?

Ever had your async Python service crash because an API was down for 2 seconds? Or watched your workers pile up because a third-party service hit its rate limit? Traditional httpx and aiohttp give you the request, but leave you to handle the reality of network failures.

smooai-fetch automatically handles:

For Unreliable APIs:

  • Smart retries - Exponential backoff with jitter to prevent thundering herds
  • Automatic timeouts - Never hang indefinitely on slow endpoints
  • Rate limit respect - Reads Retry-After headers and backs off intelligently
  • Circuit breaking - Stop hammering services that are clearly down
  • Pydantic validation - Validate response shapes with your existing models

For Developer Experience:

  • Async-native - Built on httpx.AsyncClient with full async/await support
  • FetchBuilder - Fluent builder API for reusable configured clients
  • Lifecycle hooks - Pre-request and post-response hooks for auth and logging
  • Typed responses - FetchResponse[T] wraps parsed Pydantic models

Install

pip install smooai-fetch

or with uv:

uv add smooai-fetch
LanguagePackageInstall
TypeScript@smooai/fetchpnpm add @smooai/fetch
Pythonsmooai-fetchpip install smooai-fetch
Rustsmooai-fetchcargo add smooai-fetch
Gogithub.com/SmooAI/fetch/go/fetch/v3go get github.com/SmooAI/fetch/go/fetch/v3

The Power of Resilient Fetching

Never Let a Hiccup Break Your App

Watch how smooai-fetch handles common failure scenarios:

fromsmooai_fetchimportfetch# This won't crash if the API is temporarily downresponse=awaitfetch("https://flaky-api.com/data")
# Behind the scenes:# Attempt 1: 500 error - waits 500ms# Attempt 2: 503 error - waits 1000ms# Attempt 3: 200 success!

Your users never know the API had issues — the request just works.

Respect Rate Limits Automatically

No more manual retry-after parsing:

response=awaitfetch("https://api.github.com/user/repos")
# If GitHub says "slow down":# - Sees 429 status + Retry-After: 60# - Automatically waits 60 seconds# - Retries and succeeds# - Your code continues normally

Production-Ready Examples

Simple GET Request

fromsmooai_fetchimportfetchresponse=awaitfetch("https://api.example.com/users")
users=response.data# parsed JSON as dict

POST Request with Body

fromsmooai_fetchimportfetch, FetchOptionsresponse=awaitfetch(
"https://api.example.com/users",
FetchOptions(
method="POST",
headers={"Content-Type": "application/json"},
body={"name": "Alice", "email": "alice@example.com"},
),
)

Pydantic Schema Validation

frompydanticimportBaseModelfromsmooai_fetchimportfetch, FetchOptionsclassUser(BaseModel):
id: stremail: strname: str# Your API returns garbage? You'll know immediatelyresponse=awaitfetch(
"https://api.example.com/users/123",
FetchOptions(schema=User),
)
# response.data is a fully validated User instanceprint(response.data.email)

FetchBuilder for Reusable Clients

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportRetryOptions, RateLimitOptionsbuilder= (
FetchBuilder()
.with_timeout(5000)
.with_retry(RetryOptions(attempts=3, initial_interval_ms=500))
.with_rate_limit(RateLimitOptions(max_requests=100, window_ms=60_000))
.with_headers({"X-API-Key": "your-key"})
)
response=awaitbuilder.fetch("https://api.example.com/users/123")

Circuit Breaking for Critical Services

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._typesimportCircuitBreakerOptionsfromsmooai_fetch._errorsimportCircuitBreakerError# Stop hammering services that are clearly strugglingbuilder= (
FetchBuilder()
.with_circuit_breaker(CircuitBreakerOptions(
failure_threshold=5,
success_threshold=2,
open_state_delay_ms=30_000,
))
.with_timeout(5000)
)
try:
response=awaitbuilder.fetch(
"https://payment-processor.com/charge",
method="POST",
body=charge_data,
)
exceptCircuitBreakerError:
# Circuit is open - service is down, fail fastreturnfallback_response()

Real-World Scenarios

Handle Authentication Globally

fromsmooai_fetchimportFetchBuilderbuilder= (
FetchBuilder()
.with_auth(get_token()) # sets Authorization: Bearer <token>
.with_retry()
)
# All requests automatically include the Authorization headerresponse=awaitbuilder.fetch("https://api.example.com/protected")

Add Custom Headers Per Request

response=awaitbuilder.fetch(
"https://api.example.com/data",
headers={"X-Request-ID": "req-abc-123"},
)

Pre-Request and Post-Response Hooks

fromsmooai_fetchimportFetchBuilderdefadd_trace_header(url, request_kwargs):
request_kwargs["headers"]["X-Trace-ID"] =generate_trace_id()
returnurl, request_kwargsdeflog_response(url, request_kwargs, response):
print(f"GET {url} -> {response.response.status_code}")
returnresponsebuilder= (
FetchBuilder()
.with_pre_request_hook(add_trace_header)
.with_post_response_success_hook(log_response)
)

Graceful Degradation

fromsmooai_fetchimportFetchBuilderfromsmooai_fetch._errorsimportCircuitBreakerErrorprimary=FetchBuilder().with_circuit_breaker(CircuitBreakerOptions(failure_threshold=3)).with_timeout(5000)
fallback=FetchBuilder().with_timeout(2000)
asyncdefget_weather(city: str):
try:
returnawaitprimary.fetch(f"https://api1.weather.com/{city}")
exceptCircuitBreakerError:
# Seamlessly fall back to secondary servicereturnawaitfallback.fetch(f"https://api2.weather.com/{city}")

The Smart Defaults

Out of the box, smooai-fetch is configured for the real world:

Retry Strategy:

  • 2 automatic retries on failure
  • Exponential backoff: 500ms -> 1s -> 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Timeout Protection:

  • 10-second default timeout
  • Prevents indefinite hangs
  • Configurable per request

Rate Limit Handling:

  • Respects Retry-After headers
  • Automatic backoff on 429 responses
  • Prevents API ban hammers

API Reference

fetch(url, options) — Top-Level Function

The simplest way to make a request with automatic retries and timeout:

fromsmooai_fetchimportfetch, FetchOptionsfromsmooai_fetch._typesimportRetryOptions, TimeoutOptionsresponse=awaitfetch(
"https://api.example.com/data",
FetchOptions(
method="GET",
headers={"Authorization": "Bearer token"},
retry=RetryOptions(attempts=3, initial_interval_ms=500),
timeout=TimeoutOptions(timeout_ms=10_000),
),
)

Error Handling

fromsmooai_fetch._errorsimport (
HTTPResponseError,
RetryError,
TimeoutError,
RateLimitError,
CircuitBreakerError,
SchemaValidationError,
)
try:
response=awaitfetch("https://api.example.com/data")
exceptHTTPResponseErrorase:
print(f"HTTP {e.status}: {e.status_text}")
print(f"Body: {e.data_string}")
exceptRetryErrorase:
print(f"Failed after {e.attempts} attempts: {e.last_error}")
exceptTimeoutErrorase:
print(f"Timed out after {e.timeout_ms}ms")
exceptRateLimitError:
print("Rate limit exceeded")
exceptCircuitBreakerError:
print("Circuit breaker open — service is down")
exceptSchemaValidationErrorase:
print(f"Validation failed: {e.validation_errors}")

Built With

  • Python 3.13+ - Full async/await and type hints support
  • httpx - Async HTTP client
  • Pydantic - Data validation and schema enforcement
  • Sliding window rate limiter
  • Circuit breaker state machine (Closed/Open/HalfOpen)

Related Packages

Development

uv sync
uv run poe install-dev
uv run pytest
uv run poe lint
uv run poe lint:fix # optional fixer
uv run poe format
uv run poe typecheck
uv run poe build

Set UV_PUBLISH_TOKEN before running uv run poe publish to upload to PyPI.

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

License

MIT © SmooAI