Skip to main content

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 if raise_on_error=True)

Key parameters:

  • max_concurrency: cap in-flight requests (default 32)
  • base_url or request_fn: mutually exclusive ways to issue requests
  • timeout: 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. True enables without a label; a string sets the label; False disables
  • raise_on_error: when True, failures are raised; when False (default), failures are yielded as Exceptions
  • retries: 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 (alias JSON), not ClientResponse.
  • With on_result: yields ResultT returned by your callback.
  • When raise_on_error=False (default), failures are yielded as Exception.
  • When raise_on_error=True, failures raise immediately and are not yielded.

Type-safety constraints (overloads guide editors):

  • Provide exactly one of base_url or request_fn.
  • If raise_on_error=True, then on_error must be None (ignored when raising).
  • If on_error is provided, raise_on_error must be False.

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 outside lifespan() or run_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 managed lifespan and returns a sync callable.

Gotchas & Best Practices

  • aiohttp/iter_responses:

    • Response handling: When on_result is None (default), the helper reads resp.json() and closes the response for you; you receive parsed JSON (JSON). When you provide on_result, you receive the raw ClientResponse in that callback and must read/close it there.
    • Error handling: Results are yielded in completion order. Failures are yielded as Exception values when raise_on_error=False (default) and are raised immediately when True.
    • 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_url or request_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 (pass session=) to benefit from connection pooling when making many calls.
  • httpx helpers:

    • Always call get_client() or http_get() inside async with lifespan(): ... or via a function decorated with @run_in_lifespan. Otherwise get_client() raises to avoid leaking a global client.
    • http_get raises for non-2xx by default; use raise_on_status_except_for to allow specific codes (e.g., [404]).
  • 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


Download files

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

Source Distribution

ji_async_http_utils-0.1.1.tar.gz (8.9 kB view details)

Uploaded Source

Built Distribution

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

ji_async_http_utils-0.1.1-py3-none-any.whl (10.7 kB view details)

Uploaded Python 3

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

Hashes for ji_async_http_utils-0.1.1.tar.gz
Algorithm Hash digest
SHA256 4602aecae7f877d2744dee8e0744110aac2af602ceb560b8ad68c2bbeeffc28a
MD5 210620b9c78b247f36cfbbffda0801d3
BLAKE2b-256 1a5e5e2c07db9c08585c70e4d036eeac94785232dbff9e9d9a3681c355981f9c

See more details on using hashes here.

File details

Details for the file ji_async_http_utils-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for ji_async_http_utils-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 079d1e5c7b5d2bf348f41472aea5002c16981f7205d725d9a68a46315c0ffb39
MD5 597ba4f1fbe95289ce49a4e497c74304
BLAKE2b-256 553e29f8977d07410978c4f2bd59b1182d668ea1de3fe439f82ac38661facc1c

See more details on using hashes here.

Supported by

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