Metadata-Version: 2.4
Name: sendafrica
Version: 1.0.1
Summary: Official Python SDK for the SendAfrica SMS Infrastructure-as-a-Service API
Author: CamelTech
License: MIT
Keywords: sms,sendafrica,africa,tanzania,sdk
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Provides-Extra: async
Requires-Dist: httpx>=0.24; extra == "async"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: responses; extra == "dev"

# SendAfrica Python SDK

Official Python client for the SendAfrica SMS Infrastructure-as-a-Service API.
Designed to feel like Stripe's Python library: simple for a first integration,
enough control for production use.

## Install

```bash
pip install sendafrica
# for the async client:
pip install "sendafrica[async]"
```

## Quickstart

```python
from sendafrica import SendAfrica

client = SendAfrica(api_key="sk_live_xxxxx")

result = client.sms.send(to="0712345678", message="Welcome to SendAfrica")
print(result.message_id, result.status, result.credits_used)
```

The API key can also be set via the `SENDAFRICA_API_KEY` environment
variable, in which case `SendAfrica()` needs no arguments.

## Configuration

```python
client = SendAfrica(
    api_key="key",
    timeout=30,          # seconds, default 10
    max_retries=3,        # default 3, retries on 429/5xx and connection errors
    environment="production",
    debug=True,            # logs each request/response to stdout
)
```

## Resources

| Resource | Methods |
|---|---|
| `client.sms` | `send`, `send_many`, `analyze` |
| `client.credits` | `balance`, `history` |
| `client.packages` | `list` |
| `client.payments` | `create` |
| `client.webhooks` | `parse` |

`packages.get(id)` and `payments.get/list` don't exist: the API only
exposes `GET /v1/packages` (list) and `POST /v1/payments/` (create) to API
key callers — fetching/listing individual payment orders is an
admin-console (JWT + `is_admin`) feature with no API-key equivalent.

### SMS

```python
client.sms.send(to="0712345678", message="Your OTP is 123456", sender="MyBrand")

results = client.sms.send_many([
    {"to": "0711111111", "message": "Hello John"},
    {"to": "0722222222", "message": "Hello Mary"},
])
print(results.sent_count, results.failed_count)

analysis = client.sms.analyze("Habari 😊")
# SMSAnalysis(encoding="UCS-2", characters=8, parts=1, credits=1)
```

Phone numbers are normalized locally (e.g. `0712345678` → `+255712345678`)
before any network call, and invalid numbers raise `InvalidPhoneError`
without hitting the API.

### Credits & Packages

```python
balance = client.credits.balance()
history = client.credits.history(page=1, per_page=20)
packages = client.packages.list()
```

### Payments

```python
payment = client.payments.create(
    package_id="pkg-pro",
    provider="mpesa",
    amount=50000,
    phone="+255712345678",
)
```

### Webhooks (FastAPI example)

> **Status:** speculative. The SendAfrica API does not yet forward signed
> events to customer endpoints — this resource is ready for when that
> ships server-side.

```python
from fastapi import Request

@app.post("/webhooks/sendafrica")
async def webhook(request: Request):
    event = client.webhooks.parse(
        await request.body(),
        signature=request.headers.get("X-SendAfrica-Signature"),
    )
    if event.type == "sms.delivered":
        print(event.message_id)
```

## Errors

All errors inherit from `SendAfricaError`:

```python
from sendafrica.exceptions import InsufficientCreditsError

try:
    client.sms.send(to="0712345678", message="hello")
except InsufficientCreditsError:
    print("Please buy credits")
```

| Exception | Meaning |
|---|---|
| `AuthenticationError` | invalid/missing API key |
| `ValidationError` | bad request payload |
| `InvalidPhoneError` | phone number failed validation |
| `InsufficientCreditsError` | not enough SMS credits |
| `RateLimitError` | too many requests (`.retry_after` in seconds) |
| `NotFoundError` | resource does not exist |
| `ServerError` | 5xx from the API |
| `APIConnectionError` | network/timeout failure |
| `WebhookSignatureError` | webhook signature mismatch |

## CLI

```bash
export SENDAFRICA_API_KEY="SA_d10f5f01a8fc34415522edef675fc3e3b4487c11"
sendafrica balance
sendafrica sms send --to 0712345678 --message "Hello"
```

## Async

```python
from sendafrica import AsyncSendAfrica

client = AsyncSendAfrica(api_key="key")
result = await client.sms.send(to="0712345678", message="Hello")
await client.aclose()
```

> **Status:** the async transport (`AsyncSendAfrica`, httpx-based, with
> retries/backoff) is implemented. Resource classes currently call
> `transport.request(...)` without `await` internally, which works for the
> sync client but not yet for async — Phase 2 work is wiring `await` through
> `SMSResource`/`CreditsResource`/etc. (or splitting async-aware resource
> variants). Tracked as the next task before shipping async support.

## Project layout

```
sendafrica/
├── client.py          # SendAfrica, AsyncSendAfrica
├── http.py             # HTTPTransport, AsyncHTTPTransport (retries, auth, error mapping)
├── auth.py             # API key resolution
├── exceptions.py       # error hierarchy
├── models.py           # response dataclasses
├── cli.py               # `sendafrica` command
├── resources/
│   ├── sms.py
│   ├── credits.py
│   ├── payments.py
│   ├── packages.py
│   └── webhooks.py
└── utils/
    ├── phone.py         # E.164 normalization
    ├── sms.py             # GSM-7/UCS-2 encoding + segment analysis
    └── validators.py
```

## Roadmap

- **Phase 1 (done):** client, auth, SMS send, balance, packages, payments, errors, types
- **Phase 2:** finish async resource wiring, bulk SMS rate-limiting polish, CLI expansion
- **Phase 3:** campaigns, contacts, templates, scheduling
