Wurun
Async OpenAI API wrapper optimized for Jupyter notebooks with connection pooling, retry logic, and batch processing.
Features
- 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
Installation
# From PyPI
pip install wurun
# With pandas DataFrame support
pip install "wurun[dataframe]"
# Development (testing and linting tools)
pip install -e ".[dev,dataframe]"
Quick Start
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()
DataFrame Processing
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.
🧪 Practical Demo on Kaggle
API Reference
Setup
Wurun.setup()- Initialize client (call once per kernel)Wurun.close()- Clean up resources
Single Calls
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
Batch Processing
Wurun.run_gather()- Preserve input orderWurun.run_as_completed()- Yield(index, answer)in completion orderconcurrencyparameter controls parallel requestsmax_tokensandtemperatureavailable on all batch methods
DataFrame Processing
Wurun.run_dataframe()- Process messages from DataFrame column
Notebook Helpers
Wurun.print_qna_ordered()- Pretty print Q&A formatWurun.print_as_ready()- Print results as they complete
Configuration
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.
Error Handling
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
)
Development
# 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
Release Process
- 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
Manual Version Update
# Updates pyproject.toml, wurun.py and CITATION.cff together
python scripts/update_version.py 1.2.3
License
Apache License 2.0
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 wurun-0.2.3.tar.gz.
File metadata
- Download URL: wurun-0.2.3.tar.gz
- Upload date:
- Size: 12.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03f61ed3ba03b3f4a03ce3e73a3308e8a8bad785225c48c1ba7552a24487bd51
|
|
| MD5 |
dc563346008011faa38b11e64092193e
|
|
| BLAKE2b-256 |
bf0bcf96eafec2e5b1d9032921001dc77d0edc43f60067c62de3b25d66c81d3e
|
Provenance
The following attestation bundles were made for wurun-0.2.3.tar.gz:
Publisher:
publish.yml on fairintelligence/wurun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wurun-0.2.3.tar.gz -
Subject digest:
03f61ed3ba03b3f4a03ce3e73a3308e8a8bad785225c48c1ba7552a24487bd51 - Sigstore transparency entry: 2291025047
- Sigstore integration time:
-
Permalink:
fairintelligence/wurun@955d1ae8a933c58f4394457e9e5c5c45a1a7b299 -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/fairintelligence
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@955d1ae8a933c58f4394457e9e5c5c45a1a7b299 -
Trigger Event:
release
-
Statement type:
File details
Details for the file wurun-0.2.3-py3-none-any.whl.
File metadata
- Download URL: wurun-0.2.3-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
036ba3dc6a562129ed1788db5e57f1476346fd5d13d6362e2b1b2f2f586ac200
|
|
| MD5 |
1c7eeeecdd676d73e202cc925c37ed44
|
|
| BLAKE2b-256 |
46e7f013658eaf52cafd210f326cd9d63e5bd9f9f69f488f6a94558b2811e68c
|
Provenance
The following attestation bundles were made for wurun-0.2.3-py3-none-any.whl:
Publisher:
publish.yml on fairintelligence/wurun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wurun-0.2.3-py3-none-any.whl -
Subject digest:
036ba3dc6a562129ed1788db5e57f1476346fd5d13d6362e2b1b2f2f586ac200 - Sigstore transparency entry: 2291025127
- Sigstore integration time:
-
Permalink:
fairintelligence/wurun@955d1ae8a933c58f4394457e9e5c5c45a1a7b299 -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/fairintelligence
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@955d1ae8a933c58f4394457e9e5c5c45a1a7b299 -
Trigger Event:
release
-
Statement type: