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,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Account Resource
|
|
3
|
+
|
|
4
|
+
Access account information, credit balance, and API keys.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from ..types import Account, ApiKey, Credits, CreditTransaction
|
|
10
|
+
from ..utils.http import AsyncHttpClient, HttpClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _transform_response(data: Dict[str, Any], key_map: Dict[str, str]) -> Dict[str, Any]:
|
|
14
|
+
"""Transform snake_case API response to camelCase for pydantic models."""
|
|
15
|
+
result = {}
|
|
16
|
+
for key, value in data.items():
|
|
17
|
+
new_key = key_map.get(key, key)
|
|
18
|
+
result[new_key] = value
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CREDITS_KEY_MAP = {
|
|
23
|
+
"reserved_balance": "reservedBalance",
|
|
24
|
+
"available_balance": "availableBalance",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TRANSACTION_KEY_MAP = {
|
|
28
|
+
"balance_after": "balanceAfter",
|
|
29
|
+
"message_id": "messageId",
|
|
30
|
+
"created_at": "createdAt",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
API_KEY_MAP = {
|
|
34
|
+
"last_four": "lastFour",
|
|
35
|
+
"created_at": "createdAt",
|
|
36
|
+
"last_used_at": "lastUsedAt",
|
|
37
|
+
"expires_at": "expiresAt",
|
|
38
|
+
"is_revoked": "isRevoked",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ACCOUNT_KEY_MAP = {
|
|
42
|
+
"created_at": "createdAt",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AccountResource:
|
|
47
|
+
"""
|
|
48
|
+
Account API resource (synchronous)
|
|
49
|
+
|
|
50
|
+
Access account information, credit balance, and API keys.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> # Get credit balance
|
|
54
|
+
>>> credits = client.account.get_credits()
|
|
55
|
+
>>> print(f'Available: {credits.available_balance} credits')
|
|
56
|
+
>>>
|
|
57
|
+
>>> # Get transaction history
|
|
58
|
+
>>> transactions = client.account.get_credit_transactions()
|
|
59
|
+
>>>
|
|
60
|
+
>>> # List API keys
|
|
61
|
+
>>> keys = client.account.list_api_keys()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, http: HttpClient):
|
|
65
|
+
self._http = http
|
|
66
|
+
|
|
67
|
+
def get(self) -> Account:
|
|
68
|
+
"""
|
|
69
|
+
Get account information.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Account details
|
|
73
|
+
"""
|
|
74
|
+
response = self._http.request("GET", "/account")
|
|
75
|
+
return Account(**_transform_response(response, ACCOUNT_KEY_MAP))
|
|
76
|
+
|
|
77
|
+
def get_credits(self) -> Credits:
|
|
78
|
+
"""
|
|
79
|
+
Get credit balance.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Current credit balance and reserved credits
|
|
83
|
+
"""
|
|
84
|
+
response = self._http.request("GET", "/credits")
|
|
85
|
+
return Credits(**_transform_response(response, CREDITS_KEY_MAP))
|
|
86
|
+
|
|
87
|
+
def get_credit_transactions(
|
|
88
|
+
self, limit: Optional[int] = None, offset: Optional[int] = None
|
|
89
|
+
) -> List[CreditTransaction]:
|
|
90
|
+
"""
|
|
91
|
+
Get credit transaction history.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
limit: Maximum number of transactions to return
|
|
95
|
+
offset: Number of transactions to skip
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Array of credit transactions
|
|
99
|
+
"""
|
|
100
|
+
params = {}
|
|
101
|
+
if limit is not None:
|
|
102
|
+
params["limit"] = limit
|
|
103
|
+
if offset is not None:
|
|
104
|
+
params["offset"] = offset
|
|
105
|
+
|
|
106
|
+
response = self._http.request("GET", "/credits/transactions", params=params)
|
|
107
|
+
return [CreditTransaction(**_transform_response(t, TRANSACTION_KEY_MAP)) for t in response]
|
|
108
|
+
|
|
109
|
+
def list_api_keys(self) -> List[ApiKey]:
|
|
110
|
+
"""
|
|
111
|
+
List API keys for the account.
|
|
112
|
+
|
|
113
|
+
Note: This returns key metadata, not the actual secret keys.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Array of API keys
|
|
117
|
+
"""
|
|
118
|
+
response = self._http.request("GET", "/keys")
|
|
119
|
+
return [ApiKey(**_transform_response(k, API_KEY_MAP)) for k in response]
|
|
120
|
+
|
|
121
|
+
def get_api_key(self, key_id: str) -> ApiKey:
|
|
122
|
+
"""
|
|
123
|
+
Get a specific API key by ID.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
key_id: API key ID
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
API key details
|
|
130
|
+
"""
|
|
131
|
+
response = self._http.request("GET", f"/keys/{key_id}")
|
|
132
|
+
return ApiKey(**_transform_response(response, API_KEY_MAP))
|
|
133
|
+
|
|
134
|
+
def get_api_key_usage(self, key_id: str) -> Dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Get usage statistics for an API key.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
key_id: API key ID
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Usage statistics
|
|
143
|
+
"""
|
|
144
|
+
response = self._http.request("GET", f"/keys/{key_id}/usage")
|
|
145
|
+
return response
|
|
146
|
+
|
|
147
|
+
def create_api_key(self, name: str, expires_at: Optional[str] = None) -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Create a new API key.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
name: Display name for the API key
|
|
153
|
+
expires_at: Optional expiration date (ISO 8601)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict with 'apiKey' (ApiKey metadata) and 'key' (full secret key - only shown once)
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> result = client.account.create_api_key('Production')
|
|
160
|
+
>>> print(f"Save this key: {result['key']}") # Only shown once!
|
|
161
|
+
"""
|
|
162
|
+
if not name:
|
|
163
|
+
raise ValueError("API key name is required")
|
|
164
|
+
|
|
165
|
+
body: Dict[str, Any] = {"name": name}
|
|
166
|
+
if expires_at:
|
|
167
|
+
body["expiresAt"] = expires_at
|
|
168
|
+
|
|
169
|
+
response = self._http.request("POST", "/account/keys", body=body)
|
|
170
|
+
return response
|
|
171
|
+
|
|
172
|
+
def revoke_api_key(self, key_id: str) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Revoke an API key.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
key_id: API key ID to revoke
|
|
178
|
+
"""
|
|
179
|
+
if not key_id:
|
|
180
|
+
raise ValueError("API key ID is required")
|
|
181
|
+
|
|
182
|
+
self._http.request("DELETE", f"/account/keys/{key_id}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AsyncAccountResource:
|
|
186
|
+
"""
|
|
187
|
+
Account API resource (asynchronous)
|
|
188
|
+
|
|
189
|
+
Async version of the account resource for use with asyncio.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(self, http: AsyncHttpClient):
|
|
193
|
+
self._http = http
|
|
194
|
+
|
|
195
|
+
async def get(self) -> Account:
|
|
196
|
+
"""Get account information."""
|
|
197
|
+
response = await self._http.request("GET", "/account")
|
|
198
|
+
return Account(**_transform_response(response, ACCOUNT_KEY_MAP))
|
|
199
|
+
|
|
200
|
+
async def get_credits(self) -> Credits:
|
|
201
|
+
"""Get credit balance."""
|
|
202
|
+
response = await self._http.request("GET", "/credits")
|
|
203
|
+
return Credits(**_transform_response(response, CREDITS_KEY_MAP))
|
|
204
|
+
|
|
205
|
+
async def get_credit_transactions(
|
|
206
|
+
self, limit: Optional[int] = None, offset: Optional[int] = None
|
|
207
|
+
) -> List[CreditTransaction]:
|
|
208
|
+
"""Get credit transaction history."""
|
|
209
|
+
params = {}
|
|
210
|
+
if limit is not None:
|
|
211
|
+
params["limit"] = limit
|
|
212
|
+
if offset is not None:
|
|
213
|
+
params["offset"] = offset
|
|
214
|
+
|
|
215
|
+
response = await self._http.request("GET", "/credits/transactions", params=params)
|
|
216
|
+
return [CreditTransaction(**_transform_response(t, TRANSACTION_KEY_MAP)) for t in response]
|
|
217
|
+
|
|
218
|
+
async def list_api_keys(self) -> List[ApiKey]:
|
|
219
|
+
"""List API keys for the account."""
|
|
220
|
+
response = await self._http.request("GET", "/keys")
|
|
221
|
+
return [ApiKey(**_transform_response(k, API_KEY_MAP)) for k in response]
|
|
222
|
+
|
|
223
|
+
async def get_api_key(self, key_id: str) -> ApiKey:
|
|
224
|
+
"""Get a specific API key by ID."""
|
|
225
|
+
response = await self._http.request("GET", f"/keys/{key_id}")
|
|
226
|
+
return ApiKey(**_transform_response(response, API_KEY_MAP))
|
|
227
|
+
|
|
228
|
+
async def get_api_key_usage(self, key_id: str) -> Dict[str, Any]:
|
|
229
|
+
"""Get usage statistics for an API key."""
|
|
230
|
+
response = await self._http.request("GET", f"/keys/{key_id}/usage")
|
|
231
|
+
return response
|
|
232
|
+
|
|
233
|
+
async def create_api_key(self, name: str, expires_at: Optional[str] = None) -> Dict[str, Any]:
|
|
234
|
+
"""
|
|
235
|
+
Create a new API key (async).
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
name: Display name for the API key
|
|
239
|
+
expires_at: Optional expiration date (ISO 8601)
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Dict with 'apiKey' (ApiKey metadata) and 'key' (full secret key - only shown once)
|
|
243
|
+
"""
|
|
244
|
+
if not name:
|
|
245
|
+
raise ValueError("API key name is required")
|
|
246
|
+
|
|
247
|
+
body: Dict[str, Any] = {"name": name}
|
|
248
|
+
if expires_at:
|
|
249
|
+
body["expiresAt"] = expires_at
|
|
250
|
+
|
|
251
|
+
response = await self._http.request("POST", "/account/keys", body=body)
|
|
252
|
+
return response
|
|
253
|
+
|
|
254
|
+
async def revoke_api_key(self, key_id: str) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Revoke an API key (async).
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
key_id: API key ID to revoke
|
|
260
|
+
"""
|
|
261
|
+
if not key_id:
|
|
262
|
+
raise ValueError("API key ID is required")
|
|
263
|
+
|
|
264
|
+
await self._http.request("DELETE", f"/account/keys/{key_id}")
|