Metadata-Version: 2.4
Name: chatads-sdk
Version: 0.2.1
Summary: Lightweight Python client for the ChatAds affiliate scoring API
Author-email: ChatAds <chris@getchatads.com>
License: MIT
Project-URL: Homepage, https://docs.getchatads.com
Project-URL: Documentation, https://docs.getchatads.com
Keywords: chatads,sdk,ads,automation
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx<1.0,>=0.27
Provides-Extra: async
Requires-Dist: httpx[http2]<1.0,>=0.27; extra == "async"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"

# ChatAds Python SDK

A tiny, dependency-light wrapper around the ChatAds `/v1/chatads/messages` endpoint. It mirrors the response payloads returned by the Go API so you can drop it into CLIs, serverless functions, or orchestration tools.

Learn more at [ChatAds](https://www.getchatads.com).

## Installation

```bash
pip install chatads-sdk
```

The package is published on [PyPI](https://pypi.org/project/chatads-sdk/). Install from source only if you're developing locally.

## Quickstart

```python
from chatads_sdk import ChatAdsClient, AsyncChatAdsClient, FunctionItemPayload

# Synchronous usage
with ChatAdsClient(
    api_key="YOUR_X_API_KEY",
    base_url="https://api.getchatads.com",
    raise_on_failure=True,
    max_retries=2,
    retry_backoff_factor=0.75,
) as client:
    payload = FunctionItemPayload(
        message="Looking for a CRM to close more deals",
        country="US",
        max_offers=2,
    )
    result = client.analyze(payload)
    if result.data and result.data.returned > 0:
        offer = result.data.offers[0]
        print(offer.link_text, offer.url)
    else:
        print("No match")

# Async usage
async with AsyncChatAdsClient(
    api_key="YOUR_X_API_KEY",
    base_url="https://api.getchatads.com",
    max_retries=3,
) as async_client:
    result = await async_client.analyze_message(
        "Need data warehousing ideas",
        country="US",
    )
    print(result.raw)
```

## Request Options

The `FunctionItemPayload` supports these fields:

| Field | Type | Description |
|-------|------|-------------|
| `message` | str (required) | Message to analyze (max 10,000 chars) |
| `ip` | str | IPv4/IPv6 address for country detection (max 45 characters) |
| `country` | str | Country code (e.g., 'US'). If provided, skips IP-based country detection |
| `extraction_mode` | str | Extraction control: `none` or `standard` (default) |
| `resolution_mode` | str | Resolution control: `none` or `standard` (default) |
| `max_offers` | int | Max distinct keyterms with offers extracted (1-3, default 1) |
| `max_products_per_offer` | int | Max resolved products per offer carousel (1-3, default 1) |
| `metadata` | dict | Arbitrary JSON metadata passed through to the response (max 10KB) |

## Response Structure

```python
result.data.offers          # List[Offer] - Array of affiliate offers
result.data.requested       # int - Number of offers requested
result.data.returned        # int - Number of offers returned
result.error                # ChatAdsError or None (code, message)
result.meta.request_id      # Unique request identifier
result.meta.usage           # UsageInfo with quota information
result.raw                  # Full raw JSON response

# Response data has:
result.data.status          # "filled", "partial_fill", "no_offer", "message_too_short", etc.

# Each Offer has:
offer.link_text             # Text to use for the affiliate link
offer.url                   # Affiliate URL (empty when resolution_mode=none)
offer.is_branded            # bool | None — True if term was brand-derived, False if generic, None if unavailable (e.g. extraction_mode=none)
```

## Error Handling

Non-2xx responses raise `ChatAdsAPIError` with:
- `status_code` - HTTP status code
- `response.error.code` - Error code (e.g., `DAILY_LIMIT_EXCEEDED`, `MONTHLY_LIMIT_EXCEEDED`)
- `response.error.message` - Human-readable message
- `retry_after` - Seconds to wait (for 429 responses)

Set `raise_on_failure=True` to also raise on 200 responses with error responses.

**Retryable status codes** (automatic with `max_retries>0`):
- `408` Request Timeout
- `429` Rate Limited
- `500`, `502`, `503`, `504` Server errors

## Notes

- Retries are opt-in. Provide `max_retries>0` to automatically retry transport errors and retryable status codes. The client honors `Retry-After` headers and falls back to exponential backoff.
- `base_url` must point to your HTTPS deployment (the client rejects plaintext URLs so API keys are never transmitted insecurely).
- The default hosted environment lives at `https://api.getchatads.com`; use your own domain if you're proxying ChatAds behind something else.
- `FunctionItemPayload` matches the server-side Go request struct. Keyword arguments passed to `ChatAdsClient.analyze_message()` accept either snake_case or camelCase keys.
- Reserved payload keys (e.g., `message`, `country`) cannot be overridden through `extra_fields`; doing so raises `ValueError` to prevent silent mutations.
- `debug=True` enables structured request/response logging, but payload contents are redacted automatically so you don't leak PII into logs.

## CLI Smoke Test

For a super-quick check, either edit the config block at the top of `run_sdk_smoke.py` or set:

```bash
export CHATADS_API_KEY="..."
export CHATADS_BASE_URL="https://api.getchatads.com"
export CHATADS_MESSAGE="Looking for ergonomic office chairs"
# Optional extras
export CHATADS_IP="1.2.3.4"
export CHATADS_COUNTRY="US"
```

Then run:

```bash
python run_sdk_smoke.py
```

It prints the raw JSON response or surfaces a `ChatAdsAPIError` with status/error fields so you can see exactly what the API returned.
