sendly 3.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sendly/__init__.py +165 -0
- sendly/client.py +248 -0
- sendly/errors.py +169 -0
- sendly/resources/__init__.py +5 -0
- sendly/resources/account.py +264 -0
- sendly/resources/messages.py +1087 -0
- sendly/resources/webhooks.py +435 -0
- sendly/types.py +748 -0
- sendly/utils/__init__.py +26 -0
- sendly/utils/http.py +358 -0
- sendly/utils/validation.py +248 -0
- sendly/webhooks.py +245 -0
- sendly-3.8.1.dist-info/METADATA +589 -0
- sendly-3.8.1.dist-info/RECORD +15 -0
- sendly-3.8.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sendly
|
|
3
|
+
Version: 3.8.1
|
|
4
|
+
Summary: Official Sendly Python SDK for SMS messaging
|
|
5
|
+
Project-URL: Homepage, https://sendly.live
|
|
6
|
+
Project-URL: Documentation, https://sendly.live/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/sendly-live/sendly-python
|
|
8
|
+
Project-URL: Issues, https://github.com/sendly-live/sendly-python/issues
|
|
9
|
+
Author-email: Sendly <support@sendly.live>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api,messaging,notifications,sendly,sms,text
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Communications :: Telephony
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Requires-Dist: httpx>=0.25.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-httpx>=0.22.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# sendly
|
|
37
|
+
|
|
38
|
+
Official Python SDK for the [Sendly](https://sendly.live) SMS API.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# pip
|
|
44
|
+
pip install sendly
|
|
45
|
+
|
|
46
|
+
# poetry
|
|
47
|
+
poetry add sendly
|
|
48
|
+
|
|
49
|
+
# pipenv
|
|
50
|
+
pipenv install sendly
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Python 3.8+
|
|
56
|
+
- A Sendly API key ([get one here](https://sendly.live/dashboard))
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from sendly import Sendly
|
|
62
|
+
|
|
63
|
+
# Initialize with your API key
|
|
64
|
+
client = Sendly('sk_live_v1_your_api_key')
|
|
65
|
+
|
|
66
|
+
# Send an SMS
|
|
67
|
+
message = client.messages.send(
|
|
68
|
+
to='+15551234567',
|
|
69
|
+
text='Hello from Sendly!'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(f'Message sent: {message.id}')
|
|
73
|
+
print(f'Status: {message.status}')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Prerequisites for Live Messaging
|
|
77
|
+
|
|
78
|
+
Before sending live SMS messages, you need:
|
|
79
|
+
|
|
80
|
+
1. **Business Verification** - Complete verification in the [Sendly dashboard](https://sendly.live/dashboard)
|
|
81
|
+
- **International**: Instant approval (just provide Sender ID)
|
|
82
|
+
- **US/Canada**: Requires carrier approval (3-7 business days)
|
|
83
|
+
|
|
84
|
+
2. **Credits** - Add credits to your account
|
|
85
|
+
- Test keys (`sk_test_*`) work without credits (sandbox mode)
|
|
86
|
+
- Live keys (`sk_live_*`) require credits for each message
|
|
87
|
+
|
|
88
|
+
3. **Live API Key** - Generate after verification + credits
|
|
89
|
+
- Dashboard → API Keys → Create Live Key
|
|
90
|
+
|
|
91
|
+
### Test vs Live Keys
|
|
92
|
+
|
|
93
|
+
| Key Type | Prefix | Credits Required | Verification Required | Use Case |
|
|
94
|
+
|----------|--------|------------------|----------------------|----------|
|
|
95
|
+
| Test | `sk_test_v1_*` | No | No | Development, testing |
|
|
96
|
+
| Live | `sk_live_v1_*` | Yes | Yes | Production messaging |
|
|
97
|
+
|
|
98
|
+
> **Note**: You can start development immediately with a test key. Messages to sandbox test numbers are free and don't require verification.
|
|
99
|
+
|
|
100
|
+
## Features
|
|
101
|
+
|
|
102
|
+
- ✅ Full type hints (PEP 484)
|
|
103
|
+
- ✅ Sync and async clients
|
|
104
|
+
- ✅ Automatic retries with exponential backoff
|
|
105
|
+
- ✅ Rate limit handling
|
|
106
|
+
- ✅ Pydantic models for data validation
|
|
107
|
+
- ✅ Python 3.8+ support
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### Sending Messages
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sendly import Sendly
|
|
115
|
+
|
|
116
|
+
client = Sendly('sk_live_v1_xxx')
|
|
117
|
+
|
|
118
|
+
# Basic usage (marketing message - default)
|
|
119
|
+
message = client.messages.send(
|
|
120
|
+
to='+15551234567',
|
|
121
|
+
text='Check out our new features!'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Transactional message (bypasses quiet hours)
|
|
125
|
+
message = client.messages.send(
|
|
126
|
+
to='+15551234567',
|
|
127
|
+
text='Your verification code is: 123456',
|
|
128
|
+
message_type='transactional'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# With custom sender ID (international)
|
|
132
|
+
message = client.messages.send(
|
|
133
|
+
to='+447700900123',
|
|
134
|
+
text='Hello from MyApp!',
|
|
135
|
+
from_='MYAPP'
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Listing Messages
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Get recent messages (default limit: 50)
|
|
143
|
+
result = client.messages.list()
|
|
144
|
+
print(f'Found {result.count} messages')
|
|
145
|
+
|
|
146
|
+
# Get last 10 messages
|
|
147
|
+
result = client.messages.list(limit=10)
|
|
148
|
+
|
|
149
|
+
# Iterate through messages
|
|
150
|
+
for msg in result.data:
|
|
151
|
+
print(f'{msg.to}: {msg.status}')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Getting a Message
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
message = client.messages.get('msg_xxx')
|
|
158
|
+
|
|
159
|
+
print(f'Status: {message.status}')
|
|
160
|
+
print(f'Delivered: {message.delivered_at}')
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Scheduling Messages
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Schedule a message for future delivery
|
|
167
|
+
scheduled = client.messages.schedule(
|
|
168
|
+
to='+15551234567',
|
|
169
|
+
text='Your appointment is tomorrow!',
|
|
170
|
+
scheduled_at='2025-01-15T10:00:00Z'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
print(f'Scheduled: {scheduled.id}')
|
|
174
|
+
print(f'Will send at: {scheduled.scheduled_at}')
|
|
175
|
+
|
|
176
|
+
# List scheduled messages
|
|
177
|
+
result = client.messages.list_scheduled()
|
|
178
|
+
for msg in result.data:
|
|
179
|
+
print(f'{msg.id}: {msg.scheduled_at}')
|
|
180
|
+
|
|
181
|
+
# Get a specific scheduled message
|
|
182
|
+
msg = client.messages.get_scheduled('sched_xxx')
|
|
183
|
+
|
|
184
|
+
# Cancel a scheduled message (refunds credits)
|
|
185
|
+
result = client.messages.cancel_scheduled('sched_xxx')
|
|
186
|
+
print(f'Refunded: {result.credits_refunded} credits')
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Batch Messages
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Send multiple messages in one API call (up to 1000)
|
|
193
|
+
batch = client.messages.send_batch(
|
|
194
|
+
messages=[
|
|
195
|
+
{'to': '+15551234567', 'text': 'Hello User 1!'},
|
|
196
|
+
{'to': '+15559876543', 'text': 'Hello User 2!'},
|
|
197
|
+
{'to': '+15551112222', 'text': 'Hello User 3!'}
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
print(f'Batch ID: {batch.batch_id}')
|
|
202
|
+
print(f'Queued: {batch.queued}')
|
|
203
|
+
print(f'Failed: {batch.failed}')
|
|
204
|
+
print(f'Credits used: {batch.credits_used}')
|
|
205
|
+
|
|
206
|
+
# Get batch status
|
|
207
|
+
status = client.messages.get_batch('batch_xxx')
|
|
208
|
+
|
|
209
|
+
# List all batches
|
|
210
|
+
result = client.messages.list_batches()
|
|
211
|
+
|
|
212
|
+
# Preview batch (dry run) - validates without sending
|
|
213
|
+
preview = client.messages.preview_batch(
|
|
214
|
+
messages=[
|
|
215
|
+
{'to': '+15551234567', 'text': 'Hello User 1!'},
|
|
216
|
+
{'to': '+447700900123', 'text': 'Hello UK!'}
|
|
217
|
+
]
|
|
218
|
+
)
|
|
219
|
+
print(f'Total credits needed: {preview.total_credits}')
|
|
220
|
+
print(f'Valid: {preview.valid}, Invalid: {preview.invalid}')
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Rate Limit Information
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
# After any API call, check rate limit status
|
|
227
|
+
client.messages.send(to='+1555...', text='Hello!')
|
|
228
|
+
|
|
229
|
+
rate_limit = client.get_rate_limit_info()
|
|
230
|
+
if rate_limit:
|
|
231
|
+
print(f'{rate_limit.remaining}/{rate_limit.limit} requests remaining')
|
|
232
|
+
print(f'Resets in {rate_limit.reset} seconds')
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Async Client
|
|
236
|
+
|
|
237
|
+
For async/await support, use `AsyncSendly`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import asyncio
|
|
241
|
+
from sendly import AsyncSendly
|
|
242
|
+
|
|
243
|
+
async def main():
|
|
244
|
+
async with AsyncSendly('sk_live_v1_xxx') as client:
|
|
245
|
+
# Send a message
|
|
246
|
+
message = await client.messages.send(
|
|
247
|
+
to='+15551234567',
|
|
248
|
+
text='Hello from async!'
|
|
249
|
+
)
|
|
250
|
+
print(message.id)
|
|
251
|
+
|
|
252
|
+
# List messages
|
|
253
|
+
result = await client.messages.list(limit=10)
|
|
254
|
+
for msg in result.data:
|
|
255
|
+
print(f'{msg.to}: {msg.status}')
|
|
256
|
+
|
|
257
|
+
asyncio.run(main())
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Configuration
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from sendly import Sendly, SendlyConfig
|
|
264
|
+
|
|
265
|
+
# Using keyword arguments
|
|
266
|
+
client = Sendly(
|
|
267
|
+
api_key='sk_live_v1_xxx',
|
|
268
|
+
base_url='https://sendly.live/api/v1', # Optional
|
|
269
|
+
timeout=60.0, # Optional: seconds (default: 30)
|
|
270
|
+
max_retries=5 # Optional: (default: 3)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Using config object
|
|
274
|
+
config = SendlyConfig(
|
|
275
|
+
api_key='sk_live_v1_xxx',
|
|
276
|
+
timeout=60.0,
|
|
277
|
+
max_retries=5
|
|
278
|
+
)
|
|
279
|
+
client = Sendly(config=config)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Webhooks
|
|
283
|
+
|
|
284
|
+
Manage webhook endpoints to receive real-time delivery status updates.
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# Create a webhook endpoint
|
|
288
|
+
webhook = client.webhooks.create(
|
|
289
|
+
url='https://example.com/webhooks/sendly',
|
|
290
|
+
events=['message.delivered', 'message.failed']
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
print(f'Webhook ID: {webhook.id}')
|
|
294
|
+
print(f'Secret: {webhook.secret}') # Store this securely!
|
|
295
|
+
|
|
296
|
+
# List all webhooks
|
|
297
|
+
webhooks = client.webhooks.list()
|
|
298
|
+
|
|
299
|
+
# Get a specific webhook
|
|
300
|
+
wh = client.webhooks.get('whk_xxx')
|
|
301
|
+
|
|
302
|
+
# Update a webhook
|
|
303
|
+
client.webhooks.update('whk_xxx',
|
|
304
|
+
url='https://new-endpoint.example.com/webhook',
|
|
305
|
+
events=['message.delivered', 'message.failed', 'message.sent']
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Test a webhook (sends a test event)
|
|
309
|
+
result = client.webhooks.test('whk_xxx')
|
|
310
|
+
print(f'Test {"passed" if result.success else "failed"}')
|
|
311
|
+
|
|
312
|
+
# Rotate webhook secret
|
|
313
|
+
rotation = client.webhooks.rotate_secret('whk_xxx')
|
|
314
|
+
print(f'New secret: {rotation.secret}')
|
|
315
|
+
|
|
316
|
+
# View delivery history
|
|
317
|
+
deliveries = client.webhooks.get_deliveries('whk_xxx')
|
|
318
|
+
|
|
319
|
+
# Retry a failed delivery
|
|
320
|
+
client.webhooks.retry_delivery('whk_xxx', 'del_yyy')
|
|
321
|
+
|
|
322
|
+
# Delete a webhook
|
|
323
|
+
client.webhooks.delete('whk_xxx')
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Verifying Webhook Signatures
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from sendly import Webhooks
|
|
330
|
+
|
|
331
|
+
webhooks = Webhooks('your_webhook_secret')
|
|
332
|
+
|
|
333
|
+
# In your webhook handler (Flask example)
|
|
334
|
+
@app.route('/webhooks/sendly', methods=['POST'])
|
|
335
|
+
def handle_webhook():
|
|
336
|
+
signature = request.headers.get('X-Sendly-Signature')
|
|
337
|
+
payload = request.get_data(as_text=True)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
event = webhooks.verify_and_parse(payload, signature)
|
|
341
|
+
|
|
342
|
+
if event.type == 'message.delivered':
|
|
343
|
+
print(f'Message {event.data.id} delivered')
|
|
344
|
+
elif event.type == 'message.failed':
|
|
345
|
+
print(f'Message {event.data.id} failed: {event.data.error_code}')
|
|
346
|
+
|
|
347
|
+
return 'OK', 200
|
|
348
|
+
except Exception as e:
|
|
349
|
+
print(f'Invalid signature: {e}')
|
|
350
|
+
return 'Invalid signature', 400
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Account & Credits
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
# Get account information
|
|
357
|
+
account = client.account.get()
|
|
358
|
+
print(f'Email: {account.email}')
|
|
359
|
+
|
|
360
|
+
# Check credit balance
|
|
361
|
+
credits = client.account.get_credits()
|
|
362
|
+
print(f'Available: {credits.available_balance} credits')
|
|
363
|
+
print(f'Reserved (scheduled): {credits.reserved_balance} credits')
|
|
364
|
+
print(f'Total: {credits.balance} credits')
|
|
365
|
+
|
|
366
|
+
# View credit transaction history
|
|
367
|
+
result = client.account.get_credit_transactions()
|
|
368
|
+
for tx in result.data:
|
|
369
|
+
print(f'{tx.type}: {tx.amount} credits - {tx.description}')
|
|
370
|
+
|
|
371
|
+
# List API keys
|
|
372
|
+
result = client.account.list_api_keys()
|
|
373
|
+
for key in result.data:
|
|
374
|
+
print(f'{key.name}: {key.prefix}*** ({key.type})')
|
|
375
|
+
|
|
376
|
+
# Get API key usage stats
|
|
377
|
+
usage = client.account.get_api_key_usage('key_xxx')
|
|
378
|
+
print(f'Messages sent: {usage.messages_sent}')
|
|
379
|
+
print(f'Credits used: {usage.credits_used}')
|
|
380
|
+
|
|
381
|
+
# Create a new API key
|
|
382
|
+
new_key = client.account.create_api_key(
|
|
383
|
+
name='Production Key',
|
|
384
|
+
key_type='live',
|
|
385
|
+
scopes=['sms:send', 'sms:read']
|
|
386
|
+
)
|
|
387
|
+
print(f'New key: {new_key.key}') # Only shown once!
|
|
388
|
+
|
|
389
|
+
# Revoke an API key
|
|
390
|
+
client.account.revoke_api_key('key_xxx')
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Error Handling
|
|
394
|
+
|
|
395
|
+
The SDK provides typed exception classes:
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
from sendly import (
|
|
399
|
+
Sendly,
|
|
400
|
+
SendlyError,
|
|
401
|
+
AuthenticationError,
|
|
402
|
+
RateLimitError,
|
|
403
|
+
InsufficientCreditsError,
|
|
404
|
+
ValidationError,
|
|
405
|
+
NotFoundError,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
client = Sendly('sk_live_v1_xxx')
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
message = client.messages.send(
|
|
412
|
+
to='+15551234567',
|
|
413
|
+
text='Hello!'
|
|
414
|
+
)
|
|
415
|
+
except AuthenticationError as e:
|
|
416
|
+
print(f'Invalid API key: {e.message}')
|
|
417
|
+
except RateLimitError as e:
|
|
418
|
+
print(f'Rate limited. Retry after {e.retry_after} seconds')
|
|
419
|
+
except InsufficientCreditsError as e:
|
|
420
|
+
print(f'Need {e.credits_needed} credits, have {e.current_balance}')
|
|
421
|
+
except ValidationError as e:
|
|
422
|
+
print(f'Invalid request: {e.message}')
|
|
423
|
+
except NotFoundError as e:
|
|
424
|
+
print(f'Resource not found: {e.message}')
|
|
425
|
+
except SendlyError as e:
|
|
426
|
+
print(f'API error [{e.code}]: {e.message}')
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Testing (Sandbox Mode)
|
|
430
|
+
|
|
431
|
+
Use a test API key (`sk_test_v1_xxx`) for testing:
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from sendly import Sendly, SANDBOX_TEST_NUMBERS
|
|
435
|
+
|
|
436
|
+
client = Sendly('sk_test_v1_xxx')
|
|
437
|
+
|
|
438
|
+
# Check if in test mode
|
|
439
|
+
print(client.is_test_mode()) # True
|
|
440
|
+
|
|
441
|
+
# Use sandbox test numbers
|
|
442
|
+
message = client.messages.send(
|
|
443
|
+
to=SANDBOX_TEST_NUMBERS.SUCCESS, # +15005550000
|
|
444
|
+
text='Test message'
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Test error scenarios
|
|
448
|
+
message = client.messages.send(
|
|
449
|
+
to=SANDBOX_TEST_NUMBERS.INVALID, # +15005550001
|
|
450
|
+
text='This will fail'
|
|
451
|
+
)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Available Test Numbers
|
|
455
|
+
|
|
456
|
+
| Number | Behavior |
|
|
457
|
+
|--------|----------|
|
|
458
|
+
| `+15005550000` | Success (instant) |
|
|
459
|
+
| `+15005550001` | Fails: invalid_number |
|
|
460
|
+
| `+15005550002` | Fails: unroutable_destination |
|
|
461
|
+
| `+15005550003` | Fails: queue_full |
|
|
462
|
+
| `+15005550004` | Fails: rate_limit_exceeded |
|
|
463
|
+
| `+15005550006` | Fails: carrier_violation |
|
|
464
|
+
|
|
465
|
+
## Pricing Tiers
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
from sendly import CREDITS_PER_SMS, SUPPORTED_COUNTRIES, PricingTier
|
|
469
|
+
|
|
470
|
+
# Credits per SMS by tier
|
|
471
|
+
print(CREDITS_PER_SMS[PricingTier.DOMESTIC]) # 1 (US/Canada)
|
|
472
|
+
print(CREDITS_PER_SMS[PricingTier.TIER1]) # 8 (UK, Poland, etc.)
|
|
473
|
+
print(CREDITS_PER_SMS[PricingTier.TIER2]) # 12 (France, Japan, etc.)
|
|
474
|
+
print(CREDITS_PER_SMS[PricingTier.TIER3]) # 16 (Germany, Italy, etc.)
|
|
475
|
+
|
|
476
|
+
# Supported countries by tier
|
|
477
|
+
print(SUPPORTED_COUNTRIES[PricingTier.DOMESTIC]) # ['US', 'CA']
|
|
478
|
+
print(SUPPORTED_COUNTRIES[PricingTier.TIER1]) # ['GB', 'PL', ...]
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Utilities
|
|
482
|
+
|
|
483
|
+
The SDK exports validation utilities:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from sendly import (
|
|
487
|
+
validate_phone_number,
|
|
488
|
+
get_country_from_phone,
|
|
489
|
+
is_country_supported,
|
|
490
|
+
calculate_segments,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Validate phone number format
|
|
494
|
+
validate_phone_number('+15551234567') # OK
|
|
495
|
+
validate_phone_number('555-1234') # Raises ValidationError
|
|
496
|
+
|
|
497
|
+
# Get country from phone number
|
|
498
|
+
get_country_from_phone('+447700900123') # 'GB'
|
|
499
|
+
get_country_from_phone('+15551234567') # 'US'
|
|
500
|
+
|
|
501
|
+
# Check if country is supported
|
|
502
|
+
is_country_supported('GB') # True
|
|
503
|
+
is_country_supported('XX') # False
|
|
504
|
+
|
|
505
|
+
# Calculate SMS segments
|
|
506
|
+
calculate_segments('Hello!') # 1
|
|
507
|
+
calculate_segments('A' * 200) # 2
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Type Hints
|
|
511
|
+
|
|
512
|
+
The SDK is fully typed. Import types for your IDE:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from sendly import (
|
|
516
|
+
SendlyConfig,
|
|
517
|
+
SendMessageRequest,
|
|
518
|
+
Message,
|
|
519
|
+
MessageStatus,
|
|
520
|
+
ListMessagesOptions,
|
|
521
|
+
MessageListResponse,
|
|
522
|
+
RateLimitInfo,
|
|
523
|
+
PricingTier,
|
|
524
|
+
)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Context Manager
|
|
528
|
+
|
|
529
|
+
Both sync and async clients support context managers:
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
# Sync
|
|
533
|
+
with Sendly('sk_live_v1_xxx') as client:
|
|
534
|
+
message = client.messages.send(to='+1555...', text='Hello!')
|
|
535
|
+
|
|
536
|
+
# Async
|
|
537
|
+
async with AsyncSendly('sk_live_v1_xxx') as client:
|
|
538
|
+
message = await client.messages.send(to='+1555...', text='Hello!')
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## API Reference
|
|
542
|
+
|
|
543
|
+
### `Sendly` / `AsyncSendly`
|
|
544
|
+
|
|
545
|
+
#### Constructor
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
Sendly(
|
|
549
|
+
api_key: str,
|
|
550
|
+
base_url: str = 'https://sendly.live/api/v1',
|
|
551
|
+
timeout: float = 30.0,
|
|
552
|
+
max_retries: int = 3,
|
|
553
|
+
)
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
#### Properties
|
|
557
|
+
|
|
558
|
+
- `messages` - Messages resource
|
|
559
|
+
- `base_url` - Configured base URL
|
|
560
|
+
|
|
561
|
+
#### Methods
|
|
562
|
+
|
|
563
|
+
- `is_test_mode()` - Returns `True` if using a test API key
|
|
564
|
+
- `get_rate_limit_info()` - Returns current rate limit info
|
|
565
|
+
- `close()` - Close the HTTP client
|
|
566
|
+
|
|
567
|
+
### `client.messages`
|
|
568
|
+
|
|
569
|
+
#### `send(to, text, from_=None) -> Message`
|
|
570
|
+
|
|
571
|
+
Send an SMS message.
|
|
572
|
+
|
|
573
|
+
#### `list(limit=None) -> MessageListResponse`
|
|
574
|
+
|
|
575
|
+
List sent messages.
|
|
576
|
+
|
|
577
|
+
#### `get(id) -> Message`
|
|
578
|
+
|
|
579
|
+
Get a specific message by ID.
|
|
580
|
+
|
|
581
|
+
## Support
|
|
582
|
+
|
|
583
|
+
- 📚 [Documentation](https://sendly.live/docs)
|
|
584
|
+
- 💬 [Discord](https://discord.gg/sendly)
|
|
585
|
+
- 📧 [support@sendly.live](mailto:support@sendly.live)
|
|
586
|
+
|
|
587
|
+
## License
|
|
588
|
+
|
|
589
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
sendly/__init__.py,sha256=uvAR50t7DF844f-WREh8ixSkyxA-Fydhy83zO6ubfTU,3408
|
|
2
|
+
sendly/client.py,sha256=kTj4Kqw9BAtZnOixWvKgcwxmVeSXwORVKfpanWhGBc0,7397
|
|
3
|
+
sendly/errors.py,sha256=9PSuNTVe-t4NRa968Y72kw70cTQRd3_uUadHNO8qPxw,5056
|
|
4
|
+
sendly/types.py,sha256=Ycga8KPJBwHzAvC3Z-xxxJ76_Pnfmo9YsFITBsYse3I,24750
|
|
5
|
+
sendly/webhooks.py,sha256=L0DeuwcThde-ZZV6HDSyqddZ847s-nNASYkg4ubVH1Y,7213
|
|
6
|
+
sendly/resources/__init__.py,sha256=HQR8ygra2R1cbjE9PwvFts57IYWRxkA0urnZ9_0G6M0,147
|
|
7
|
+
sendly/resources/account.py,sha256=Be2SJ5n-RlGwwv0mAuyzpC-Krp7dbDyEQ3gINLN2vT8,7967
|
|
8
|
+
sendly/resources/messages.py,sha256=9c3rNvZYVrwh5UMoGHJWjNN6Idl9eR7hMb7s6Xuw_gc,31917
|
|
9
|
+
sendly/resources/webhooks.py,sha256=ulFq6ON2RWD-dOgdM0J7YGNei_Zo7UXkopQgxMmKeFA,15031
|
|
10
|
+
sendly/utils/__init__.py,sha256=qj065Z-a4jPHBCd2CZSXYQBcTl15jBn8pQaLUmHMeCQ,576
|
|
11
|
+
sendly/utils/http.py,sha256=bAwPTY2K9ohsLG8R9-DGYMOcTjWcuUP78nRKXxUfZow,11627
|
|
12
|
+
sendly/utils/validation.py,sha256=DJly096JsoYJKxbavOFjepD1SXF5WqIDskQ30QmD0rw,5888
|
|
13
|
+
sendly-3.8.1.dist-info/METADATA,sha256=U2muoRD-6lJWkSoM6erfvfZZ1pOj7PHxWot-HN2U-cY,14780
|
|
14
|
+
sendly-3.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
sendly-3.8.1.dist-info/RECORD,,
|