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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any