Metadata-Version: 2.5
Name: setalabs
Version: 1.0.0
Summary: Python client for the Setalabs API gateway
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: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# Setalabs Python SDK

Client for the Setalabs gateway. Each API has its own uuid, key, and set of endpoints. The gateway address is shared.

The key secret is used only to sign requests. It is never sent.

## Installation

```bash
pip install setalabs
```

Until the package is published, install it from a local path:

```bash
pip install -e ../setalabs-python-sdk
```

## Requests

The default gateway address is `setalabs.Gateway.URL` (`https://gw.setalabs.com`). Pass `gateway` to use another one. An OpenAPI server URL already includes the API uuid; this SDK takes the uuid separately.

```python
from setalabs import Client
from setalabs.exceptions import ApiException, RateLimitException, ValidationException

client = Client(
    api_uuid="05141812-7772-403e-9ca6-5e8e8bf22730",
    api_key="your-key",
    api_secret="your-secret",
)

local = Client(
    api_uuid="05141812-7772-403e-9ca6-5e8e8bf22730",
    api_key="your-key",
    api_secret="your-secret",
    gateway="http://localhost:8080",
)

response = client.get("/catalog", {"language": "en"})
data = response.json()
```

The path is the endpoint path from the OpenAPI document (`/catalog`, `/product/{id}`, `/search`), without the `/v1/api/{uuid}` prefix. Substitute path parameters yourself: `/product/12345`.

Another API on the same gateway:

```python
other = client.with_api("another-api-uuid", "other-key", "other-secret")
```

If the key has no secret, pass an empty string. The request is not signed.

```python
client.post("/orders", {"sku": "A1"})
client.request(
    "PUT",
    "/orders/1",
    json={"status": "paid"},
    query={"language": "en"},
    timeout=10,
)
```

A successful response is the upstream body and status, unchanged. The gateway returns the request id in `X-Request-Id`:

```python
response.status_code
response.request_id()
response.json()
```

`Client` can be used as a context manager. That closes the underlying HTTP connection pool.

## Signature

When a secret is set, every request includes:

- `X-Api-Key`
- `X-Api-Timestamp` — Unix time in seconds
- `X-Api-Sign` — `HMAC-SHA256(key + secret + timestamp + path?query, secret)`, lowercase hex

`path?query` is the path the gateway sees, plus the raw query string. For example: `/v1/api/{uuid}/catalog?language=en`. Parameter order is preserved. The query string uses the same encoding as PHP `http_build_query` with `PHP_QUERY_RFC3986`.

## Errors

A response with status 400 or higher raises an exception. The message comes from the gateway `reason`. Every API error exposes `status_code`, `error`, `reason`, `request_id`, and the original `response`.

| HTTP | `error` | Exception | When |
| --- | --- | --- | --- |
| 400 | `bad_request` | `BadRequestException` | The API has no base URL |
| 401 | `unauthorized` | `UnauthorizedException` | Missing or invalid key, missing or invalid signature, missing timestamp |
| 403 | `forbidden` | `ForbiddenException` | Key is blocked, subscription has expired, no active tariff, or the key belongs to another API |
| 404 | `not_found` | `NotFoundException` | API or endpoint was not found |
| 422 | `validation_error` | `ValidationException` | Required fields are missing; names are in `missing_fields` |
| 429 | `rate_limited` | `RateLimitException` | Plan RPS limit exceeded; wait `retry_after` seconds |
| 429 | `usage_limit_exceeded` | `UsageLimitException` | Call quota for the period is exhausted |
| 500 | `internal_error` | `ServerException` | Gateway internal error |
| 502 | `bad_gateway` | `BadGatewayException` | Upstream is unavailable |

A network failure raises `TransportException`. `SetalabsException` is the base type for gateway and transport errors. Invalid client arguments raise `ValueError`.

```python
import time

try:
    data = client.get("/search", {"text": "phone", "page": 1, "limit": 24}).json()
except ValidationException as exc:
    exc.missing_fields
except RateLimitException as exc:
    time.sleep(exc.retry_after or 1)
except ApiException as exc:
    exc.status_code
    exc.reason
    exc.request_id
```
