Metadata-Version: 2.4
Name: pairbroker-sdk
Version: 1.0.0
Summary: Async Python client for the PairBroker API
Author-email: maximunya <maximunya.dev@gmail.com>
License: MIT License
        
        Copyright (c) 2026 maximunya
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Repository, https://github.com/maximunya/pairbroker-sdk
Project-URL: Issues, https://github.com/maximunya/pairbroker-sdk/issues
Keywords: sdk,api-client,proxy,accounts,async
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: Operating System :: OS Independent
Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.13.2
Requires-Dist: pydantic>=2.12.4
Requires-Dist: pydantic-settings>=2.12.0
Dynamic: license-file

# pairbroker-sdk

[![CI](https://github.com/maximunya/pairbroker-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/maximunya/pairbroker-sdk/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/pairbroker-sdk?color=blue)](https://pypi.org/project/pairbroker-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/pairbroker-sdk)](https://pypi.org/project/pairbroker-sdk/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
[![PyPI downloads](https://img.shields.io/pypi/dm/pairbroker-sdk)](https://pypi.org/project/pairbroker-sdk/)

**A Python async client for working with accounts and proxies through PairBroker API.**

Handles account rotation, rate limits, retries, and proxy management — so you can focus on the actual request logic.

---

## What is this?

`pairbroker-sdk` is a **client library**. You install it into the project that actually needs to make outbound requests (scraping, API integrations, automation, etc.) using a pool of rotating accounts and proxies.

It doesn't manage accounts or proxies by itself — it talks over HTTP to **PairBroker API**, a separate, self-hosted backend service that owns your accounts, proxies, rate limits and usage stats. You deploy your own instance, load it with your accounts/proxies, and point this SDK at it.

> **Status:** PairBroker API isn't public yet — it exists and is self-hosted (Django + PostgreSQL, Docker Compose), but it's not published as an open-source project at this time. This SDK is fully usable against any instance that implements the same REST contract; docs for self-hosting PairBroker API itself will be added here once it's released.

Importantly, **PairBroker API never sends the actual outbound requests itself** — it only hands out accounts/proxies and tracks their usage. Your project makes the real request to the target site/API directly, using the credentials/proxies it was issued.

Below is an example scheme of using the SDK to make a request to an external resource:

```
┌──────────────────┐    HTTP (get pair,          ┌─────────────────────────┐
│  your project    │    report usage, etc.)      │      PairBroker API     │
│ (uses this SDK)  │ ──────────────────────────► │      (self-hosted)      │
│                  │ ◄────────────────────────── │                         │
└────────┬─────────┘                             └────────────┬────────────┘
         │                                                    │
         │  the actual HTTP request — sent directly           │
         │  by your project, using whatever account/          │
         │  proxy it was issued (not through                  ▼
         │  PairBroker API)                      ┌─────────────────────────┐
         ▼                                       │ your accounts & proxies │
┌─────────────────────┐                          └─────────────────────────┘
│  external target    │
│ (the site/API you   │
│  actually wanted)   │
└─────────────────────┘
```

So there are two moving parts, not one:

1. **PairBroker API** — the backend you self-host. It stores accounts/proxies and exposes the REST API this SDK calls.
2. **`pairbroker-sdk`** (this repo) — the client you `pip install` into whatever project needs to consume those accounts/proxies.

---

## Features

| Feature | Description |
|---|---|
| **Auto-rotation** | Gets a fresh account + proxy pair on each call |
| **Rate limiting** | Tracks per-account limits and enforces delays |
| **Retries** | Automatic retries on 5xx / 429 / network errors with exponential backoff |
| **Auth strategies** | `TokenAuth()`, `PasswordAuth()`, `CookiesAuth()` |
| **Usage reporting** | `require_report=True` — auto-sends usage stats after each request |
| **Proxy management** | Auto-applies proxies, disables them on proxy errors |

---

## Prerequisites

Before this SDK is useful, you need a running **PairBroker API** instance — a self-hosted backend (Django + DRF + PostgreSQL) that stores your accounts/proxies and exposes the REST API this SDK talks to. It's not public yet (see the status note above), so for now this means either your own compatible deployment or an instance provided to you by whoever runs one for your team.

Once you have an instance running:

### 1. Get an API key

In the PairBroker API admin, create a client for your project — it generates an API key. That's the `api_key` you'll pass to `PairBrokerClient` below.

### 2. Point the SDK at your instance

The SDK needs to know the base URL of wherever PairBroker API is running. Either pass it directly:

```python
client = PairBrokerClient(api_key="...", base_url="https://your-pairbroker-instance.example.com")
```

or set it via environment variable (see [Configuration](#configuration)):

```bash
export PAIRBROKER__API_BASE_URL=https://your-pairbroker-instance.example.com
```

`base_url=` takes priority if both are set. If neither is provided, `PairBrokerClient` raises a `ValueError` on construction.

---

## Installation

```bash
pip install pairbroker-sdk
```

**Requirements:** Python 3.10+, `aiohttp`, `pydantic`, `pydantic-settings`

---

## Quick start

```python
import asyncio
from pairbroker import PairBrokerClient, TokenAuth

async def main():
    async with PairBrokerClient(api_key="YOUR_API_KEY") as client:
        pair = await client.get_pair("your_account_type")
        async with pair:
            resp = await pair.request(
                "https://api.example.com/check",
                params={"query": "value"},
                auth_strategy=TokenAuth(auth_header="Key"),
            )
            print(resp.json)

asyncio.run(main())
```

---

## API Reference

### Single pair

```python
async with PairBrokerClient(api_key="...") as client:
    pair = await client.get_pair("account_type")
    async with pair:
        resp = await pair.request("https://api.example.com")
```

### Bulk requests

```python
async for pair, url in client.iter_pairs(
    account_type="account_type",
    items=["url1", "url2", "url3"],
    limit=10,
    require_report=True,
):
    async with pair:
        resp = await pair.request(url)
```

### Auth strategies

```python
from pairbroker import TokenAuth, PasswordAuth, CookiesAuth

# Bearer / API key header
token_auth = TokenAuth(
    auth_header="Authorization",
    prefix="Bearer",
)

# Login + password (posts credentials, stores session cookies)
password_auth = PasswordAuth(
    login_url="https://site.com/login",
    auth_request_method="POST",
    payload_map={"email": "login", "password": "password"},
)
```

### `request()` parameters

```python
await pair.request(
    url="https://api.example.com",
    method="GET",               # GET / POST / PUT / PATCH / DELETE
    use_proxy=True,             # whether to use a proxy from PairBroker
    auth_strategy=TokenAuth(),

    # request params (passed through to aiohttp)
    params={"page": 1},
    json={"data": "value"},
    data={"name": "user"},
    headers={"User-Agent": "..."},
    cookies={"session": "abc"},

    # transport settings
    timeout=30,                 # request timeout in seconds
    retries=3,                  # retry attempts on 429 / 5xx / network error
    delay_multiplier=0.5,       # exponential backoff multiplier (0.5s → 1s → 2s)
)
```

### `TransportResponse` fields

| Field | Type | Description |
|---|---|---|
| `ok` | `bool` | `True` if `status_code < 400` |
| `status_code` | `int \| None` | HTTP status code |
| `reason` | `str \| None` | HTTP reason phrase |
| `text` | `str \| None` | Raw response text |
| `json` | `Any \| None` | Parsed JSON body |
| `headers` | `Mapping[str, str] \| None` | Response headers |
| `cookies` | `SimpleCookie \| None` | Response cookies |
| `url` | `str \| None` | Final URL (after redirects) |
| `proxy` | `str \| None` | Proxy that was used |
| `method` | `str \| None` | Request method |
| `elapsed` | `float \| None` | Request duration in seconds |
| `attempt` | `int \| None` | Successful attempt number |
| `exception` | `Exception \| None` | Exception raised, if any |
| `error` | `str \| None` | Serialized error string |

**Convenience properties:**

| Property | Type | Description |
|---|---|---|
| `result` | `str \| None` | Human-readable result summary |
| `is_proxy_error` | `bool` | `True` if the error was proxy-related |

---

## Configuration

SDK uses `pydantic-settings` for configuration. All settings can be overridden via environment variables with the `PAIRBROKER__` prefix.

```python
from pairbroker.config import settings

print(settings.API_BASE_URL)
print(settings.DEFAULT_RETRIES)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `API_BASE_URL` | `str \| None` | `None` | Base URL of **your** PairBroker API instance. Can also be passed as `base_url=` to `PairBrokerClient()`, which takes priority; one of the two is required |
| `MIN_SECONDS_BETWEEN_REQUESTS` | `float` | `0.25` | Min pause between requests per client |
| `DEFAULT_RETRIES` | `int` | `3` | Retry attempts on network / 5xx / 429 |
| `DEFAULT_RETRY_DELAY_MULTIPLIER` | `float` | `0.5` | Exponential backoff multiplier |
| `ITER_PAIRS_MAX_CONSECUTIVE_ERRORS` | `int` | `10` | Max consecutive errors before stopping iteration |

Example `.env`:

```dotenv
PAIRBROKER__API_BASE_URL=https://your-pairbroker-instance.example.com
PAIRBROKER__DEFAULT_RETRIES=5
PAIRBROKER__MIN_SECONDS_BETWEEN_REQUESTS=0.1
```

---

## Examples

See the [`examples/`](./examples/) folder:

| Scenario | File |
|---|---|
| Single request | [`basic_usage.py`](./examples/basic_usage.py) |
| Bulk requests | [`iter_pairs_usage.py`](./examples/iter_pairs_usage.py) |
| Login / password auth | [`password_auth_usage.py`](./examples/password_auth_usage.py) |
| VirusTotal API | [`virustotal_real.py`](./examples/virustotal_real.py) |

---

## Contributing

1. Clone the repo and install dependencies including the dev group: `uv sync --group dev`
2. Make your changes
3. Lint: `uv run ruff check src/ tests/`
4. Test: `uv run pytest tests/`
5. Push your branch and open a PR — CI runs lint and tests on every PR automatically

Releases are published to PyPI automatically by CI when a maintainer pushes a `vX.Y.Z` tag — no manual `twine upload` needed. See [CHANGELOG.md](./CHANGELOG.md) for release history.

---

## License

[MIT](./LICENSE)
