Async HTTP utilities using aiohttp and httpx.
Project description
ji-async-http-utils
Small async helpers for HTTP clients, with a focus on practical concurrency for aiohttp.
This package provides utilities to:
- Iterate responses with controlled concurrency
- Stream results in completion order while mapping back to inputs
- Handle retries (respecting Retry-After) and timeouts
- Show progress bars via tqdm
Install
Requires Python 3.10+.
pip install ji-async-http-utils
Direct dependencies: aiohttp, tqdm (plus httpx, rich for the httpx helpers).
Quickstart
Basic list with base_url and a progress label:
from ji_async_http_utils.aiohttp import iter_responses
async def main():
async for user_id, data in iter_responses(
base_url="https://api.example.com/users",
items=[1, 2, 3, 4, 5],
max_concurrency=16,
pbar="Fetching users",
):
print(user_id, data["name"]) # default yields parsed JSON
Usage Examples (aiohttp)
Single request with request
from ji_async_http_utils.aiohttp import request
async def main():
data = await request(
url="https://jsonplaceholder.typicode.com/todos/1",
raise_on_error=True, # raise instead of returning an Exception
timeout=15.0,
)
# `data` is parsed JSON (alias `JSON`)
print(data["title"]) # e.g., "delectus aut autem"
Range helper: inclusive [min_id, max_id]
from ji_async_http_utils.aiohttp import iter_responses
async def main():
async for user_id, data in iter_responses(
base_url="https://api.example.com/users",
items=range(100, 126), # inclusive [100..125]
max_concurrency=32,
pbar="Users",
):
print(user_id, data.get("id"))
Reverse order and exclusions
async def main():
ids = list(range(50, 0, -1)) # reverse from 50 down to 1
excluded = {3, 7, 9}
filtered = (i for i in ids if i not in excluded)
async for i, data in iter_responses(
base_url="https://example.com/docs",
items=filtered,
pbar="Docs",
):
# default assumes JSON; for non-JSON endpoints, pass on_result
...
Retry policy and raise_on_error
async def main():
async for i, data in iter_responses(
base_url="https://api.example.com/items",
items=range(1, 101),
retries=3, # retry 3 times on 429/5xx or client/timeout
raise_on_error=True, # raise on failure instead of yielding Exception
pbar="Items",
):
# data is parsed JSON here
...
Custom headers and query params
async def main():
async for i, data in iter_responses(
base_url="https://api.example.com/items",
items=range(1, 6),
headers={"Authorization": "Bearer TOKEN"},
params={"expand": "details"},
pbar=True,
):
...
Provide your own session (connection reuse, custom connector)
import aiohttp
from ji_async_http_utils.aiohttp import iter_responses
async def main():
timeout = aiohttp.ClientTimeout(total=30)
connector = aiohttp.TCPConnector(limit=64)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
async for key, data in iter_responses(
base_url="https://api.example.com/resources",
items=["a", "b", "c"],
session=session, # reuse this session
max_concurrency=32,
pbar=True,
): # early breaks are safe; pending tasks are cancelled
...
Progress bar on/off
# On with label
async for k, data in iter_responses(..., pbar="Downloading"): ...
# On without label
async for k, data in iter_responses(..., pbar=True): ...
# Off (default)
async for k, data in iter_responses(...): ...
Using request_fn (no base_url): build requests per-item
from ji_async_http_utils.aiohttp import iter_responses
import aiohttp
async def make_request(session: aiohttp.ClientSession, item: tuple[int, str]):
# item can be any type; you decide how to build the request
item_id, slug = item
url = f"https://example.com/{slug}/{item_id}"
return await session.get(url)
async def main():
items = [(1, "alpha"), (2, "beta"), (3, "gamma")]
async for item, data in iter_responses(
request_fn=make_request, # base_url must be None in this mode
items=items,
max_concurrency=10,
pbar="Custom requests",
):
# Gotcha avoidance: do not consume the body inside on_result if you
# plan to read it here. Always consume/release exactly once.
print(item, data)
Hooks: on_result and on_error
async def on_error(item, exc):
# Called right before a failed request raises after retries.
print("FAIL", item, type(exc).__name__)
async for item, data in iter_responses(
base_url="https://api.example.com/jobs",
items=range(1, 51),
on_error=on_error, # keep default JSON results, but hook errors
pbar=True,
):
...
Using raise_on_error=True with a result transformer
When you want exceptions to raise immediately and your code consumes transformed results only:
from ji_async_http_utils.aiohttp import iter_responses
async def to_json(item, resp):
async with resp:
return await resp.json()
async for item, data in iter_responses(
base_url="https://api.example.com/items",
items=range(1, 6),
on_result=to_json,
raise_on_error=True, # no on_error allowed in this mode
):
# data is guaranteed to be successful JSON here
print(item, data)
Timeout control
import aiohttp
from ji_async_http_utils.aiohttp import iter_responses
# Use a float (seconds) for total timeout when the function creates the session
async for item, data in iter_responses(
base_url="https://example.com",
items=range(10),
timeout=15.0,
):
...
# Or provide a full ClientTimeout
async for item, data in iter_responses(
base_url="https://example.com",
items=range(10),
timeout=aiohttp.ClientTimeout(total=120, connect=5),
):
...
Usage Examples (httpx)
Basic client lifecycle with lifespan and http_get
from ji_async_http_utils.httpx import lifespan, http_get, get_client
async def main():
async with lifespan():
# Simple GET with optional headers/params; raises for non-2xx by default
res = await http_get(
"https://httpbin.org/get",
params={"q": "hello"},
headers={"X-Demo": "1"},
)
print(res.status_code, res.json())
# Or use the client directly
r2 = await get_client().post("https://httpbin.org/post", json={"ok": True})
print(r2.status_code)
Allow specific non-2xx without raising
from ji_async_http_utils.httpx import lifespan, http_get
async def main():
async with lifespan():
# 404 is allowed here and will not raise
res = await http_get(
"https://httpbin.org/status/404",
raise_on_status_except_for=[404],
)
print(res.status_code)
Synchronous entrypoint using run_in_lifespan
from ji_async_http_utils.httpx import run_in_lifespan, get_client
@run_in_lifespan
async def main() -> None:
res = await get_client().get("https://httpbin.org/uuid")
print(res.json()["uuid"]) # prints a UUID
main() # runs with a managed client lifecycle
API Overview (aiohttp)
Exports from ji_async_http_utils.aiohttp:
iter_responses(items=..., ...) -> AsyncIterator[tuple[item, JSON | BaseException]]request(url=..., ...) -> JSON | BaseException(raises on failure ifraise_on_error=True)
Key parameters:
max_concurrency: cap in-flight requests (default 32)base_urlorrequest_fn: mutually exclusive ways to issue requeststimeout: total timeout (defaults to 60s if we create the session)method: HTTP method literal ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT")pbar: progress toggle/label.Trueenables without a label; a string sets the label;Falsedisablesraise_on_error: whenTrue, failures are raised; whenFalse(default), failures are yielded as Exceptionsretries: retry count for retryable statuses/exceptions (429/5xx, client/timeouts)retry_statuses: customize which HTTP status codes trigger a retry (default: 429, 500, 502, 503, 504)on_result/on_error: async hooks for side effects
Return types:
- Default (no
on_result): yields parsed JSON (aliasJSON), notClientResponse. - With
on_result: yieldsResultTreturned by your callback. - When
raise_on_error=False(default), failures are yielded asException. - When
raise_on_error=True, failures raise immediately and are not yielded.
Type-safety constraints (overloads guide editors):
- Provide exactly one of
base_urlorrequest_fn. - If
raise_on_error=True, thenon_errormust beNone(ignored when raising). - If
on_erroris provided,raise_on_errormust beFalse.
API Overview (httpx)
Exports from ji_async_http_utils.httpx:
lifespan() -> AsyncIterator[httpx.AsyncClient]: context manager that creates a configured AsyncClient and sets it in a ContextVar for use by helpers.get_client() -> httpx.AsyncClient: returns the context-scoped client; raises if called outsidelifespan()orrun_in_lifespan.create_client() -> httpx.AsyncClient: constructs the default client (30s timeout, follow_redirects=True, response logging hook).http_get(url, *, headers=None, params=None, raise_on_status_except_for=None) -> httpx.Response: GET helper that raises for non-2xx unless allowed.run_in_lifespan(func) -> Callable[...]: decorator that runs an async function inside a managedlifespanand returns a sync callable.
Gotchas & Best Practices
-
aiohttp/iter_responses:
- Response handling: When
on_resultisNone(default), the helper readsresp.json()and closes the response for you; you receive parsed JSON (JSON). When you provideon_result, you receive the rawClientResponsein that callback and must read/close it there. - Error handling: Results are yielded in completion order. Failures are yielded as
Exceptionvalues whenraise_on_error=False(default) and are raised immediately whenTrue. - Concurrency: Start with 16–32 in-flight requests; tune by observing 95th percentile latency and error codes (429/5xx). Internal session uses
TCPConnector(limit=max_concurrency). - Arguments: Provide exactly one of
base_urlorrequest_fn. Overloads guide correct usage in editors. - Early termination: Safe to break out of the loop — pending tasks are cancelled and the session/progress bar are cleaned up.
- Sessions: Reuse a single
ClientSession(passsession=) to benefit from connection pooling when making many calls.
- Response handling: When
-
httpx helpers:
- Always call
get_client()orhttp_get()insideasync with lifespan(): ...or via a function decorated with@run_in_lifespan. Otherwiseget_client()raises to avoid leaking a global client. http_getraises for non-2xx by default; useraise_on_status_except_forto allow specific codes (e.g.,[404]).
- Always call
-
The default client (via
create_client/lifespan) uses a 30s timeout, follows redirects, and logs responses to the console.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ji_async_http_utils-0.1.1.tar.gz.
File metadata
- Download URL: ji_async_http_utils-0.1.1.tar.gz
- Upload date:
- Size: 8.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4602aecae7f877d2744dee8e0744110aac2af602ceb560b8ad68c2bbeeffc28a
|
|
| MD5 |
210620b9c78b247f36cfbbffda0801d3
|
|
| BLAKE2b-256 |
1a5e5e2c07db9c08585c70e4d036eeac94785232dbff9e9d9a3681c355981f9c
|
File details
Details for the file ji_async_http_utils-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ji_async_http_utils-0.1.1-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
079d1e5c7b5d2bf348f41472aea5002c16981f7205d725d9a68a46315c0ffb39
|
|
| MD5 |
597ba4f1fbe95289ce49a4e497c74304
|
|
| BLAKE2b-256 |
553e29f8977d07410978c4f2bd59b1182d668ea1de3fe439f82ac38661facc1c
|