sendclipper 0.1.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.
Files changed (29) hide show
  1. sendclipper-0.1.0/.gitignore +9 -0
  2. sendclipper-0.1.0/CHANGELOG.md +12 -0
  3. sendclipper-0.1.0/LICENSE +21 -0
  4. sendclipper-0.1.0/PKG-INFO +233 -0
  5. sendclipper-0.1.0/README.md +204 -0
  6. sendclipper-0.1.0/pyproject.toml +55 -0
  7. sendclipper-0.1.0/src/sendclipper/__init__.py +65 -0
  8. sendclipper-0.1.0/src/sendclipper/_client.py +81 -0
  9. sendclipper-0.1.0/src/sendclipper/_core.py +454 -0
  10. sendclipper-0.1.0/src/sendclipper/_errors.py +118 -0
  11. sendclipper-0.1.0/src/sendclipper/_version.py +3 -0
  12. sendclipper-0.1.0/src/sendclipper/_webhooks.py +147 -0
  13. sendclipper-0.1.0/src/sendclipper/py.typed +0 -0
  14. sendclipper-0.1.0/src/sendclipper/resources/__init__.py +1 -0
  15. sendclipper-0.1.0/src/sendclipper/resources/_base.py +15 -0
  16. sendclipper-0.1.0/src/sendclipper/resources/audience.py +119 -0
  17. sendclipper-0.1.0/src/sendclipper/resources/domains.py +47 -0
  18. sendclipper-0.1.0/src/sendclipper/resources/emails.py +68 -0
  19. sendclipper-0.1.0/src/sendclipper/resources/mailboxes.py +89 -0
  20. sendclipper-0.1.0/src/sendclipper/resources/sandboxes.py +137 -0
  21. sendclipper-0.1.0/src/sendclipper/resources/templates.py +115 -0
  22. sendclipper-0.1.0/src/sendclipper/resources/webhooks.py +105 -0
  23. sendclipper-0.1.0/src/sendclipper/resources/workspace.py +30 -0
  24. sendclipper-0.1.0/src/sendclipper/types.py +875 -0
  25. sendclipper-0.1.0/tests/__init__.py +0 -0
  26. sendclipper-0.1.0/tests/conftest.py +81 -0
  27. sendclipper-0.1.0/tests/test_client.py +499 -0
  28. sendclipper-0.1.0/tests/test_webhooks.py +108 -0
  29. sendclipper-0.1.0/tests/wheel/smoke.py +35 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ /dist/
