Metadata-Version: 2.4
Name: sendly
Version: 1.0.5
Summary: Official Sendly Python SDK for SMS messaging
Project-URL: Homepage, https://sendly.live
Project-URL: Documentation, https://sendly.live/docs
Project-URL: Repository, https://github.com/sendly-live/sendly-python
Project-URL: Issues, https://github.com/sendly-live/sendly-python/issues
Author-email: Sendly <support@sendly.live>
License-Expression: MIT
Keywords: api,messaging,notifications,sendly,sms,text
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Telephony
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.22.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# sendly

Official Python SDK for the [Sendly](https://sendly.live) SMS API.

## Installation

```bash
# pip
pip install sendly

# poetry
poetry add sendly

# pipenv
pipenv install sendly
```

## Requirements

- Python 3.8+
- A Sendly API key ([get one here](https://sendly.live/dashboard))

## Quick Start

```python
from sendly import Sendly

# Initialize with your API key
client = Sendly('sk_live_v1_your_api_key')

# Send an SMS
message = client.messages.send(
    to='+15551234567',
    text='Hello from Sendly!'
)

print(f'Message sent: {message.id}')
print(f'Status: {message.status}')
```

## Prerequisites for Live Messaging

Before sending live SMS messages, you need:

1. **Business Verification** - Complete verification in the [Sendly dashboard](https://sendly.live/dashboard)
   - **International**: Instant approval (just provide Sender ID)
   - **US/Canada**: Requires carrier approval (3-7 business days)

2. **Credits** - Add credits to your account
   - Test keys (`sk_test_*`) work without credits (sandbox mode)
   - Live keys (`sk_live_*`) require credits for each message

3. **Live API Key** - Generate after verification + credits
   - Dashboard → API Keys → Create Live Key

### Test vs Live Keys

| Key Type | Prefix | Credits Required | Verification Required | Use Case |
|----------|--------|------------------|----------------------|----------|
| Test | `sk_test_v1_*` | No | No | Development, testing |
| Live | `sk_live_v1_*` | Yes | Yes | Production messaging |

> **Note**: You can start development immediately with a test key. Messages to sandbox test numbers are free and don't require verification.

## Features

- ✅ Full type hints (PEP 484)
- ✅ Sync and async clients
- ✅ Automatic retries with exponential backoff
- ✅ Rate limit handling
- ✅ Pydantic models for data validation
- ✅ Python 3.8+ support

## Usage

### Sending Messages

```python
from sendly import Sendly

client = Sendly('sk_live_v1_xxx')

# Basic usage
message = client.messages.send(
    to='+15551234567',
    text='Your verification code is: 123456'
)

# With custom sender ID (international)
message = client.messages.send(
    to='+447700900123',
    text='Hello from MyApp!',
    from_='MYAPP'
)
```

### Listing Messages

```python
# Get recent messages (default limit: 50)
result = client.messages.list()
print(f'Found {result.count} messages')

# Get last 10 messages
result = client.messages.list(limit=10)

# Iterate through messages
for msg in result.data:
    print(f'{msg.to}: {msg.status}')
```

### Getting a Message

```python
message = client.messages.get('msg_xxx')

print(f'Status: {message.status}')
print(f'Delivered: {message.delivered_at}')
```

### Rate Limit Information

```python
# After any API call, check rate limit status
client.messages.send(to='+1555...', text='Hello!')

rate_limit = client.get_rate_limit_info()
if rate_limit:
    print(f'{rate_limit.remaining}/{rate_limit.limit} requests remaining')
    print(f'Resets in {rate_limit.reset} seconds')
```

## Async Client

For async/await support, use `AsyncSendly`:

```python
import asyncio
from sendly import AsyncSendly

async def main():
    async with AsyncSendly('sk_live_v1_xxx') as client:
        # Send a message
        message = await client.messages.send(
            to='+15551234567',
            text='Hello from async!'
        )
        print(message.id)

        # List messages
        result = await client.messages.list(limit=10)
        for msg in result.data:
            print(f'{msg.to}: {msg.status}')

asyncio.run(main())
```

## Configuration

```python
from sendly import Sendly, SendlyConfig

# Using keyword arguments
client = Sendly(
    api_key='sk_live_v1_xxx',
    base_url='https://sendly.live/api',  # Optional
    timeout=60.0,  # Optional: seconds (default: 30)
    max_retries=5  # Optional: (default: 3)
)

# Using config object
config = SendlyConfig(
    api_key='sk_live_v1_xxx',
    timeout=60.0,
    max_retries=5
)
client = Sendly(config=config)
```

## Error Handling

The SDK provides typed exception classes:

```python
from sendly import (
    Sendly,
    SendlyError,
    AuthenticationError,
    RateLimitError,
    InsufficientCreditsError,
    ValidationError,
    NotFoundError,
)

client = Sendly('sk_live_v1_xxx')

try:
    message = client.messages.send(
        to='+15551234567',
        text='Hello!'
    )
except AuthenticationError as e:
    print(f'Invalid API key: {e.message}')
except RateLimitError as e:
    print(f'Rate limited. Retry after {e.retry_after} seconds')
except InsufficientCreditsError as e:
    print(f'Need {e.credits_needed} credits, have {e.current_balance}')
except ValidationError as e:
    print(f'Invalid request: {e.message}')
except NotFoundError as e:
    print(f'Resource not found: {e.message}')
except SendlyError as e:
    print(f'API error [{e.code}]: {e.message}')
```

## Testing (Sandbox Mode)

Use a test API key (`sk_test_v1_xxx`) for testing:

```python
from sendly import Sendly, SANDBOX_TEST_NUMBERS

client = Sendly('sk_test_v1_xxx')

# Check if in test mode
print(client.is_test_mode())  # True

# Use sandbox test numbers
message = client.messages.send(
    to=SANDBOX_TEST_NUMBERS.SUCCESS,  # +15550001234
    text='Test message'
)

# Test error scenarios
message = client.messages.send(
    to=SANDBOX_TEST_NUMBERS.INVALID,  # +15550001001
    text='This will fail'
)
```

### Available Test Numbers

| Number | Behavior |
|--------|----------|
| `+15550001234` | Instant success |
| `+15550001010` | Success after 10s delay |
| `+15550001001` | Fails: invalid_number |
| `+15550001002` | Fails: carrier_rejected (2s delay) |
| `+15550001003` | Fails: rate_limit_exceeded |

## Pricing Tiers

```python
from sendly import CREDITS_PER_SMS, SUPPORTED_COUNTRIES, PricingTier

# Credits per SMS by tier
print(CREDITS_PER_SMS[PricingTier.DOMESTIC])  # 1 (US/Canada)
print(CREDITS_PER_SMS[PricingTier.TIER1])     # 8 (UK, Poland, etc.)
print(CREDITS_PER_SMS[PricingTier.TIER2])     # 12 (France, Japan, etc.)
print(CREDITS_PER_SMS[PricingTier.TIER3])     # 16 (Germany, Italy, etc.)

# Supported countries by tier
print(SUPPORTED_COUNTRIES[PricingTier.DOMESTIC])  # ['US', 'CA']
print(SUPPORTED_COUNTRIES[PricingTier.TIER1])     # ['GB', 'PL', ...]
```

## Utilities

The SDK exports validation utilities:

```python
from sendly import (
    validate_phone_number,
    get_country_from_phone,
    is_country_supported,
    calculate_segments,
)

# Validate phone number format
validate_phone_number('+15551234567')  # OK
validate_phone_number('555-1234')  # Raises ValidationError

# Get country from phone number
get_country_from_phone('+447700900123')  # 'GB'
get_country_from_phone('+15551234567')   # 'US'

# Check if country is supported
is_country_supported('GB')  # True
is_country_supported('XX')  # False

# Calculate SMS segments
calculate_segments('Hello!')  # 1
calculate_segments('A' * 200)  # 2
```

## Type Hints

The SDK is fully typed. Import types for your IDE:

```python
from sendly import (
    SendlyConfig,
    SendMessageRequest,
    Message,
    MessageStatus,
    ListMessagesOptions,
    MessageListResponse,
    RateLimitInfo,
    PricingTier,
)
```

## Context Manager

Both sync and async clients support context managers:

```python
# Sync
with Sendly('sk_live_v1_xxx') as client:
    message = client.messages.send(to='+1555...', text='Hello!')

# Async
async with AsyncSendly('sk_live_v1_xxx') as client:
    message = await client.messages.send(to='+1555...', text='Hello!')
```

## API Reference

### `Sendly` / `AsyncSendly`

#### Constructor

```python
Sendly(
    api_key: str,
    base_url: str = 'https://sendly.live/api',
    timeout: float = 30.0,
    max_retries: int = 3,
)
```

#### Properties

- `messages` - Messages resource
- `base_url` - Configured base URL

#### Methods

- `is_test_mode()` - Returns `True` if using a test API key
- `get_rate_limit_info()` - Returns current rate limit info
- `close()` - Close the HTTP client

### `client.messages`

#### `send(to, text, from_=None) -> Message`

Send an SMS message.

#### `list(limit=None) -> MessageListResponse`

List sent messages.

#### `get(id) -> Message`

Get a specific message by ID.

## Support

- 📚 [Documentation](https://sendly.live/docs)
- 💬 [Discord](https://discord.gg/sendly)
- 📧 [support@sendly.live](mailto:support@sendly.live)

## License

MIT
