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
sendly/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sendly Python SDK
|
|
3
|
+
|
|
4
|
+
Official SDK for the Sendly SMS API.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from sendly import Sendly
|
|
8
|
+
>>>
|
|
9
|
+
>>> client = Sendly('sk_live_v1_your_api_key')
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Send an SMS
|
|
12
|
+
>>> message = client.messages.send(
|
|
13
|
+
... to='+15551234567',
|
|
14
|
+
... text='Hello from Sendly!'
|
|
15
|
+
... )
|
|
16
|
+
>>> print(f'Message sent: {message.id}')
|
|
17
|
+
|
|
18
|
+
Async Example:
|
|
19
|
+
>>> import asyncio
|
|
20
|
+
>>> from sendly import AsyncSendly
|
|
21
|
+
>>>
|
|
22
|
+
>>> async def main():
|
|
23
|
+
... async with AsyncSendly('sk_live_v1_xxx') as client:
|
|
24
|
+
... message = await client.messages.send(
|
|
25
|
+
... to='+15551234567',
|
|
26
|
+
... text='Hello!'
|
|
27
|
+
... )
|
|
28
|
+
>>>
|
|
29
|
+
>>> asyncio.run(main())
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.5"
|
|
33
|
+
|
|
34
|
+
# Main clients
|
|
35
|
+
from .client import AsyncSendly, Sendly
|
|
36
|
+
|
|
37
|
+
# Errors
|
|
38
|
+
from .errors import (
|
|
39
|
+
AuthenticationError,
|
|
40
|
+
InsufficientCreditsError,
|
|
41
|
+
NetworkError,
|
|
42
|
+
NotFoundError,
|
|
43
|
+
RateLimitError,
|
|
44
|
+
SendlyError,
|
|
45
|
+
TimeoutError,
|
|
46
|
+
ValidationError,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Types
|
|
50
|
+
from .types import (
|
|
51
|
+
ALL_SUPPORTED_COUNTRIES,
|
|
52
|
+
# Constants
|
|
53
|
+
CREDITS_PER_SMS,
|
|
54
|
+
SANDBOX_TEST_NUMBERS,
|
|
55
|
+
SUPPORTED_COUNTRIES,
|
|
56
|
+
# Account types
|
|
57
|
+
Account,
|
|
58
|
+
ApiKey,
|
|
59
|
+
CircuitState,
|
|
60
|
+
CreateWebhookOptions,
|
|
61
|
+
Credits,
|
|
62
|
+
CreditTransaction,
|
|
63
|
+
DeliveryStatus,
|
|
64
|
+
ListMessagesOptions,
|
|
65
|
+
Message,
|
|
66
|
+
MessageListResponse,
|
|
67
|
+
MessageStatus,
|
|
68
|
+
PricingTier,
|
|
69
|
+
RateLimitInfo,
|
|
70
|
+
SandboxTestNumbers,
|
|
71
|
+
SenderType,
|
|
72
|
+
SendlyConfig,
|
|
73
|
+
SendMessageRequest,
|
|
74
|
+
TransactionType,
|
|
75
|
+
UpdateWebhookOptions,
|
|
76
|
+
# Webhook types
|
|
77
|
+
Webhook,
|
|
78
|
+
WebhookCreatedResponse,
|
|
79
|
+
WebhookDelivery,
|
|
80
|
+
WebhookSecretRotation,
|
|
81
|
+
WebhookTestResult,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Utilities (for advanced usage)
|
|
85
|
+
from .utils.validation import (
|
|
86
|
+
calculate_segments,
|
|
87
|
+
get_country_from_phone,
|
|
88
|
+
is_country_supported,
|
|
89
|
+
validate_message_text,
|
|
90
|
+
validate_phone_number,
|
|
91
|
+
validate_sender_id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Webhooks
|
|
95
|
+
from .webhooks import (
|
|
96
|
+
WebhookEvent,
|
|
97
|
+
WebhookEventType,
|
|
98
|
+
WebhookMessageData,
|
|
99
|
+
WebhookMessageStatus,
|
|
100
|
+
Webhooks,
|
|
101
|
+
WebhookSignatureError,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
# Version
|
|
106
|
+
"__version__",
|
|
107
|
+
# Clients
|
|
108
|
+
"Sendly",
|
|
109
|
+
"AsyncSendly",
|
|
110
|
+
# Types
|
|
111
|
+
"SendlyConfig",
|
|
112
|
+
"SendMessageRequest",
|
|
113
|
+
"Message",
|
|
114
|
+
"MessageStatus",
|
|
115
|
+
"SenderType",
|
|
116
|
+
"ListMessagesOptions",
|
|
117
|
+
"MessageListResponse",
|
|
118
|
+
"RateLimitInfo",
|
|
119
|
+
"PricingTier",
|
|
120
|
+
# Webhook types
|
|
121
|
+
"Webhook",
|
|
122
|
+
"WebhookCreatedResponse",
|
|
123
|
+
"CreateWebhookOptions",
|
|
124
|
+
"UpdateWebhookOptions",
|
|
125
|
+
"WebhookDelivery",
|
|
126
|
+
"WebhookTestResult",
|
|
127
|
+
"WebhookSecretRotation",
|
|
128
|
+
"CircuitState",
|
|
129
|
+
"DeliveryStatus",
|
|
130
|
+
# Account types
|
|
131
|
+
"Account",
|
|
132
|
+
"Credits",
|
|
133
|
+
"CreditTransaction",
|
|
134
|
+
"TransactionType",
|
|
135
|
+
"ApiKey",
|
|
136
|
+
# Constants
|
|
137
|
+
"CREDITS_PER_SMS",
|
|
138
|
+
"SUPPORTED_COUNTRIES",
|
|
139
|
+
"ALL_SUPPORTED_COUNTRIES",
|
|
140
|
+
"SANDBOX_TEST_NUMBERS",
|
|
141
|
+
"SandboxTestNumbers",
|
|
142
|
+
# Errors
|
|
143
|
+
"SendlyError",
|
|
144
|
+
"AuthenticationError",
|
|
145
|
+
"RateLimitError",
|
|
146
|
+
"InsufficientCreditsError",
|
|
147
|
+
"ValidationError",
|
|
148
|
+
"NotFoundError",
|
|
149
|
+
"NetworkError",
|
|
150
|
+
"TimeoutError",
|
|
151
|
+
# Utilities
|
|
152
|
+
"validate_phone_number",
|
|
153
|
+
"validate_message_text",
|
|
154
|
+
"validate_sender_id",
|
|
155
|
+
"get_country_from_phone",
|
|
156
|
+
"is_country_supported",
|
|
157
|
+
"calculate_segments",
|
|
158
|
+
# Webhooks
|
|
159
|
+
"Webhooks",
|
|
160
|
+
"WebhookSignatureError",
|
|
161
|
+
"WebhookEvent",
|
|
162
|
+
"WebhookEventType",
|
|
163
|
+
"WebhookMessageData",
|
|
164
|
+
"WebhookMessageStatus",
|
|
165
|
+
]
|
sendly/client.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sendly Client
|
|
3
|
+
|
|
4
|
+
Main entry point for the Sendly SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .resources.account import AccountResource, AsyncAccountResource
|
|
10
|
+
from .resources.messages import AsyncMessagesResource, MessagesResource
|
|
11
|
+
from .resources.webhooks import AsyncWebhooksResource, WebhooksResource
|
|
12
|
+
from .types import RateLimitInfo, SendlyConfig
|
|
13
|
+
from .utils.http import AsyncHttpClient, HttpClient
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://sendly.live/api/v1"
|
|
16
|
+
DEFAULT_TIMEOUT = 30.0
|
|
17
|
+
DEFAULT_MAX_RETRIES = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Sendly:
|
|
21
|
+
"""
|
|
22
|
+
Sendly API Client (synchronous)
|
|
23
|
+
|
|
24
|
+
The main entry point for interacting with the Sendly SMS API.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> from sendly import Sendly
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Initialize with API key
|
|
30
|
+
>>> client = Sendly('sk_live_v1_your_api_key')
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Send an SMS
|
|
33
|
+
>>> message = client.messages.send(
|
|
34
|
+
... to='+15551234567',
|
|
35
|
+
... text='Hello from Sendly!'
|
|
36
|
+
... )
|
|
37
|
+
>>> print(message.id)
|
|
38
|
+
|
|
39
|
+
Example with configuration:
|
|
40
|
+
>>> client = Sendly(
|
|
41
|
+
... api_key='sk_live_v1_your_api_key',
|
|
42
|
+
... timeout=60.0,
|
|
43
|
+
... max_retries=5
|
|
44
|
+
... )
|
|
45
|
+
|
|
46
|
+
Example with context manager:
|
|
47
|
+
>>> with Sendly('sk_live_v1_xxx') as client:
|
|
48
|
+
... message = client.messages.send(to='+1555...', text='Hello!')
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_key: Optional[str] = None,
|
|
54
|
+
*,
|
|
55
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
56
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
57
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
58
|
+
config: Optional[SendlyConfig] = None,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Create a new Sendly client
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
api_key: Your Sendly API key (sk_test_v1_xxx or sk_live_v1_xxx)
|
|
65
|
+
base_url: Base URL for the API (default: https://sendly.live/api/v1)
|
|
66
|
+
timeout: Request timeout in seconds (default: 30)
|
|
67
|
+
max_retries: Maximum retry attempts (default: 3)
|
|
68
|
+
config: Alternative configuration object
|
|
69
|
+
"""
|
|
70
|
+
# Handle configuration
|
|
71
|
+
if config is not None:
|
|
72
|
+
api_key = config.api_key
|
|
73
|
+
base_url = config.base_url
|
|
74
|
+
timeout = config.timeout
|
|
75
|
+
max_retries = config.max_retries
|
|
76
|
+
elif api_key is None:
|
|
77
|
+
raise ValueError("api_key is required")
|
|
78
|
+
|
|
79
|
+
self._api_key = api_key
|
|
80
|
+
self._base_url = base_url
|
|
81
|
+
self._timeout = timeout
|
|
82
|
+
self._max_retries = max_retries
|
|
83
|
+
|
|
84
|
+
# Initialize HTTP client
|
|
85
|
+
self._http = HttpClient(
|
|
86
|
+
api_key=api_key,
|
|
87
|
+
base_url=base_url,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
max_retries=max_retries,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Initialize resources
|
|
93
|
+
self.messages = MessagesResource(self._http)
|
|
94
|
+
self.webhooks = WebhooksResource(self._http)
|
|
95
|
+
self.account = AccountResource(self._http)
|
|
96
|
+
|
|
97
|
+
def __enter__(self) -> "Sendly":
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, *args: Any) -> None:
|
|
101
|
+
self.close()
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
"""Close the HTTP client and release resources"""
|
|
105
|
+
self._http.close()
|
|
106
|
+
|
|
107
|
+
def is_test_mode(self) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if the client is using a test API key
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if using a test key (sk_test_v1_xxx)
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> if client.is_test_mode():
|
|
116
|
+
... print('Running in test mode')
|
|
117
|
+
"""
|
|
118
|
+
return self._http.is_test_mode()
|
|
119
|
+
|
|
120
|
+
def get_rate_limit_info(self) -> Optional[RateLimitInfo]:
|
|
121
|
+
"""
|
|
122
|
+
Get current rate limit information
|
|
123
|
+
|
|
124
|
+
Returns the rate limit info from the most recent API request.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Rate limit info or None if no requests have been made
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> client.messages.send(to='+1555...', text='Hello!')
|
|
131
|
+
>>> rate_limit = client.get_rate_limit_info()
|
|
132
|
+
>>> if rate_limit:
|
|
133
|
+
... print(f'{rate_limit.remaining}/{rate_limit.limit} remaining')
|
|
134
|
+
"""
|
|
135
|
+
return self._http.get_rate_limit_info()
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def base_url(self) -> str:
|
|
139
|
+
"""Get the configured base URL"""
|
|
140
|
+
return self._base_url
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class AsyncSendly:
|
|
144
|
+
"""
|
|
145
|
+
Sendly API Client (asynchronous)
|
|
146
|
+
|
|
147
|
+
Async version of the Sendly client for use with asyncio.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> import asyncio
|
|
151
|
+
>>> from sendly import AsyncSendly
|
|
152
|
+
>>>
|
|
153
|
+
>>> async def main():
|
|
154
|
+
... async with AsyncSendly('sk_live_v1_xxx') as client:
|
|
155
|
+
... message = await client.messages.send(
|
|
156
|
+
... to='+15551234567',
|
|
157
|
+
... text='Hello from Sendly!'
|
|
158
|
+
... )
|
|
159
|
+
... print(message.id)
|
|
160
|
+
>>>
|
|
161
|
+
>>> asyncio.run(main())
|
|
162
|
+
|
|
163
|
+
Example without context manager:
|
|
164
|
+
>>> client = AsyncSendly('sk_live_v1_xxx')
|
|
165
|
+
>>> try:
|
|
166
|
+
... message = await client.messages.send(to='+1555...', text='Hello!')
|
|
167
|
+
... finally:
|
|
168
|
+
... await client.close()
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
api_key: Optional[str] = None,
|
|
174
|
+
*,
|
|
175
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
176
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
177
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
178
|
+
config: Optional[SendlyConfig] = None,
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Create a new async Sendly client
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
api_key: Your Sendly API key (sk_test_v1_xxx or sk_live_v1_xxx)
|
|
185
|
+
base_url: Base URL for the API (default: https://sendly.live/api/v1)
|
|
186
|
+
timeout: Request timeout in seconds (default: 30)
|
|
187
|
+
max_retries: Maximum retry attempts (default: 3)
|
|
188
|
+
config: Alternative configuration object
|
|
189
|
+
"""
|
|
190
|
+
# Handle configuration
|
|
191
|
+
if config is not None:
|
|
192
|
+
api_key = config.api_key
|
|
193
|
+
base_url = config.base_url
|
|
194
|
+
timeout = config.timeout
|
|
195
|
+
max_retries = config.max_retries
|
|
196
|
+
elif api_key is None:
|
|
197
|
+
raise ValueError("api_key is required")
|
|
198
|
+
|
|
199
|
+
self._api_key = api_key
|
|
200
|
+
self._base_url = base_url
|
|
201
|
+
self._timeout = timeout
|
|
202
|
+
self._max_retries = max_retries
|
|
203
|
+
|
|
204
|
+
# Initialize HTTP client
|
|
205
|
+
self._http = AsyncHttpClient(
|
|
206
|
+
api_key=api_key,
|
|
207
|
+
base_url=base_url,
|
|
208
|
+
timeout=timeout,
|
|
209
|
+
max_retries=max_retries,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Initialize resources
|
|
213
|
+
self.messages = AsyncMessagesResource(self._http)
|
|
214
|
+
self.webhooks = AsyncWebhooksResource(self._http)
|
|
215
|
+
self.account = AsyncAccountResource(self._http)
|
|
216
|
+
|
|
217
|
+
async def __aenter__(self) -> "AsyncSendly":
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
221
|
+
await self.close()
|
|
222
|
+
|
|
223
|
+
async def close(self) -> None:
|
|
224
|
+
"""Close the HTTP client and release resources"""
|
|
225
|
+
await self._http.close()
|
|
226
|
+
|
|
227
|
+
def is_test_mode(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Check if the client is using a test API key
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if using a test key (sk_test_v1_xxx)
|
|
233
|
+
"""
|
|
234
|
+
return self._http.is_test_mode()
|
|
235
|
+
|
|
236
|
+
def get_rate_limit_info(self) -> Optional[RateLimitInfo]:
|
|
237
|
+
"""
|
|
238
|
+
Get current rate limit information
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Rate limit info or None if no requests have been made
|
|
242
|
+
"""
|
|
243
|
+
return self._http.get_rate_limit_info()
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def base_url(self) -> str:
|
|
247
|
+
"""Get the configured base URL"""
|
|
248
|
+
return self._base_url
|
sendly/errors.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sendly SDK Error Classes
|
|
3
|
+
|
|
4
|
+
Custom exceptions for different error scenarios.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from .types import ApiErrorResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SendlyError(Exception):
|
|
13
|
+
"""Base error class for all Sendly SDK errors"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
code: str = "internal_error",
|
|
19
|
+
status_code: Optional[int] = None,
|
|
20
|
+
response: Optional[ApiErrorResponse] = None,
|
|
21
|
+
):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.code = code
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.response = response
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
if self.status_code:
|
|
30
|
+
return f"[{self.code}] ({self.status_code}) {self.message}"
|
|
31
|
+
return f"[{self.code}] {self.message}"
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_response(cls, status_code: int, response_data: Dict[str, Any]) -> "SendlyError":
|
|
38
|
+
"""Create a SendlyError from an API response"""
|
|
39
|
+
try:
|
|
40
|
+
error_response = ApiErrorResponse(**response_data)
|
|
41
|
+
except Exception:
|
|
42
|
+
error_response = ApiErrorResponse(
|
|
43
|
+
error="internal_error",
|
|
44
|
+
message=str(response_data),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
code = error_response.error
|
|
48
|
+
message = error_response.message
|
|
49
|
+
|
|
50
|
+
# Return specific error types based on error code
|
|
51
|
+
if code in (
|
|
52
|
+
"unauthorized",
|
|
53
|
+
"invalid_auth_format",
|
|
54
|
+
"invalid_key_format",
|
|
55
|
+
"invalid_api_key",
|
|
56
|
+
"api_key_required",
|
|
57
|
+
"key_revoked",
|
|
58
|
+
"key_expired",
|
|
59
|
+
"insufficient_permissions",
|
|
60
|
+
):
|
|
61
|
+
return AuthenticationError(message, code, status_code, error_response)
|
|
62
|
+
|
|
63
|
+
if code == "rate_limit_exceeded":
|
|
64
|
+
return RateLimitError(
|
|
65
|
+
message,
|
|
66
|
+
retry_after=error_response.retry_after or 60,
|
|
67
|
+
status_code=status_code,
|
|
68
|
+
response=error_response,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if code == "insufficient_credits":
|
|
72
|
+
return InsufficientCreditsError(
|
|
73
|
+
message,
|
|
74
|
+
credits_needed=error_response.credits_needed or 0,
|
|
75
|
+
current_balance=error_response.current_balance or 0,
|
|
76
|
+
status_code=status_code,
|
|
77
|
+
response=error_response,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if code in ("invalid_request", "unsupported_destination"):
|
|
81
|
+
return ValidationError(message, code, status_code, error_response)
|
|
82
|
+
|
|
83
|
+
if code == "not_found":
|
|
84
|
+
return NotFoundError(message, status_code, error_response)
|
|
85
|
+
|
|
86
|
+
return cls(message, code, status_code, error_response)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AuthenticationError(SendlyError):
|
|
90
|
+
"""Thrown when authentication fails"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
message: str,
|
|
95
|
+
code: str = "unauthorized",
|
|
96
|
+
status_code: Optional[int] = None,
|
|
97
|
+
response: Optional[ApiErrorResponse] = None,
|
|
98
|
+
):
|
|
99
|
+
super().__init__(message, code, status_code, response)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RateLimitError(SendlyError):
|
|
103
|
+
"""Thrown when rate limit is exceeded"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
message: str,
|
|
108
|
+
retry_after: int,
|
|
109
|
+
status_code: Optional[int] = None,
|
|
110
|
+
response: Optional[ApiErrorResponse] = None,
|
|
111
|
+
):
|
|
112
|
+
super().__init__(message, "rate_limit_exceeded", status_code, response)
|
|
113
|
+
self.retry_after = retry_after
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class InsufficientCreditsError(SendlyError):
|
|
117
|
+
"""Thrown when credit balance is insufficient"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
message: str,
|
|
122
|
+
credits_needed: int,
|
|
123
|
+
current_balance: int,
|
|
124
|
+
status_code: Optional[int] = None,
|
|
125
|
+
response: Optional[ApiErrorResponse] = None,
|
|
126
|
+
):
|
|
127
|
+
super().__init__(message, "insufficient_credits", status_code, response)
|
|
128
|
+
self.credits_needed = credits_needed
|
|
129
|
+
self.current_balance = current_balance
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ValidationError(SendlyError):
|
|
133
|
+
"""Thrown when request validation fails"""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
message: str,
|
|
138
|
+
code: str = "invalid_request",
|
|
139
|
+
status_code: Optional[int] = None,
|
|
140
|
+
response: Optional[ApiErrorResponse] = None,
|
|
141
|
+
):
|
|
142
|
+
super().__init__(message, code, status_code, response)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class NotFoundError(SendlyError):
|
|
146
|
+
"""Thrown when a resource is not found"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
message: str,
|
|
151
|
+
status_code: Optional[int] = None,
|
|
152
|
+
response: Optional[ApiErrorResponse] = None,
|
|
153
|
+
):
|
|
154
|
+
super().__init__(message, "not_found", status_code, response)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NetworkError(SendlyError):
|
|
158
|
+
"""Thrown when a network or connection error occurs"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, message: str, cause: Optional[Exception] = None):
|
|
161
|
+
super().__init__(message, "internal_error")
|
|
162
|
+
self.cause = cause
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TimeoutError(SendlyError):
|
|
166
|
+
"""Thrown when a request times out"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, message: str = "Request timed out"):
|
|
169
|
+
super().__init__(message, "internal_error")
|