5
+ /build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-16
4
+
5
+ First release.
6
+
7
+ - `emails.send`, `batch.send`, `emails.get`, `emails.list`, `emails.iterate`, `emails.stats`.
8
+ - Templates (with tags and image assets), domains, sandboxes (including the JSON door and `wait_for`), webhooks and their dispatches, contacts, audiences, suppressions, mailboxes, automations, workspace and API keys.
9
+ - Every method returns a `Result` with `data`, `error` and `headers`; it unpacks to `(data, error)`.
10
+ - Automatic `Idempotency-Key` on every JSON write; retries on network errors, 429 and 5xx.
11
+ - `verify_webhook()` for `X-SendClipper-Signature-V2`, rotation-aware.
12
+ - No runtime dependencies; typed (`py.typed`), Python 3.9 or newer.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SendClipper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.5
2
+ Name: sendclipper
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the SendClipper email API: send mail, manage domains, templates, sandboxes, webhooks, contacts and suppressions.
5
+ Project-URL: Homepage, https://sendclipper.com
6
+ Project-URL: Documentation, https://sendclipper.com/docs#sdk-python
7
+ Project-URL: Source, https://github.com/sendclipper/sendclipper-python
8
+ Project-URL: Issues, https://github.com/sendclipper/sendclipper-python/issues
9
+ Project-URL: Changelog, https://github.com/sendclipper/sendclipper-python/blob/main/CHANGELOG.md
10
+ Author-email: SendClipper <support@sendclipper.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: email,sdk,sendclipper,smtp,transactional-email,webhooks
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Communications :: Email
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+
30
+ # SendClipper Python SDK
31
+
32
+ The official Python library for the [SendClipper](https://sendclipper.com) email API.
33
+
34
+ ```bash
35
+ pip install sendclipper
36
+ # or
37
+ uv add sendclipper
38
+ # or
39
+ poetry add sendclipper
40
+ ```
41
+
42
+ Python 3.9 or newer. No runtime dependencies. Fully typed.
43
+
44
+ ## Setup
45
+
46
+ Create an API key in the dashboard under **API keys**.
47
+
48
+ ```python
49
+ from sendclipper import SendClipper
50
+
51
+ sc = SendClipper("sc_live_xxxxxxxxxxxxxxxx")
52
+ # or: SendClipper() reads SENDCLIPPER_API_KEY
53
+ ```
54
+
55
+ ## Send an email
56
+
57
+ ```python
58
+ data, error = sc.emails.send({
59
+ "from": "hello@send.acme.io",
60
+ "to": "ada@lumen.io",
61
+ "subject": "Your receipt",
62
+ "html": "<p>Thanks for your order.</p>",
63
+ "text": "Thanks for your order.",
64
+ })
65
+
66
+ if error:
67
+ print(error.name, error.message) # e.g. domain_not_verified
68
+ else:
69
+ print(f"Email {data['id']} accepted")
70
+ ```
71
+
72
+ Parameters and responses are plain dicts whose keys are the API's own field names (`templateId`, `sendClass`, ...), exactly as the [API reference](https://sendclipper.com/docs) writes them. Your editor still completes them: every one is a `TypedDict` in `sendclipper.types`.
73
+
74
+ `from` must be a bare address on a domain you verified in SendClipper. Each message has one recipient in `to`; for several, use `batch.send`.
75
+
76
+ ## Send with a template
77
+
78
+ Design and publish a template in the dashboard, then send it by id or by its send name:
79
+
80
+ ```python
81
+ sc.emails.send({
82
+ "from": "hello@send.acme.io",
83
+ "to": "ada@lumen.io",
84
+ "templateId": "tpl_9f2b7c10ad42", # or "template": "welcome"
85
+ "variables": {"first_name": "Ada"},
86
+ })
87
+ ```
88
+
89
+ A tag with no value and no `{{tag|fallback}}` is refused by name (`template_variables_missing`), never sent blank.
90
+
91
+ ## Send a batch
92
+
93
+ Up to 500 messages in one request. Each item is accepted or refused on its own:
94
+
95
+ ```python
96
+ data, error = sc.batch.send([
97
+ {"from": "hello@send.acme.io", "to": "ada@lumen.io", "subject": "Hi Ada", "html": "<p>Hi</p>"},
98
+ {"from": "hello@send.acme.io", "to": "alan@lumen.io", "templateId": "tpl_9f2b7c10ad42", "variables": {"first_name": "Alan"}},
99
+ ])
100
+
101
+ for item in data["data"]:
102
+ if item["status"] == "failed":
103
+ print(item["index"], item["error"]["code"])
104
+ ```
105
+
106
+ ## Errors
107
+
108
+ API methods never raise for an API refusal or a network failure. They return a `Result` with `data`, `error` and `headers`, and exactly one of `data` and `error` is set. A `Result` unpacks to `(data, error)`:
109
+
110
+ ```python
111
+ result = sc.domains.list()
112
+ result.data # the parsed response, or None
113
+ result.error # an ErrorResponse, or None
114
+ result.headers # response headers (lower-case names), or None when no answer arrived
115
+ result.ok # error is None
116
+
117
+ domains = sc.domains.list().unwrap() # data, or raises SendClipperApiError
118
+ ```
119
+
120
+ ```python
121
+ @dataclass(frozen=True)
122
+ class ErrorResponse:
123
+ name: str # SendClipper's code: "validation_failed", "rate_limited", "network_error", ...
124
+ message: str # a sentence for a person
125
+ status_code: Optional[int] # None when no answer arrived (network, timeout)
126
+ detail: Any = None # code-specific facts
127
+ ```
128
+
129
+ The iterators (`emails.iterate()` and friends) raise `SendClipperApiError` instead, since a loop has no `error` to hand back. `err.code` is the API's code and `err.status` the HTTP status.
130
+
131
+ ## Retries and idempotency
132
+
133
+ Network errors, `429` and `5xx` answers are retried twice with backoff, honouring `Retry-After`. Every JSON write carries an `Idempotency-Key`, and the API answers a repeated key with the first result for 24 hours. A retried send therefore never goes out twice.
134
+
135
+ To make your own retries safe as well (a job that runs again after a crash), pass a key that names the logical operation:
136
+
137
+ ```python
138
+ sc.emails.send(params, idempotency_key=f"order-{order.id}-receipt")
139
+ ```
140
+
141
+ ## Request options
142
+
143
+ Every method takes request options as keyword arguments after its own parameters:
144
+
145
+ ```python
146
+ sc.emails.list({"status": "bounced"}, timeout=5, headers={"X-Trace-Id": trace_id})
147
+ ```
148
+
149
+ | Option | Meaning |
150
+ |---|---|
151
+ | `idempotency_key` | Your own key for a write |
152
+ | `timeout` | Per-attempt timeout in seconds (default 30) |
153
+ | `max_retries` | Retries for this call (default 2) |
154
+ | `headers` | Extra request headers |
155
+
156
+ Client options: `SendClipper(api_key, base_url=..., timeout=..., max_retries=..., default_headers=..., transport=...)`. `base_url` also reads `SENDCLIPPER_BASE_URL`. `transport` replaces the standard-library HTTP sender, e.g. to go through a proxy library: it receives a `TransportRequest` and returns a `TransportResponse`.
157
+
158
+ ## Verify webhooks
159
+
160
+ SendClipper signs each delivery. Verify it against the **raw** body, before parsing:
161
+
162
+ ```python
163
+ import os
164
+ from flask import Flask, request
165
+ from sendclipper import verify_webhook, WebhookVerificationError
166
+
167
+ app = Flask(__name__)
168
+
169
+ @app.post("/hooks/sendclipper")
170
+ def sendclipper_hook():
171
+ try:
172
+ event = verify_webhook(
173
+ request.get_data(), # raw bytes, not request.json
174
+ request.headers,
175
+ os.environ["SENDCLIPPER_WEBHOOK_SECRET"],
176
+ )
177
+ except WebhookVerificationError as err:
178
+ return err.reason, 400
179
+ # event["id"] is stable across retries: deduplicate on it
180
+ print(event["event"], event["data"]["to"])
181
+ return "", 200
182
+ ```
183
+
184
+ The same call works in Django (`request.body`, `request.headers`) and FastAPI (`await request.body()`, `request.headers`). During a secret rotation a delivery carries two signatures and either one is accepted. A delivery signed more than five minutes away from now is refused. `sc.webhooks.verify(...)` is the same function.
185
+
186
+ To test your handler locally, `signature_header(secret, raw_body)` builds a valid header.
187
+
188
+ ## Sandboxes
189
+
190
+ A sandbox traps mail while you develop: nothing is delivered.
191
+
192
+ ```python
193
+ box = sc.sandboxes.create({"name": "my-app-dev"}).unwrap()
194
+ creds = sc.sandboxes.credentials(box["id"]).unwrap() # SMTP host, port, username, password
195
+
196
+ # Or send through the sandbox's JSON door with its own token, using the same fields as emails.send():
197
+ token = sc.sandboxes.reset_credentials(box["id"]).unwrap()
198
+ sc.sandboxes.send(token["apiToken"], {"from": "app@example.com", "to": "test@example.com", "subject": "Hi", "html": "<p>Hi</p>"})
199
+
200
+ # In a test: wait for the message to arrive
201
+ msg, error = sc.sandboxes.messages.wait_for(box["id"], match=lambda m: m["subject"] == "Hi")
202
+ ```
203
+
204
+ ## Everything else
205
+
206
+ | Resource | Methods |
207
+ |---|---|
208
+ | `emails` | `send`, `get`, `list`, `iterate`, `stats` |
209
+ | `batch` | `send` |
210
+ | `domains` | `list`, `create`, `records`, `verify`, `enable_receiving`, `disable_receiving`, `set_region`, `deletion_preview`, `remove` |
211
+ | `templates` | `list`, `get`, `html`, `create`, `update`, `remove`, `duplicate`, `publish`, `unpublish`, `preview`, `set_tags`, `projects` |
212
+ | `templates.tags` | `list`, `create`, `update`, `remove` |
213
+ | `templates.assets` | `list`, `upload` |
214
+ | `sandboxes` | `list`, `get`, `create`, `update`, `remove`, `credentials`, `reset_credentials`, `send` |
215
+ | `sandboxes.messages` | `list`, `get`, `raw`, `remove`, `clear`, `mark_all_read`, `wait_for` |
216
+ | `webhooks` | `list`, `create`, `update`, `remove`, `rotate_secret`, `deliveries`, `verify` |
217
+ | `webhooks.dispatches` | `list`, `iterate`, `get`, `replay`, `replay_all` |
218
+ | `contacts` | `list`, `iterate`, `create`, `import_`, `import_preview`, `export` |
219
+ | `audiences` | `list` |
220
+ | `suppressions` | `list`, `iterate`, `create`, `remove`, `export` |
221
+ | `mailboxes` | `list`, `get`, `create`, `update`, `archive` |
222
+ | `mailboxes.messages` | `list`, `iterate`, `get`, `attachment` |
223
+ | `automations` | `enroll` |
224
+ | `workspace` | `get` |
225
+ | `api_keys` | `list` |
226
+
227
+ `contacts.import_` has a trailing underscore because `import` is a Python keyword. Management routes need a key with **Full access**. A sending-only key can call `emails.send` and `batch.send`.
228
+
229
+ Full API reference: <https://sendclipper.com/docs>.
230
+
231
+ ## License
232
+
233
+ MIT
@@ -0,0 +1,204 @@
1
+ # SendClipper Python SDK
2
+
3
+ The official Python library for the [SendClipper](https://sendclipper.com) email API.
4
+
5
+ ```bash
6
+ pip install sendclipper
7
+ # or
8
+ uv add sendclipper
9
+ # or
10
+ poetry add sendclipper
11
+ ```
12
+
13
+ Python 3.9 or newer. No runtime dependencies. Fully typed.
14
+
15
+ ## Setup
16
+
17
+ Create an API key in the dashboard under **API keys**.
18
+
19
+ ```python
20
+ from sendclipper import SendClipper
21
+
22
+ sc = SendClipper("sc_live_xxxxxxxxxxxxxxxx")
23
+ # or: SendClipper() reads SENDCLIPPER_API_KEY
24
+ ```
25
+
26
+ ## Send an email
27
+
28
+ ```python
29
+ data, error = sc.emails.send({
30
+ "from": "hello@send.acme.io",
31
+ "to": "ada@lumen.io",
32
+ "subject": "Your receipt",
33
+ "html": "<p>Thanks for your order.</p>",
34
+ "text": "Thanks for your order.",
35
+ })
36
+
37
+ if error:
38
+ print(error.name, error.message) # e.g. domain_not_verified
39
+ else:
40
+ print(f"Email {data['id']} accepted")
41
+ ```
42
+
43
+ Parameters and responses are plain dicts whose keys are the API's own field names (`templateId`, `sendClass`, ...), exactly as the [API reference](https://sendclipper.com/docs) writes them. Your editor still completes them: every one is a `TypedDict` in `sendclipper.types`.
44
+
45
+ `from` must be a bare address on a domain you verified in SendClipper. Each message has one recipient in `to`; for several, use `batch.send`.
46
+
47
+ ## Send with a template
48
+
49
+ Design and publish a template in the dashboard, then send it by id or by its send name:
50
+
51
+ ```python
52
+ sc.emails.send({
53
+ "from": "hello@send.acme.io",
54
+ "to": "ada@lumen.io",
55
+ "templateId": "tpl_9f2b7c10ad42", # or "template": "welcome"
56
+ "variables": {"first_name": "Ada"},
57
+ })
58
+ ```
59
+
60
+ A tag with no value and no `{{tag|fallback}}` is refused by name (`template_variables_missing`), never sent blank.
61
+
62
+ ## Send a batch
63
+
64
+ Up to 500 messages in one request. Each item is accepted or refused on its own:
65
+
66
+ ```python
67
+ data, error = sc.batch.send([
68
+ {"from": "hello@send.acme.io", "to": "ada@lumen.io", "subject": "Hi Ada", "html": "<p>Hi</p>"},
69
+ {"from": "hello@send.acme.io", "to": "alan@lumen.io", "templateId": "tpl_9f2b7c10ad42", "variables": {"first_name": "Alan"}},
70
+ ])
71
+
72
+ for item in data["data"]:
73
+ if item["status"] == "failed":
74
+ print(item["index"], item["error"]["code"])
75
+ ```
76
+
77
+ ## Errors
78
+
79
+ API methods never raise for an API refusal or a network failure. They return a `Result` with `data`, `error` and `headers`, and exactly one of `data` and `error` is set. A `Result` unpacks to `(data, error)`:
80
+
81
+ ```python
82
+ result = sc.domains.list()
83
+ result.data # the parsed response, or None
84
+ result.error # an ErrorResponse, or None
85
+ result.headers # response headers (lower-case names), or None when no answer arrived
86
+ result.ok # error is None
87
+
88
+ domains = sc.domains.list().unwrap() # data, or raises SendClipperApiError
89
+ ```
90
+
91
+ ```python
92
+ @dataclass(frozen=True)
93
+ class ErrorResponse:
94
+ name: str # SendClipper's code: "validation_failed", "rate_limited", "network_error", ...
95
+ message: str # a sentence for a person
96
+ status_code: Optional[int] # None when no answer arrived (network, timeout)
97
+ detail: Any = None # code-specific facts
98
+ ```
99
+
100
+ The iterators (`emails.iterate()` and friends) raise `SendClipperApiError` instead, since a loop has no `error` to hand back. `err.code` is the API's code and `err.status` the HTTP status.
101
+
102
+ ## Retries and idempotency
103
+
104
+ Network errors, `429` and `5xx` answers are retried twice with backoff, honouring `Retry-After`. Every JSON write carries an `Idempotency-Key`, and the API answers a repeated key with the first result for 24 hours. A retried send therefore never goes out twice.
105
+
106
+ To make your own retries safe as well (a job that runs again after a crash), pass a key that names the logical operation:
107
+
108
+ ```python
109
+ sc.emails.send(params, idempotency_key=f"order-{order.id}-receipt")
110
+ ```
111
+
112
+ ## Request options
113
+
114
+ Every method takes request options as keyword arguments after its own parameters:
115
+
116
+ ```python
117
+ sc.emails.list({"status": "bounced"}, timeout=5, headers={"X-Trace-Id": trace_id})
118
+ ```
119
+
120
+ | Option | Meaning |
121
+ |---|---|
122
+ | `idempotency_key` | Your own key for a write |
123
+ | `timeout` | Per-attempt timeout in seconds (default 30) |
124
+ | `max_retries` | Retries for this call (default 2) |
125
+ | `headers` | Extra request headers |
126
+
127
+ Client options: `SendClipper(api_key, base_url=..., timeout=..., max_retries=..., default_headers=..., transport=...)`. `base_url` also reads `SENDCLIPPER_BASE_URL`. `transport` replaces the standard-library HTTP sender, e.g. to go through a proxy library: it receives a `TransportRequest` and returns a `TransportResponse`.
128
+
129
+ ## Verify webhooks
130
+
131
+ SendClipper signs each delivery. Verify it against the **raw** body, before parsing:
132
+
133
+ ```python
134
+ import os
135
+ from flask import Flask, request
136
+ from sendclipper import verify_webhook, WebhookVerificationError
137
+
138
+ app = Flask(__name__)
139
+
140
+ @app.post("/hooks/sendclipper")
141
+ def sendclipper_hook():
142
+ try:
143
+ event = verify_webhook(
144
+ request.get_data(), # raw bytes, not request.json
145
+ request.headers,
146
+ os.environ["SENDCLIPPER_WEBHOOK_SECRET"],
147
+ )
148
+ except WebhookVerificationError as err:
149
+ return err.reason, 400
150
+ # event["id"] is stable across retries: deduplicate on it
151
+ print(event["event"], event["data"]["to"])
152
+ return "", 200
153
+ ```
154
+
155
+ The same call works in Django (`request.body`, `request.headers`) and FastAPI (`await request.body()`, `request.headers`). During a secret rotation a delivery carries two signatures and either one is accepted. A delivery signed more than five minutes away from now is refused. `sc.webhooks.verify(...)` is the same function.
156
+
157
+ To test your handler locally, `signature_header(secret, raw_body)` builds a valid header.
158
+
159
+ ## Sandboxes
160
+
161
+ A sandbox traps mail while you develop: nothing is delivered.
162
+
163
+ ```python
164
+ box = sc.sandboxes.create({"name": "my-app-dev"}).unwrap()
165
+ creds = sc.sandboxes.credentials(box["id"]).unwrap() # SMTP host, port, username, password
166
+
167
+ # Or send through the sandbox's JSON door with its own token, using the same fields as emails.send():
168
+ token = sc.sandboxes.reset_credentials(box["id"]).unwrap()
169
+ sc.sandboxes.send(token["apiToken"], {"from": "app@example.com", "to": "test@example.com", "subject": "Hi", "html": "<p>Hi</p>"})
170
+
171
+ # In a test: wait for the message to arrive
172
+ msg, error = sc.sandboxes.messages.wait_for(box["id"], match=lambda m: m["subject"] == "Hi")
173
+ ```
174
+
175
+ ## Everything else
176
+
177
+ | Resource | Methods |
178
+ |---|---|
179
+ | `emails` | `send`, `get`, `list`, `iterate`, `stats` |
180
+ | `batch` | `send` |
181
+ | `domains` | `list`, `create`, `records`, `verify`, `enable_receiving`, `disable_receiving`, `set_region`, `deletion_preview`, `remove` |
182
+ | `templates` | `list`, `get`, `html`, `create`, `update`, `remove`, `duplicate`, `publish`, `unpublish`, `preview`, `set_tags`, `projects` |
183
+ | `templates.tags` | `list`, `create`, `update`, `remove` |
184
+ | `templates.assets` | `list`, `upload` |
185
+ | `sandboxes` | `list`, `get`, `create`, `update`, `remove`, `credentials`, `reset_credentials`, `send` |
186
+ | `sandboxes.messages` | `list`, `get`, `raw`, `remove`, `clear`, `mark_all_read`, `wait_for` |
187
+ | `webhooks` | `list`, `create`, `update`, `remove`, `rotate_secret`, `deliveries`, `verify` |
188
+ | `webhooks.dispatches` | `list`, `iterate`, `get`, `replay`, `replay_all` |
189
+ | `contacts` | `list`, `iterate`, `create`, `import_`, `import_preview`, `export` |
190
+ | `audiences` | `list` |
191
+ | `suppressions` | `list`, `iterate`, `create`, `remove`, `export` |
192
+ | `mailboxes` | `list`, `get`, `create`, `update`, `archive` |
193
+ | `mailboxes.messages` | `list`, `iterate`, `get`, `attachment` |
194
+ | `automations` | `enroll` |
195
+ | `workspace` | `get` |
196
+ | `api_keys` | `list` |
197
+
198
+ `contacts.import_` has a trailing underscore because `import` is a Python keyword. Management routes need a key with **Full access**. A sending-only key can call `emails.send` and `batch.send`.
199
+
200
+ Full API reference: <https://sendclipper.com/docs>.
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sendclipper"
7
+ dynamic = ["version"]
8
+ description = "Official Python SDK for the SendClipper email API: send mail, manage domains, templates, sandboxes, webhooks, contacts and suppressions."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "SendClipper", email = "support@sendclipper.com" }]
13
+ requires-python = ">=3.9"
14
+ dependencies = []
15
+ keywords = ["sendclipper", "email", "transactional-email", "smtp", "webhooks", "sdk"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Communications :: Email",
29
+ "Typing :: Typed",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://sendclipper.com"
34
+ Documentation = "https://sendclipper.com/docs#sdk-python"
35
+ Source = "https://github.com/sendclipper/sendclipper-python"
36
+ Issues = "https://github.com/sendclipper/sendclipper-python/issues"
37
+ Changelog = "https://github.com/sendclipper/sendclipper-python/blob/main/CHANGELOG.md"
38
+
39
+ [tool.hatch.version]
40
+ path = "src/sendclipper/_version.py"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/sendclipper"]
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = ["src/sendclipper", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
47
+ exclude = ["tests/live"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ addopts = "--ignore=tests/live --ignore=tests/wheel"
52
+
53
+ [tool.mypy]
54
+ strict = true
55
+ files = ["src/sendclipper"]
@@ -0,0 +1,65 @@
1
+ """
2
+ The official Python SDK for the SendClipper email API.
3
+
4
+ ::
5
+
6
+ from sendclipper import SendClipper
7
+
8
+ sc = SendClipper("sc_live_...")
9
+ data, error = sc.emails.send({
10
+ "from": "hello@send.acme.io",
11
+ "to": "ada@lumen.io",
12
+ "subject": "Your receipt",
13
+ "html": "<p>Thanks for your order.</p>",
14
+ })
15
+ """
16
+
17
+ from ._client import SendClipper
18
+ from ._core import (
19
+ DEFAULT_BASE_URL,
20
+ DownloadedFile,
21
+ ErrorResponse,
22
+ RequestOptions,
23
+ Result,
24
+ Transport,
25
+ TransportRequest,
26
+ TransportResponse,
27
+ urllib_transport,
28
+ )
29
+ from ._errors import (
30
+ SendClipperApiError,
31
+ SendClipperConnectionError,
32
+ SendClipperError,
33
+ WebhookVerificationError,
34
+ )
35
+ from ._version import __version__
36
+ from ._webhooks import (
37
+ WEBHOOK_TOLERANCE_SECONDS,
38
+ parse_signature_header,
39
+ sign_payload,
40
+ signature_header,
41
+ verify_webhook,
42
+ )
43
+
44
+ __all__ = [
45
+ "SendClipper",
46
+ "DEFAULT_BASE_URL",
47
+ "DownloadedFile",
48
+ "ErrorResponse",
49
+ "RequestOptions",
50
+ "Result",
51
+ "Transport",
52
+ "TransportRequest",
53
+ "TransportResponse",
54
+ "urllib_transport",
55
+ "SendClipperError",
56
+ "SendClipperApiError",
57
+ "SendClipperConnectionError",
58
+ "WebhookVerificationError",
59
+ "verify_webhook",
60
+ "sign_payload",
61
+ "signature_header",
62
+ "parse_signature_header",
63
+ "WEBHOOK_TOLERANCE_SECONDS",
64
+ "__version__",
65
+ ]
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Mapping, Optional
4
+
5
+ from ._core import HttpClient, Transport
6
+ from .resources.audience import Audiences, Contacts, Suppressions
7
+ from .resources.domains import Domains
8
+ from .resources.emails import Batch, Emails
9
+ from .resources.mailboxes import Mailboxes
10
+ from .resources.sandboxes import Sandboxes
11
+ from .resources.templates import Templates
12
+ from .resources.webhooks import Webhooks
13
+ from .resources.workspace import ApiKeys, Automations, WorkspaceResource
14
+
15
+ __all__ = ["SendClipper"]
16
+
17
+
18
+ class SendClipper:
19
+ """
20
+ The SendClipper API client.
21
+
22
+ ::
23
+
24
+ from sendclipper import SendClipper
25
+
26
+ sc = SendClipper("sc_live_...") # or SendClipper(): reads SENDCLIPPER_API_KEY
27
+ data, error = sc.emails.send({
28
+ "from": "hello@acme.io",
29
+ "to": "ada@lumen.io",
30
+ "subject": "Hi",
31
+ "html": "<p>Hi</p>",
32
+ })
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ api_key: Optional[str] = None,
38
+ *,
39
+ base_url: Optional[str] = None,
40
+ timeout: float = 30.0,
41
+ max_retries: int = 2,
42
+ default_headers: Optional[Mapping[str, str]] = None,
43
+ transport: Optional[Transport] = None,
44
+ ) -> None:
45
+ """
46
+ :param api_key: A ``sc_live_...`` key. Defaults to ``SENDCLIPPER_API_KEY``.
47
+ :param base_url: Defaults to ``SENDCLIPPER_BASE_URL``, then ``https://sendclipper.com``. No ``/v1``.
48
+ :param timeout: Per attempt, in seconds.
49
+ :param max_retries: Retries after a network error, a 429 or a 5xx. A write is
50
+ only retried with an ``Idempotency-Key``, which the SDK adds to every JSON
51
+ write, so a retried send can never go out twice.
52
+ :param default_headers: Sent on every request.
53
+ :param transport: Your own HTTP sender (a proxy, a test double). Defaults to the standard library.
54
+ """
55
+ http = HttpClient(
56
+ api_key,
57
+ base_url=base_url,
58
+ timeout=timeout,
59
+ max_retries=max_retries,
60
+ default_headers=default_headers,
61
+ transport=transport,
62
+ )
63
+ self._http = http
64
+ #: The API origin requests go to, without ``/v1``.
65
+ self.base_url = http.base_url
66
+ self.emails = Emails(http)
67
+ self.batch = Batch(http)
68
+ self.domains = Domains(http)
69
+ self.templates = Templates(http)
70
+ self.sandboxes = Sandboxes(http)
71
+ self.webhooks = Webhooks(http)
72
+ self.contacts = Contacts(http)
73
+ self.audiences = Audiences(http)
74
+ self.suppressions = Suppressions(http)
75
+ self.mailboxes = Mailboxes(http)
76
+ self.automations = Automations(http)
77
+ self.workspace = WorkspaceResource(http)
78
+ self.api_keys = ApiKeys(http)
79
+
80
+ def __repr__(self) -> str:
81
+ return f"SendClipper(base_url={self.base_url!r})"