Metadata-Version: 2.4
Name: herosms
Version: 0.1.1
Summary: Async Python client for HeroSMS (sms-activate.ru compatible) API
License: MIT
Keywords: async,herosms,otp,sms,sms-activate
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: tenacity>=8.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# herosms

Async and sync Python client for [HeroSMS](https://hero-sms.com/?ref=911497) (SMS-Activate compatible API).

## Features

- Async-first client with context-manager support
- Synchronous wrapper for non-async projects
- Activation lifecycle helpers (create, wait, resend, complete, cancel)
- Rent APIs support
- Reference data helpers (countries, services, operators, prices)
- Typed models with Pydantic v2
- Custom exception hierarchy
- Webhook payload parsing for incoming SMS events

## Installation

```bash
pip install herosms
```

For development:

```bash
uv sync --extra dev
```

## Quickstart (async)

```python
import asyncio

from herosms import HeroSMSClient


async def main() -> None:
    async with HeroSMSClient(api_key="YOUR_API_KEY") as client:
        balance = await client.get_balance()
        print(f"Balance: {balance}")

        activation = await client.activations.create(service="tg", country=6)
        print("Phone:", activation.phone)  # available immediately

        status = await activation.wait_for_sms(timeout=120)
        print("Code:", status.code)
        await activation.complete()


asyncio.run(main())
```

## Quickstart (sync)

```python
from herosms import SyncHeroSMSClient


with SyncHeroSMSClient(api_key="YOUR_API_KEY") as client:
    balance = client.get_balance()
    print(f"Balance: {balance}")
```

## Response formats (`response_format`)

Most methods support `response_format` with three modes:

- `"model"` (default): return typed Pydantic models / parsed structures.
- `"json"`: return transport payload as JSON-compatible value.
- `"raw"`: return raw API payload as plain string.

For endpoints that return legacy plain text (for example `ACCESS_BALANCE:0.84` or `STATUS_WAIT_CODE`), `response_format="json"` wraps text into:

```json
{"type": "text", "raw": "..."}
```

This keeps JSON mode consistent even when the upstream API responds with plain text.

## HTTP debug logging

Enable HTTP debug logs when constructing the client:

```python
from herosms import HeroSMSClient

client = HeroSMSClient(
    api_key="YOUR_API_KEY",
    debug=True,
    log_body_max_length=500,
)
```

Parameters:

- `debug`: enables request/response debug logs (`herosms.http` logger).
- `log_body_max_length`: maximum number of response-body characters to log.
- `log_body_max_length=None`: disables trimming and logs full body.

## Activation lifecycle

### Manual mode (recommended, immediate phone)

```python
activation = await client.activations.create(service="tg", country=6)
print(activation.phone)  # available right after create

status = await activation.wait_for_sms(timeout=120)
print(status.code)

await activation.complete()
# or
await activation.cancel()
```

### Async context manager mode (`async with`)

```python
async with await client.activations.create(service="tg", country=6) as activation:
    status = await activation.wait_for_sms(timeout=120)
    print(status.code)
    await activation.complete()
```

### Continue existing activation by `activation_id` (attach)

```python
activation = client.activations.attach(
    activation_id=777001,
    phone_number="79990009999",  # optional
)

status = await activation.wait_for_sms(timeout=120)
print(status.code)

await activation.complete()
```

Request another SMS:

```python
await activation.request_sms()
```

Get all OTP messages for activation:

```python
messages = await activation.get_all_sms()
```

## Legacy status endpoint (`getStatus`)

Use `get_status_v1` if you need raw text-compatible status values from legacy integrations:

```python
raw_status = await client.get_status_v1(activation_id=123456)
print(raw_status)  # e.g. STATUS_WAIT_CODE or STATUS_OK:1234
```

Sync variant:

```python
raw_status = client.get_status_v1(activation_id=123456)
```

## Sync/async parity

Both clients expose the same core API surface:

- `HeroSMSClient` (async) — use with `await`
- `SyncHeroSMSClient` (sync) — same method names without `await`

Activation namespace parity (`client.activations.*`):

- `create(...)`
- `attach(...)`
- `list_active(...)`
- `get_active_activations(...)` (alias of `list_active`)
- `get_history(...)`
- `finish(...)`
- `cancel(...)`
- `get_sms(...)`

### `get_active_activations` examples

Async:

```python
active = await client.activations.get_active_activations(limit=10)
print(active)
```

Sync:

```python
active = client.activations.get_active_activations(limit=10)
print(active)
```

### History examples

Async:

```python
history = await client.get_history(size=20)
print(history)
```

Sync:

```python
history = client.get_history(size=20)
print(history)
```

## Rent flow

Check availability:

Async:

```python
availability = await client.get_rent_availability(service="tg", country=6)
print(availability)
```

Sync:

```python
availability = client.get_rent_availability(service="tg", country=6)
print(availability)
```

Rent a number:

Async:

```python
rent_activation = await client.rent_number(service="tg", country=6, duration=2)
print(rent_activation.phone)
```

Sync:

```python
rent_activation = client.rent_number(service="tg", country=6, duration=2)
print(rent_activation.phone)
```

## Reference data APIs

```python
countries = await client.get_countries()
services = await client.get_services(country=6)
operators = await client.get_operators(country=6)
prices = await client.get_prices(service="tg", country=6)
top = await client.get_top_countries(service="tg", by_rank=True)
```

## Error handling

All library exceptions inherit from `HeroSMSError`.

```python
from herosms import HeroSMSError


try:
    ...
except HeroSMSError as exc:
    print(f"HeroSMS error: {exc}")
```

## Webhook payload parsing (`smsIncoming`)

Parse and validate incoming webhook payload with Pydantic model:

```python
from herosms import WebhookSMSIncoming

payload = {
    "activationId": "635468024",
    "service": "tg",
    "text": "Telegram code 123456",
    "code": "123456",
    "country": 6,
    "receivedAt": "2026-02-16T12:36:59+03:00",
}

event = WebhookSMSIncoming.model_validate(payload)
print(event.activation_id, event.code)
```

## API compatibility note

HeroSMS is compatible with SMS-Activate-style API actions. This library targets HeroSMS endpoints while preserving familiar SMS-Activate action semantics.
