Async OpenAI API wrapper optimized for Jupyter notebooks with connection pooling, retry logic, and batch processing.
- HTTP/2 Connection Pooling - Shared client for efficient API calls
- Robust Retry Logic - Exponential backoff, and fail-fast on errors that retrying cannot fix
- Batch Processing - Concurrent API calls with semaphore control
- Jupyter Optimized - Clean notebook output and error handling
- Zero Configuration - Simple setup, works with OpenAI and Azure OpenAI
# From PyPI
pip install wurun
# With pandas DataFrame support
pip install "wurun[dataframe]"
# Development (testing and linting tools)
pip install -e ".[dev,dataframe]"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)
# Custom parameters
answer = await Wurun.ask(messages, max_tokens=512, temperature=0.7)
# Batch processing
questions = [
[{"role": "user", "content": "What is Python?"}],
[{"role": "user", "content": "What is JavaScript?"}]
]
answers = await Wurun.run_gather(questions, concurrency=2)
# Cleanup
await Wurun.close()import pandas as pd
df = pd.DataFrame({
"id": [1, 2, 3],
"messages": [
[{"role": "user", "content": "What is Python?"}],
[{"role": "user", "content": "What is JavaScript?"}],
[{"role": "user", "content": "What is Go?"}]
]
})
df["answer"] = await Wurun.run_dataframe(df, "messages", concurrency=2)Results are returned in the same order as the DataFrame rows, so they can be assigned straight back to a column.
Wurun.setup()- Initialize client (call once per kernel)Wurun.close()- Clean up resources
Wurun.ask()- Single API call with retry logicreturn_meta=True- Return(answer, meta)instead of just the answermax_tokens=1024- Maximum tokens in response (default: 1024)temperature=0- Response randomness (default: 0)timeout=None- Per-request timeout;Noneinherits thesetup()timeout
Wurun.run_gather()- Preserve input orderWurun.run_as_completed()- Yield(index, answer)in completion orderconcurrencyparameter controls parallel requestsmax_tokensandtemperatureavailable on all batch methods
Wurun.run_dataframe()- Process messages from DataFrame column
Wurun.print_qna_ordered()- Pretty print Q&A formatWurun.print_as_ready()- Print results as they complete
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 in seconds
max_connections=32,
max_keepalive=16,
http2=True,
max_retries=2 # SDK-level retries, see the note below
)setup() is safe to call again from the same kernel. The connection pool is reused when
timeout, max_connections, max_keepalive and http2 are unchanged, and rebuilt when any
of them differ - so re-running your setup cell with a new timeout actually takes effect.
Retry budget:
setup(max_retries=...)is applied by the OpenAI SDK underneathask(attempts=...). The worst case isattempts * (max_retries + 1)HTTP requests (by default5 * 3 = 15). Lower one of the two if you want 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 with {meta['error_type']} after {meta['retries']} retries")
else:
print(f"Latency: {meta['latency']:.2f}s, Retries: {meta['retries']}")The meta dict contains:
| key | meaning |
|---|---|
latency |
seconds elapsed, including retry backoff |
retries |
number of retries used (0 on first-attempt success) |
error |
True if the answer is an [ERROR] ... string |
error_type |
exception class name, or None on success |
Only transient failures are retried: rate limits, connection/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.
# Custom retry settings
result = await Wurun.ask(
messages,
attempts=3,
initial_backoff=1.0,
max_backoff=10.0,
max_tokens=512,
temperature=0.7
)# Install dev dependencies
pip install -e ".[dev,dataframe]"
# Run everything CI runs (format, lint, types, tests)
make check
# Or just the tests
pytest test_wurun.py -v- Create PR: Make changes and create pull request to
main - Auto Draft: Release Drafter automatically creates/updates draft release
- Publish Release: Go to GitHub Releases, edit draft, and publish
- Auto Deploy: Publishing triggers automatic PyPI deployment
# Updates pyproject.toml, wurun.py and CITATION.cff together
python scripts/update_version.py 1.2.3Apache License 2.0