Shared client for efficient API calls with persistent connections
Exponential backoff for rate limits, timeouts and 5xx — and fail-fast on errors retrying cannot fix
Concurrent API calls with semaphore control and order preservation
Clean notebook output and error handling for interactive development
Process pandas DataFrames with built-in message column handling
Simple setup that works with OpenAI and Azure OpenAI out of the box
pip install wurun
pip install wurun[dataframe]
pip install -e ".[dev,dataframe]"
Python 3.10 or newer. Works with the OpenAI API, Azure OpenAI, and any OpenAI-compatible endpoint.
from wurun import Wurun
# Setup once per kernel
await Wurun.setup(
endpoint="https://api.openai.com/v1",
api_key="your-api-key",
deployment_name="gpt-3.5-turbo"
)
# Single question
messages = [{"role": "user", "content": "Explain asyncio"}]
answer = await Wurun.ask(messages)
print(answer)
# Control the response
answer = await Wurun.ask(
messages,
max_tokens=512, # default: 1024
temperature=0.7, # default: 0
)
# Cleanup
await Wurun.close()
# Ordered results
questions = [
[{"role": "user", "content": "What is Python?"}],
[{"role": "user", "content": "What is JavaScript?"}]
]
answers = await Wurun.run_gather(questions, concurrency=2)
# Or stream them as they finish: (index, answer)
for idx, answer in await Wurun.run_as_completed(questions, concurrency=2):
print(idx, answer)
# DataFrame processing (order is preserved)
import pandas as pd
df = pd.DataFrame({'messages': questions})
df['answer'] = await Wurun.run_dataframe(
df, 'messages', concurrency=2, max_tokens=512
)
await Wurun.setup(
endpoint="https://api.openai.com/v1",
api_key="your-key",
deployment_name="gpt-3.5-turbo",
timeout=60.0, # per-request cap, seconds
max_connections=32,
max_keepalive=16,
http2=True,
max_retries=2, # SDK-level retries
)
timeout is the per-request cap. Calls inherit it unless they pass their own timeout.
setup() is safe to call again in the same kernel. The connection pool is reused when the pool settings are unchanged, and rebuilt when timeout, max_connections, max_keepalive or http2 differ — so re-running your setup cell with a new value actually takes effect.
Retry budget
max_retries is applied by the OpenAI SDK underneath ask(attempts=...). The worst case is attempts × (max_retries + 1) HTTP requests — 15 with the defaults. Lower either one for a tighter bound.
ask() never raises on API failures — it returns an "[ERROR] ..." string so one bad row cannot abort a whole batch. Use return_meta=True to detect failures reliably instead of matching on the string.
answer, meta = await Wurun.ask(messages, return_meta=True)
if meta["error"]:
print(f"failed: {meta['error_type']}")
else:
print(f"{meta['latency']:.2f}s, {meta['retries']} retries")
latency - seconds elapsed, including retry backoffretries - retries used (0 on first-attempt success)error - True if the answer is an error stringerror_type - exception name, or None on successOnly transient failures: rate limits, connection and timeout errors, HTTP 408/429, and any 5xx. Non-retryable statuses such as 400, 401, 403 and 404 return immediately rather than burning the retry budget on a request that cannot succeed.
Every call accepts max_tokens, temperature and timeout; every batch call also accepts concurrency and return_meta.
Wurun.setup() - Initialize clientWurun.close() - Clean up resourcesWurun.ask() - Single API call with retryreturn_meta=True - Return (answer, meta) with latency, retries and error infoWurun.run_gather() - Preserve input orderWurun.run_as_completed() - Yield (index, answer) in completion orderWurun.run_dataframe() - Process DataFrame columnsWurun.print_qna_ordered() - Pretty print Q&AWurun.print_as_ready() - Print as completed