Metadata-Version: 2.4
Name: gatewire
Version: 1.1.0
Summary: Official Python SDK for the GateWire SMS Infrastructure.
License-Expression: MIT
Project-URL: Homepage, https://gatewire.raystate.com
Project-URL: Repository, https://github.com/md-lotfi/gatewire-python
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# GateWire Python SDK

The official Python library for the **[GateWire SMS Infrastructure](https://gatewire.raystate.com)**.

Send OTPs to North African carriers (**Mobilis**, **Djezzy**, **Ooredoo**) using our
decentralized mesh network.

---

## Installation

```bash
pip install gatewire
```

Requires Python 3.10+ and `requests>=2.28`.

---

## Quick Start

```python
from gatewire import GateWireClient, GateWireException

gw = GateWireClient(api_key="YOUR_API_TOKEN")

# 1. Send OTP
res = gw.dispatch(phone="+213555123456", template_key="login_otp")
reference_id = res["reference_id"]   # store this; you'll need it for verification

# 2. Verify the code the user entered
gw.verify_otp(reference_id=reference_id, code=user_input)

# 3. (Optional) Poll status yourself
info = gw.status(reference_id)
print(info["status"])  # pending | sent | verified | failed | expired
```

---

## API Reference

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

Creates the client. A single `requests.Session` is opened and reused for all requests.

| Parameter  | Type  | Default                                       | Description        |
|------------|-------|-----------------------------------------------|--------------------|
| `api_key`  | `str` | —                                             | Your API token     |
| `base_url` | `str` | `https://gatewire.raystate.com/api/v1`        | Override for tests |

---

### `dispatch(phone, template_key=None) -> dict`

Send an OTP to a phone number.

| Parameter      | Type            | Required | Description                                                                   |
|----------------|-----------------|----------|-------------------------------------------------------------------------------|
| `phone`        | `str`           | Yes      | Recipient number in E.164 format (e.g. `+213555123456`)                      |
| `template_key` | `str` or `None` | No       | Template key configured in the dashboard. Omit to use the default template.  |

**Returns** `{"reference_id": "wg_01HX...", "status": "pending"}`

**Raises** `GateWireException` — see [Error Handling](#error-handling).

---

### `verify_otp(reference_id, code) -> dict`

Verify the OTP code entered by the end-user.

| Parameter      | Type  | Required | Description                             |
|----------------|-------|----------|-----------------------------------------|
| `reference_id` | `str` | Yes      | The `reference_id` returned by `dispatch()` |
| `code`         | `str` | Yes      | The code the user submitted             |

**Returns** `{"status": "verified", "message": "Success"}`

**Raises** `GateWireException` (HTTP 400) on wrong, expired, cancelled, or already-used codes.

---

### `status(reference_id) -> dict`

Check the delivery status of an OTP.

| Parameter      | Type  | Required | Description                             |
|----------------|-------|----------|-----------------------------------------|
| `reference_id` | `str` | Yes      | The `reference_id` returned by `dispatch()` |

**Returns** `{"reference_id": "wg_01HX...", "status": "sent", "created_at": "2026-03-03T14:22:00Z"}`

Possible `status` values: `pending` · `dispatched` · `sent` · `verified` · `failed` · `expired` · `cancelled`

---

## Error Handling

Every non-2xx response raises `GateWireException`. Inspect `e.status_code` to branch on the cause.

```python
from gatewire import GateWireClient, GateWireException

gw = GateWireClient(api_key="YOUR_API_TOKEN")

try:
    res = gw.dispatch(phone="+213555123456")
    reference_id = res["reference_id"]
except GateWireException as e:
    if e.status_code == 402:
        # Wallet is empty — direct the user to top up
        print("Insufficient balance. Please top up your account.")
    elif e.status_code == 429:
        # Daily or hourly rate limit hit — back off and retry later
        print(f"Rate limit reached: {e}")
    elif e.status_code == 503:
        # No devices are online right now — retry after a short delay
        print("No devices available. Retrying in 30 s…")
    else:
        print(f"Unexpected error: {e}")

try:
    gw.verify_otp(reference_id=reference_id, code=user_input)
except GateWireException as e:
    if e.status_code == 400:
        # Wrong code, already used, expired, or cancelled
        print(f"Verification failed: {e}")
```

`GateWireException` attributes:

| Attribute    | Type  | Description                                      |
|--------------|-------|--------------------------------------------------|
| `args[0]`    | `str` | Human-readable error message from the API        |
| `status_code`| `int` | HTTP status code; `0` for network-level failures |

`str(e)` renders as `[<status_code>] <message>` (or just `<message>` for network errors).

---

## OTP Lifecycle

```
dispatch()
    │
    ▼
 pending ──► dispatched ──► sent ──► verified   ← happy path
                │               │
                ▼               ▼
             failed           expired
                │
                ▼
            cancelled
```

Poll `status()` after `dispatch()` if you need to confirm delivery before asking the user for their code.

---

## Framework Integration

Create one `GateWireClient` at module level and reuse it across requests. The underlying
`requests.Session` is thread-safe for concurrent reads.

**Django** (`myapp/services.py`):

```python
from django.conf import settings
from gatewire import GateWireClient

gw = GateWireClient(api_key=settings.GATEWIRE_API_KEY)
```

**Flask** (`app/__init__.py`):

```python
from flask import Flask
from gatewire import GateWireClient

app = Flask(__name__)
gw = GateWireClient(api_key=app.config["GATEWIRE_API_KEY"])
```

Then import `gw` wherever you need it — no need to instantiate it per request.

---

## Development & Testing

Clone the repo and install the dev dependencies:

```bash
git clone https://github.com/md-lotfi/gatewire-python.git
cd gatewire-python
pip install -e ".[dev]"
```

Run the test suite:

```bash
pytest
```

The suite uses `unittest.mock` only — no network calls are made. All tests run in under a second.

### Test coverage at a glance

| Module | Test file | Tests |
|---|---|---|
| `gatewire.exceptions` | `tests/test_exceptions.py` | 8 |
| `gatewire.client` | `tests/test_client.py` | 45 |

Key scenarios covered:

- Session created once; headers set correctly
- `dispatch()` hits `POST /send-otp`; `template_key` omitted when `None`
- `verify_otp()` hits `POST /verify-otp`; all 400 error variants
- `status()` hits `GET /status/{reference_id}`; all seven lifecycle values
- Error body `"error"` key preferred over `"message"`; fallback to `HTTP <code>`
- Network errors (`ConnectionError`, `Timeout`) raise `GateWireException` with `status_code=0`
- Invalid JSON response body handled gracefully
- Removed methods (`get_balance`) and removed parameters (`message`, `priority`) confirmed absent

---

## License

MIT — see [LICENSE](LICENSE).
