Metadata-Version: 2.4
Name: zeridion-flare
Version: 0.2.0
Summary: Python SDK for the Zeridion Flare managed background jobs API
Project-URL: Homepage, https://zeridion.com
Project-URL: Repository, https://github.com/zeridion/zeridion
Project-URL: Documentation, https://docs.zeridion.com/flare
License: MIT
Keywords: background-jobs,flare,queue,workers,zeridion
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pydantic>=2; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: pydantic
Requires-Dist: pydantic>=2; extra == 'pydantic'
Description-Content-Type: text/markdown

# zeridion-flare

Python SDK for the [Zeridion Flare](https://docs.zeridion.com/flare) managed background jobs API.

[![PyPI](https://img.shields.io/pypi/v/zeridion-flare)](https://pypi.org/project/zeridion-flare/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

**[Full documentation at docs.zeridion.com/flare](https://docs.zeridion.com/flare)**

## Installation

```bash
pip install zeridion-flare
```

Requires Python 3.10+ and [`httpx`](https://www.python-httpx.org/) (installed automatically).

## Quick start

```python
from zeridion_flare import FlareClient

client = FlareClient(api_key="zf_live_sk_...")
# Or, with FLARE_API_KEY set in the environment:
#   client = FlareClient()

# Enqueue a job
job = client.create_job({
    "job_type": "SendWelcomeEmail",
    "payload": {"email": "alice@example.com"},
    "queue": "default",
    "max_attempts": 3,
})

print(job["id"], job["state"])  # "job_abc123", "pending"
```

## API

### `FlareClient(api_key, base_url=...)`

| Argument   | Type  | Required | Default                                    |
|------------|-------|----------|--------------------------------------------|
| `api_key`  | `str` | no       | from `FLARE_API_KEY` env var               |
| `base_url` | `str` | no       | `"https://api.zeridion.com/flare/v1"`      |

All methods accept optional `idempotency_key` and `request_id` keyword
arguments, sent as the `Idempotency-Key` and `X-Request-Id` headers
respectively. Use `request_id` to correlate SDK calls with your own logs.

### Jobs

```python
# Create a job
job = client.create_job({"job_type": "...", "payload": {...}}, idempotency_key="optional")

# Get a job — returns None if not found (404)
detail = client.get_job("job_abc123")

# List jobs with optional filters and cursor pagination
page = client.list_jobs(state="failed", queue="critical", limit=25)

# Cancel a pending job — returns None if already in a non-cancellable state (409)
result = client.cancel_job("job_abc123")

# Retry a failed / dead-lettered job — returns None if not retryable (409)
result = client.retry_job("job_abc123")
```

### Workers (advanced)

```python
# Poll for available jobs
poll = client.poll_workers({"worker_id": "w1", "queues": ["default"], "capacity": 5})

# Acknowledge a completed job
ack = client.ack_worker({
    "job_id": poll["jobs"][0]["id"],
    "worker_id": "w1",
    "status": "succeeded",   # or "failed" / "cancelled"
    "duration_ms": 120,
})
```

### Context manager

```python
with FlareClient(api_key="zf_live_sk_...") as client:
    job = client.create_job({"job_type": "MyJob"})
# HTTP connection pool closed automatically
```

## Error handling

The SDK raises typed exceptions that all inherit from `FlareError`:

```python
from zeridion_flare import (
    FlareError,
    AuthError,       # 401 — invalid API key
    QuotaError,      # 402 — quota exceeded
    NotFoundError,   # 404 — job not found
    ConflictError,   # 409 — idempotency conflict / invalid state
    RateLimitError,  # 429 — rate limit exceeded
)

try:
    client.create_job({"job_type": "MyJob"})
except RateLimitError as e:
    print(f"Rate limited. Retry after epoch: {e.retry_after}")
except AuthError:
    print("Check your API key")
except FlareError as e:
    print(e.code, e.request_id, e.status_code)
```

See the [stable error-code registry](https://docs.zeridion.com/flare/api/errors) for every `error.code` string the API can return.

## Automatic retries

The SDK automatically retries HTTP 429 / 502 / 503 / 504 responses and transient network errors (`httpx.NetworkError`, `httpx.TimeoutException`) with exponential backoff + jitter. The `Retry-After` header is honored when present.

| Argument               | Default  | Notes                                                  |
|------------------------|----------|--------------------------------------------------------|
| `max_retries`          | `3`      | Set to `0` to disable retries entirely.                |
| `retry_base_delay_ms`  | `500`    | Base for the exponential schedule.                     |
| `retry_max_delay_ms`   | `30000`  | Cap on a single backoff wait (and on `Retry-After`).   |

```python
client = FlareClient(
    api_key="zf_live_sk_...",
    max_retries=5,
    retry_base_delay_ms=200,
)
```

## Verifying webhook signatures

If you've configured outbound webhooks via the `/flare/v1/webhooks` API, verify the `X-Zeridion-Signature` header on each incoming delivery before processing the event:

```python
from zeridion_flare import verify_webhook

@app.post("/hooks/zeridion")
async def receive(request):
    raw = await request.body()
    header = request.headers.get("x-zeridion-signature", "")
    if not verify_webhook(raw, header, WEBHOOK_SECRET, tolerance_seconds=300):
        return Response(status_code=400)
    # ... process the event ...
    return Response(status_code=200)
```

`verify_webhook` is HMAC-SHA256 over `<unix_timestamp>.<raw_body>`, constant-time-compared against every `v1=` value in the header (supports secret rotation). The optional `tolerance_seconds` parameter rejects replays older than that many seconds.

## Sample app

A runnable starter (enqueue → poll → ack in a single file) lives at
[`samples/python-starter/`](../../samples/python-starter/).

## Links

- Documentation: https://docs.zeridion.com/flare
- Dashboard: https://dashboard.zeridion.com/
- .NET SDK: ../csharp/README.md
- TypeScript SDK: ../typescript/README.md
