Metadata-Version: 2.4
Name: wurun
Version: 0.2.3
Summary: Async OpenAI API wrapper for Jupyter notebooks
Author: Wurun Contributors
License-Expression: Apache-2.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx[http2]>=0.24.0
Requires-Dist: openai>=1.13.4
Provides-Extra: dev
Requires-Dist: pytest>=8.2.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: flake8>=6.0.0; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Provides-Extra: dataframe
Requires-Dist: pandas>=2.2.2; extra == "dataframe"
Dynamic: license-file

# Wurun

[![CI](https://github.com/fairintelligence/wurun/actions/workflows/ci.yml/badge.svg)](https://github.com/fairintelligence/wurun/actions/workflows/ci.yml)
[![PyPI version](https://badge.fury.io/py/wurun.svg)](https://badge.fury.io/py/wurun)
[![Python versions](https://img.shields.io/pypi/pyversions/wurun.svg)](https://pypi.org/project/wurun/)
[![License](https://img.shields.io/github/license/fairintelligence/wurun.svg)](https://github.com/fairintelligence/wurun/blob/main/LICENSE)
[![Release](https://img.shields.io/github/v/release/fairintelligence/wurun.svg)](https://github.com/fairintelligence/wurun/releases)

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

```bash
# 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

```python
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

```python
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

[![Kaggle](https://img.shields.io/badge/Kaggle-Notebook-blue?logo=kaggle)](https://www.kaggle.com/code/aisuko/manually-testing-wurun)


## 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 logic
- `return_meta=True` - Return `(answer, meta)` instead of just the answer
- `max_tokens=1024` - Maximum tokens in response (default: 1024)
- `temperature=0` - Response randomness (default: 0)
- `timeout=None` - Per-request timeout; `None` inherits the `setup()` timeout

### Batch Processing
- `Wurun.run_gather()` - Preserve input order
- `Wurun.run_as_completed()` - Yield `(index, answer)` in completion order
- `concurrency` parameter controls parallel requests
- `max_tokens` and `temperature` available on all batch methods

### DataFrame Processing
- `Wurun.run_dataframe()` - Process messages from DataFrame column

### Notebook Helpers
- `Wurun.print_qna_ordered()` - Pretty print Q&A format
- `Wurun.print_as_ready()` - Print results as they complete

## Configuration

```python
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 *underneath*
> `ask(attempts=...)`. The worst case is `attempts * (max_retries + 1)` HTTP requests
> (by default `5 * 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:

```python
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.

```python
# 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

```bash
# 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

1. **Create PR**: Make changes and create pull request to `main`
2. **Auto Draft**: Release Drafter automatically creates/updates draft release
3. **Publish Release**: Go to GitHub Releases, edit draft, and publish
4. **Auto Deploy**: Publishing triggers automatic PyPI deployment

### Manual Version Update
```bash
# Updates pyproject.toml, wurun.py and CITATION.cff together
python scripts/update_version.py 1.2.3
```

## License

Apache License 2.0
