senddy 0.2.0__tar.gz
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.
- senddy-0.2.0/.gitignore +9 -0
- senddy-0.2.0/PKG-INFO +15 -0
- senddy-0.2.0/README.md +307 -0
- senddy-0.2.0/pyproject.toml +37 -0
- senddy-0.2.0/src/senddy/__init__.py +128 -0
- senddy-0.2.0/src/senddy/_client.py +151 -0
- senddy-0.2.0/src/senddy/_errors.py +87 -0
- senddy-0.2.0/src/senddy/_http.py +363 -0
- senddy-0.2.0/src/senddy/_types.py +426 -0
- senddy-0.2.0/src/senddy/py.typed +0 -0
- senddy-0.2.0/src/senddy/resources/__init__.py +24 -0
- senddy-0.2.0/src/senddy/resources/_billing.py +60 -0
- senddy-0.2.0/src/senddy/resources/_domains.py +228 -0
- senddy-0.2.0/src/senddy/resources/_emails.py +286 -0
- senddy-0.2.0/src/senddy/resources/_inbound_emails.py +125 -0
- senddy-0.2.0/src/senddy/resources/_inbound_routes.py +246 -0
- senddy-0.2.0/src/senddy/resources/_suppressions.py +175 -0
- senddy-0.2.0/src/senddy/resources/_webhook_endpoints.py +235 -0
- senddy-0.2.0/tests/__init__.py +0 -0
- senddy-0.2.0/tests/conftest.py +8 -0
- senddy-0.2.0/tests/factories.py +70 -0
- senddy-0.2.0/tests/test_client.py +82 -0
- senddy-0.2.0/tests/test_errors.py +71 -0
- senddy-0.2.0/tests/test_http.py +260 -0
- senddy-0.2.0/tests/test_resources/__init__.py +0 -0
- senddy-0.2.0/tests/test_resources/test_emails.py +206 -0
- senddy-0.2.0/tests/test_resources/test_suppressions.py +151 -0
senddy-0.2.0/.gitignore
ADDED
senddy-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: senddy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for Senddy.io
|
|
5
|
+
Project-URL: Homepage, https://github.com/senddy-io/senddy-python
|
|
6
|
+
Project-URL: Repository, https://github.com/senddy-io/senddy-python
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx<1,>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
senddy-0.2.0/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Senddy Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Senddy.io](https://senddy.io) email API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Requires **Python 3.9+**.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install senddy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from senddy import Senddy, SendEmailParams
|
|
17
|
+
|
|
18
|
+
client = Senddy("senddy_live_api_key")
|
|
19
|
+
|
|
20
|
+
result = client.emails.send(SendEmailParams(
|
|
21
|
+
from_="sender@senddy.io",
|
|
22
|
+
to="recipient@example.com",
|
|
23
|
+
subject="Hello",
|
|
24
|
+
html="<p>Welcome!</p>",
|
|
25
|
+
))
|
|
26
|
+
|
|
27
|
+
print(result.id) # email_abc123
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Async Support
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from senddy import AsyncSenddy, SendEmailParams
|
|
34
|
+
|
|
35
|
+
async with AsyncSenddy("senddy_live_api_key") as client:
|
|
36
|
+
result = await client.emails.send(SendEmailParams(
|
|
37
|
+
from_="sender@senddy.io",
|
|
38
|
+
to="recipient@example.com",
|
|
39
|
+
subject="Hello",
|
|
40
|
+
html="<p>Welcome!</p>",
|
|
41
|
+
))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
All options are optional — sensible defaults are used if you don't override them.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
client = Senddy(
|
|
50
|
+
"senddy_live_api_key",
|
|
51
|
+
timeout=30.0, # seconds
|
|
52
|
+
retries=2,
|
|
53
|
+
headers={"X-Custom": "value"}, # extra headers on every request
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If no API key is passed to the constructor, the SDK reads from the `SENDDY_API_KEY` environment variable.
|
|
58
|
+
|
|
59
|
+
Both clients support context managers for proper resource cleanup:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
with Senddy("senddy_live_api_key") as client:
|
|
63
|
+
# client.close() called automatically on exit
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Emails
|
|
68
|
+
|
|
69
|
+
### Send
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from senddy import SendEmailParams, Attachment, RequestOptions
|
|
73
|
+
|
|
74
|
+
result = client.emails.send(
|
|
75
|
+
SendEmailParams(
|
|
76
|
+
from_="sender@senddy.io",
|
|
77
|
+
to=["alice@example.com", "bob@example.com"],
|
|
78
|
+
subject="Hello",
|
|
79
|
+
html="<p>Hi there</p>",
|
|
80
|
+
text="Hi there", # optional plain-text fallback
|
|
81
|
+
cc="cc@example.com", # optional
|
|
82
|
+
bcc="bcc@example.com", # optional
|
|
83
|
+
reply_to="reply@example.com", # optional
|
|
84
|
+
tags={"campaign": "welcome"}, # optional metadata
|
|
85
|
+
attachments=[Attachment( # optional
|
|
86
|
+
filename="report.pdf",
|
|
87
|
+
content=base64_string,
|
|
88
|
+
content_type="application/pdf",
|
|
89
|
+
)],
|
|
90
|
+
),
|
|
91
|
+
options=RequestOptions(idempotency_key="unique-key"), # prevents duplicate sends
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** The `from_` parameter uses a trailing underscore because `from` is a Python reserved word. The SDK maps it to `from` in the API request automatically.
|
|
96
|
+
|
|
97
|
+
### Get
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
email = client.emails.get("email_abc123")
|
|
101
|
+
print(email.status) # 'delivered'
|
|
102
|
+
print(email.recipients) # list of EmailRecipient
|
|
103
|
+
print(email.events) # list of EmailEvent
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### List
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from senddy import ListEmailsParams
|
|
110
|
+
|
|
111
|
+
result = client.emails.list(ListEmailsParams(
|
|
112
|
+
limit=25,
|
|
113
|
+
offset=0,
|
|
114
|
+
status="delivered",
|
|
115
|
+
since="2026-01-01T00:00:00Z",
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
for email in result.data:
|
|
119
|
+
print(email.subject)
|
|
120
|
+
print(result.pagination.total)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Get rendered content
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
content = client.emails.get_content("email_abc123")
|
|
127
|
+
print(content.html) # str or None
|
|
128
|
+
print(content.text) # str or None
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Download EML
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
download = client.emails.download("email_abc123")
|
|
135
|
+
# download.content is bytes containing the raw EML
|
|
136
|
+
# download.content_type is 'message/rfc822'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Suppressions
|
|
140
|
+
|
|
141
|
+
### List
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from senddy import ListSuppressionsParams
|
|
145
|
+
|
|
146
|
+
result = client.suppressions.list(ListSuppressionsParams(
|
|
147
|
+
limit=50,
|
|
148
|
+
search="example.com",
|
|
149
|
+
reason="hard_bounce",
|
|
150
|
+
))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Create
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from senddy import CreateSuppressionParams
|
|
157
|
+
|
|
158
|
+
entry = client.suppressions.create(CreateSuppressionParams(
|
|
159
|
+
email_address="block@example.com",
|
|
160
|
+
))
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Delete
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
result = client.suppressions.delete("block@example.com")
|
|
167
|
+
print(result.removed) # True
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Domains
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from senddy import CreateDomainParams
|
|
174
|
+
|
|
175
|
+
# Add a sending domain
|
|
176
|
+
domain = client.domains.create(CreateDomainParams(domain="mail.example.com"))
|
|
177
|
+
|
|
178
|
+
# Trigger DNS verification
|
|
179
|
+
client.domains.verify(domain.id)
|
|
180
|
+
|
|
181
|
+
# List, get, delete
|
|
182
|
+
client.domains.list()
|
|
183
|
+
client.domains.get(domain.id)
|
|
184
|
+
client.domains.delete(domain.id)
|
|
185
|
+
|
|
186
|
+
# Rotate DKIM keys
|
|
187
|
+
client.domains.regenerate_dkim(domain.id)
|
|
188
|
+
|
|
189
|
+
# Bulk DNS health check across all domains
|
|
190
|
+
client.domains.check_dns_health()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Inbound Routes
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from senddy import CreateInboundRouteParams, UpdateInboundRouteParams
|
|
197
|
+
|
|
198
|
+
# Create a catchall route that forwards to your webhook
|
|
199
|
+
route = client.inbound_routes.create(CreateInboundRouteParams(
|
|
200
|
+
match_type="catchall",
|
|
201
|
+
webhook_url="https://your-app.example.com/inbound",
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
# List, get, update, delete
|
|
205
|
+
client.inbound_routes.list()
|
|
206
|
+
client.inbound_routes.get(route.id)
|
|
207
|
+
client.inbound_routes.update(route.id, UpdateInboundRouteParams(enabled=False))
|
|
208
|
+
client.inbound_routes.delete(route.id)
|
|
209
|
+
|
|
210
|
+
# Verify the MX record points at Senddy
|
|
211
|
+
client.inbound_routes.verify_mx(route.id)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
See the [inbound docs](https://senddy.io/docs/inbound) for match patterns, webhook signing, and full configuration.
|
|
215
|
+
|
|
216
|
+
## Inbound Emails
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from senddy import ListInboundEmailsParams
|
|
220
|
+
|
|
221
|
+
result = client.inbound_emails.list(ListInboundEmailsParams(route_id=42, limit=50))
|
|
222
|
+
|
|
223
|
+
email = client.inbound_emails.get("inbound_xyz")
|
|
224
|
+
print(email.text_body, email.attachments)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Webhook Endpoints
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from senddy import CreateWebhookEndpointParams, UpdateWebhookEndpointParams
|
|
231
|
+
|
|
232
|
+
# Register an endpoint to receive event callbacks
|
|
233
|
+
result = client.webhook_endpoints.create(CreateWebhookEndpointParams(
|
|
234
|
+
url="https://your-app.example.com/webhooks/senddy",
|
|
235
|
+
domain="mail.example.com",
|
|
236
|
+
event_type="email.delivered",
|
|
237
|
+
))
|
|
238
|
+
# Save result.secret — you'll need it to verify the signature on incoming webhooks.
|
|
239
|
+
|
|
240
|
+
# Test, list, update, delete
|
|
241
|
+
client.webhook_endpoints.test(result.id)
|
|
242
|
+
client.webhook_endpoints.list()
|
|
243
|
+
client.webhook_endpoints.update(result.id, UpdateWebhookEndpointParams(url="https://new-url.example.com"))
|
|
244
|
+
client.webhook_endpoints.delete(result.id)
|
|
245
|
+
|
|
246
|
+
# Inspect delivery history
|
|
247
|
+
deliveries = client.webhook_endpoints.deliveries(result.id)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Billing
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
balance = client.billing.get_balance()
|
|
254
|
+
print(balance.credit_balance, balance.billing_tier)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Error Handling
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from senddy import (
|
|
261
|
+
APIError,
|
|
262
|
+
ValidationError,
|
|
263
|
+
RateLimitError,
|
|
264
|
+
AuthenticationError,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
client.emails.send(params)
|
|
269
|
+
except ValidationError as err:
|
|
270
|
+
print(f"Validation: {err.details}")
|
|
271
|
+
except RateLimitError as err:
|
|
272
|
+
print(f"Rate limited. Retry after {err.retry_after}s")
|
|
273
|
+
except AuthenticationError as err:
|
|
274
|
+
print(f"Auth error: {err}")
|
|
275
|
+
except APIError as err:
|
|
276
|
+
print(f"API error {err.status_code}: {err}")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Error Types
|
|
280
|
+
|
|
281
|
+
| Class | Status | Description |
|
|
282
|
+
| --------------------- | ------ | -------------------------------------------- |
|
|
283
|
+
| `ValidationError` | 400 | Invalid request, includes `.details` list |
|
|
284
|
+
| `AuthenticationError` | 401 | Missing or invalid API key |
|
|
285
|
+
| `ForbiddenError` | 403 | Insufficient permissions |
|
|
286
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
287
|
+
| `RateLimitError` | 429 | Rate limit exceeded, includes `.retry_after` |
|
|
288
|
+
| `InternalError` | 5xx | Server error |
|
|
289
|
+
|
|
290
|
+
All error classes inherit from `APIError`, which inherits from `Exception`.
|
|
291
|
+
|
|
292
|
+
## Retries
|
|
293
|
+
|
|
294
|
+
The SDK automatically retries on 429 and 5xx responses with exponential backoff and jitter. Retries respect the `Retry-After` header. Non-retryable errors (4xx except 429) are raised immediately.
|
|
295
|
+
|
|
296
|
+
## Requirements
|
|
297
|
+
|
|
298
|
+
- Python >= 3.9
|
|
299
|
+
- Runtime dependency: [httpx](https://www.python-httpx.org/)
|
|
300
|
+
|
|
301
|
+
## Type Safety
|
|
302
|
+
|
|
303
|
+
The package ships with a `py.typed` marker (PEP 561) and all public types are fully annotated. Works with mypy, pyright, and IDE autocompletion out of the box.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "senddy"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Official Python SDK for Senddy.io"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
8
|
+
|
|
9
|
+
[project.urls]
|
|
10
|
+
Homepage = "https://github.com/senddy-io/senddy-python"
|
|
11
|
+
Repository = "https://github.com/senddy-io/senddy-python"
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest>=8",
|
|
16
|
+
"pytest-asyncio>=0.24",
|
|
17
|
+
"respx>=0.22",
|
|
18
|
+
"ruff>=0.8",
|
|
19
|
+
"mypy>=1.13",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/senddy"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
asyncio_mode = "auto"
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py39"
|
|
34
|
+
line-length = 100
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
strict = true
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Official Python SDK for Senddy.io."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncSenddy, Senddy
|
|
4
|
+
from ._errors import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
InternalError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from ._types import (
|
|
14
|
+
Attachment,
|
|
15
|
+
BillingBalance,
|
|
16
|
+
CreateDomainParams,
|
|
17
|
+
CreateInboundRouteParams,
|
|
18
|
+
CreateInboundRouteResponse,
|
|
19
|
+
CreateSuppressionParams,
|
|
20
|
+
CreateWebhookEndpointParams,
|
|
21
|
+
CreateWebhookEndpointResponse,
|
|
22
|
+
DeleteDomainResponse,
|
|
23
|
+
DeleteInboundRouteResponse,
|
|
24
|
+
DeleteSuppressionResponse,
|
|
25
|
+
DeleteWebhookEndpointResponse,
|
|
26
|
+
DnsHealthEntry,
|
|
27
|
+
DnsHealthResponse,
|
|
28
|
+
Domain,
|
|
29
|
+
Email,
|
|
30
|
+
EmailContentResponse,
|
|
31
|
+
EmailDownloadResponse,
|
|
32
|
+
EmailEvent,
|
|
33
|
+
EmailRecipient,
|
|
34
|
+
EmailSummary,
|
|
35
|
+
InboundEmail,
|
|
36
|
+
InboundEmailSummary,
|
|
37
|
+
InboundRoute,
|
|
38
|
+
InboundRouteDetail,
|
|
39
|
+
ListDomainsParams,
|
|
40
|
+
ListEmailsParams,
|
|
41
|
+
ListInboundEmailsParams,
|
|
42
|
+
ListInboundRoutesParams,
|
|
43
|
+
ListSuppressionsParams,
|
|
44
|
+
ListWebhookEndpointsParams,
|
|
45
|
+
PaginatedResponse,
|
|
46
|
+
Pagination,
|
|
47
|
+
RequestOptions,
|
|
48
|
+
SendEmailParams,
|
|
49
|
+
SendEmailResponse,
|
|
50
|
+
SuppressionEntry,
|
|
51
|
+
TestWebhookResponse,
|
|
52
|
+
UpdateInboundRouteParams,
|
|
53
|
+
UpdateInboundRouteResponse,
|
|
54
|
+
UpdateWebhookEndpointParams,
|
|
55
|
+
VerifyDomainResponse,
|
|
56
|
+
VerifyMxResponse,
|
|
57
|
+
WebhookDelivery,
|
|
58
|
+
WebhookEndpoint,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
# Clients
|
|
63
|
+
"Senddy",
|
|
64
|
+
"AsyncSenddy",
|
|
65
|
+
# Errors
|
|
66
|
+
"APIError",
|
|
67
|
+
"ValidationError",
|
|
68
|
+
"AuthenticationError",
|
|
69
|
+
"ForbiddenError",
|
|
70
|
+
"NotFoundError",
|
|
71
|
+
"RateLimitError",
|
|
72
|
+
"InternalError",
|
|
73
|
+
# Common
|
|
74
|
+
"RequestOptions",
|
|
75
|
+
"PaginatedResponse",
|
|
76
|
+
"Pagination",
|
|
77
|
+
# Emails
|
|
78
|
+
"Attachment",
|
|
79
|
+
"SendEmailParams",
|
|
80
|
+
"SendEmailResponse",
|
|
81
|
+
"ListEmailsParams",
|
|
82
|
+
"EmailSummary",
|
|
83
|
+
"Email",
|
|
84
|
+
"EmailRecipient",
|
|
85
|
+
"EmailEvent",
|
|
86
|
+
"EmailDownloadResponse",
|
|
87
|
+
"EmailContentResponse",
|
|
88
|
+
# Suppressions
|
|
89
|
+
"ListSuppressionsParams",
|
|
90
|
+
"SuppressionEntry",
|
|
91
|
+
"CreateSuppressionParams",
|
|
92
|
+
"DeleteSuppressionResponse",
|
|
93
|
+
# Domains
|
|
94
|
+
"Domain",
|
|
95
|
+
"CreateDomainParams",
|
|
96
|
+
"ListDomainsParams",
|
|
97
|
+
"VerifyDomainResponse",
|
|
98
|
+
"DeleteDomainResponse",
|
|
99
|
+
"DnsHealthEntry",
|
|
100
|
+
"DnsHealthResponse",
|
|
101
|
+
# Inbound Routes
|
|
102
|
+
"InboundRoute",
|
|
103
|
+
"InboundRouteDetail",
|
|
104
|
+
"ListInboundRoutesParams",
|
|
105
|
+
"CreateInboundRouteParams",
|
|
106
|
+
"CreateInboundRouteResponse",
|
|
107
|
+
"UpdateInboundRouteParams",
|
|
108
|
+
"UpdateInboundRouteResponse",
|
|
109
|
+
"DeleteInboundRouteResponse",
|
|
110
|
+
"VerifyMxResponse",
|
|
111
|
+
# Inbound Emails
|
|
112
|
+
"InboundEmail",
|
|
113
|
+
"InboundEmailSummary",
|
|
114
|
+
"ListInboundEmailsParams",
|
|
115
|
+
# Webhook Endpoints
|
|
116
|
+
"WebhookEndpoint",
|
|
117
|
+
"ListWebhookEndpointsParams",
|
|
118
|
+
"CreateWebhookEndpointParams",
|
|
119
|
+
"CreateWebhookEndpointResponse",
|
|
120
|
+
"UpdateWebhookEndpointParams",
|
|
121
|
+
"DeleteWebhookEndpointResponse",
|
|
122
|
+
"TestWebhookResponse",
|
|
123
|
+
"WebhookDelivery",
|
|
124
|
+
# Billing
|
|
125
|
+
"BillingBalance",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._errors import AuthenticationError
|
|
11
|
+
from ._http import DEFAULT_BASE_URL, DEFAULT_RETRIES, DEFAULT_TIMEOUT
|
|
12
|
+
from .resources._billing import AsyncBilling, Billing
|
|
13
|
+
from .resources._domains import AsyncDomains, Domains
|
|
14
|
+
from .resources._emails import AsyncEmails, Emails
|
|
15
|
+
from .resources._inbound_emails import AsyncInboundEmails, InboundEmails
|
|
16
|
+
from .resources._inbound_routes import AsyncInboundRoutes, InboundRoutes
|
|
17
|
+
from .resources._suppressions import AsyncSuppressions, Suppressions
|
|
18
|
+
from .resources._webhook_endpoints import AsyncWebhookEndpoints, WebhookEndpoints
|
|
19
|
+
|
|
20
|
+
ENV_VAR_NAME = "SENDDY_API_KEY"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
24
|
+
if api_key is not None:
|
|
25
|
+
if not api_key.strip():
|
|
26
|
+
raise AuthenticationError(
|
|
27
|
+
"API key must not be blank.",
|
|
28
|
+
"missing_api_key",
|
|
29
|
+
"",
|
|
30
|
+
)
|
|
31
|
+
return api_key
|
|
32
|
+
|
|
33
|
+
env_key = os.environ.get(ENV_VAR_NAME, "").strip()
|
|
34
|
+
if env_key:
|
|
35
|
+
warnings.warn(
|
|
36
|
+
f"Using API key from {ENV_VAR_NAME} environment variable. "
|
|
37
|
+
"Pass the key explicitly to suppress this warning.",
|
|
38
|
+
stacklevel=3,
|
|
39
|
+
)
|
|
40
|
+
return env_key
|
|
41
|
+
|
|
42
|
+
raise AuthenticationError(
|
|
43
|
+
f"API key is required. Pass it to the constructor or set the {ENV_VAR_NAME} "
|
|
44
|
+
"environment variable.",
|
|
45
|
+
"missing_api_key",
|
|
46
|
+
"",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Senddy:
|
|
51
|
+
"""Synchronous client for the Senddy.io API."""
|
|
52
|
+
|
|
53
|
+
billing: Billing
|
|
54
|
+
domains: Domains
|
|
55
|
+
emails: Emails
|
|
56
|
+
inbound_emails: InboundEmails
|
|
57
|
+
inbound_routes: InboundRoutes
|
|
58
|
+
suppressions: Suppressions
|
|
59
|
+
webhook_endpoints: WebhookEndpoints
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
api_key: Optional[str] = None,
|
|
64
|
+
*,
|
|
65
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
66
|
+
retries: int = DEFAULT_RETRIES,
|
|
67
|
+
headers: Optional[dict[str, str]] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
resolved_key = _resolve_api_key(api_key)
|
|
70
|
+
|
|
71
|
+
self._http_client = httpx.Client(
|
|
72
|
+
base_url=DEFAULT_BASE_URL,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
headers=headers,
|
|
75
|
+
)
|
|
76
|
+
self._api_key = resolved_key
|
|
77
|
+
self._retries = retries
|
|
78
|
+
|
|
79
|
+
self.billing = Billing(self._http_client, resolved_key, retries)
|
|
80
|
+
self.domains = Domains(self._http_client, resolved_key, retries)
|
|
81
|
+
self.emails = Emails(self._http_client, resolved_key, retries)
|
|
82
|
+
self.inbound_emails = InboundEmails(self._http_client, resolved_key, retries)
|
|
83
|
+
self.inbound_routes = InboundRoutes(self._http_client, resolved_key, retries)
|
|
84
|
+
self.suppressions = Suppressions(self._http_client, resolved_key, retries)
|
|
85
|
+
self.webhook_endpoints = WebhookEndpoints(self._http_client, resolved_key, retries)
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
self._http_client.close()
|
|
89
|
+
|
|
90
|
+
def __enter__(self) -> Senddy:
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __exit__(
|
|
94
|
+
self,
|
|
95
|
+
exc_type: Optional[type[BaseException]],
|
|
96
|
+
exc_val: Optional[BaseException],
|
|
97
|
+
exc_tb: Optional[TracebackType],
|
|
98
|
+
) -> None:
|
|
99
|
+
self.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AsyncSenddy:
|
|
103
|
+
"""Asynchronous client for the Senddy.io API."""
|
|
104
|
+
|
|
105
|
+
billing: AsyncBilling
|
|
106
|
+
domains: AsyncDomains
|
|
107
|
+
emails: AsyncEmails
|
|
108
|
+
inbound_emails: AsyncInboundEmails
|
|
109
|
+
inbound_routes: AsyncInboundRoutes
|
|
110
|
+
suppressions: AsyncSuppressions
|
|
111
|
+
webhook_endpoints: AsyncWebhookEndpoints
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
api_key: Optional[str] = None,
|
|
116
|
+
*,
|
|
117
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
118
|
+
retries: int = DEFAULT_RETRIES,
|
|
119
|
+
headers: Optional[dict[str, str]] = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
resolved_key = _resolve_api_key(api_key)
|
|
122
|
+
|
|
123
|
+
self._http_client = httpx.AsyncClient(
|
|
124
|
+
base_url=DEFAULT_BASE_URL,
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
headers=headers,
|
|
127
|
+
)
|
|
128
|
+
self._api_key = resolved_key
|
|
129
|
+
self._retries = retries
|
|
130
|
+
|
|
131
|
+
self.billing = AsyncBilling(self._http_client, resolved_key, retries)
|
|
132
|
+
self.domains = AsyncDomains(self._http_client, resolved_key, retries)
|
|
133
|
+
self.emails = AsyncEmails(self._http_client, resolved_key, retries)
|
|
134
|
+
self.inbound_emails = AsyncInboundEmails(self._http_client, resolved_key, retries)
|
|
135
|
+
self.inbound_routes = AsyncInboundRoutes(self._http_client, resolved_key, retries)
|
|
136
|
+
self.suppressions = AsyncSuppressions(self._http_client, resolved_key, retries)
|
|
137
|
+
self.webhook_endpoints = AsyncWebhookEndpoints(self._http_client, resolved_key, retries)
|
|
138
|
+
|
|
139
|
+
async def close(self) -> None:
|
|
140
|
+
await self._http_client.aclose()
|
|
141
|
+
|
|
142
|
+
async def __aenter__(self) -> AsyncSenddy:
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
async def __aexit__(
|
|
146
|
+
self,
|
|
147
|
+
exc_type: Optional[type[BaseException]],
|
|
148
|
+
exc_val: Optional[BaseException],
|
|
149
|
+
exc_tb: Optional[TracebackType],
|
|
150
|
+
) -> None:
|
|
151
|
+
await self.close()
|