broadcast-python 0.1.0__tar.gz → 0.3.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.
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/CHANGELOG.md +40 -1
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/PKG-INFO +59 -13
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/README.md +57 -11
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/pyproject.toml +1 -1
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/client.py +18 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/base.py +3 -0
- broadcast_python-0.3.0/src/broadcast_python/resources/channel_design.py +24 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/discovery.py +14 -0
- broadcast_python-0.3.0/src/broadcast_python/resources/global_suppressions.py +40 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/migration.py +2 -1
- broadcast_python-0.3.0/src/broadcast_python/resources/suppressions.py +52 -0
- broadcast_python-0.3.0/src/broadcast_python/resources/users.py +115 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/version.py +1 -1
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/webhook.py +3 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/.gitignore +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/LICENSE +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/__init__.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/configuration.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/connection.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/errors.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/py.typed +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/__init__.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/autopilots.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/broadcasts.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/email_servers.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/opt_in_forms.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/segments.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/sequences.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/subscribers.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/templates.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/transactionals.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/webhook_endpoints.py +0 -0
- {broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/response.py +0 -0
|
@@ -2,7 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-10-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `client.migration.unsubscribed_emails()` for
|
|
12
|
+
`GET /api/migration/v1/unsubscribed_emails`, the channel's own suppression
|
|
13
|
+
list; `each_record("unsubscribed_emails")` pages it. `suppressions` returns
|
|
14
|
+
only the global suppression list, so an export needs both.
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2026-09-25
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `subscribers.purged` and `subscribers.purge_failed` webhook event types, in
|
|
20
|
+
`SUBSCRIBER_EVENTS` and `EVENT_TYPES` (now 34). A purge of the whole list
|
|
21
|
+
sends one of these instead of a `subscriber.deleted` per subscriber.
|
|
22
|
+
- `client.channel_design.get()` for `GET /api/v1/channel/design`: the token
|
|
23
|
+
channel's brand kit (colours, font and font stack, layout, logo URL, website
|
|
24
|
+
and social links), fully resolved and read-only. Needs `templates_read`.
|
|
25
|
+
- `client.users` resource for the admin-only Users API: `list`, `get`,
|
|
26
|
+
`create`, `update`, `deactivate`, `activate`, `delete`,
|
|
27
|
+
`channel_permissions`, `set_channel_permissions` (PUT, replaces the whole
|
|
28
|
+
channel record; requires exactly one of `permissions`/`role`/`preset_id`),
|
|
29
|
+
`remove_channel_permissions`, `bulk_channel_permissions`,
|
|
30
|
+
`system_permissions`, `update_system_permissions`. Requires an admin API
|
|
31
|
+
token; sudo users are read-only and sudo access can never be granted
|
|
32
|
+
through the API. Adds `BaseResource._put`.
|
|
33
|
+
|
|
34
|
+
## [0.1.0] - 2026-07-28
|
|
35
|
+
|
|
36
|
+
Published to PyPI as `broadcast-python`; the import module is `broadcast_python`,
|
|
37
|
+
because a package named `broadcast` already occupies that name on PyPI.
|
|
38
|
+
|
|
39
|
+
Released through PyPI trusted publishing (OIDC) rather than an API token — no
|
|
40
|
+
credential for this package exists anywhere. Verified from the registry: the
|
|
41
|
+
installed artifact imports, ships `py.typed`, exposes all 18 migration
|
|
42
|
+
collections and 32 event types, and computes a webhook signature identical to
|
|
43
|
+
the Ruby, PHP and Node SDKs.
|
|
44
|
+
|
|
6
45
|
|
|
7
46
|
First release. Feature parity with `broadcast-ruby` v0.3.0 — the reference
|
|
8
47
|
implementation — verified at **104/104 API operations** by the coverage report
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: broadcast-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python client for the Broadcast email platform. Subscribers, sequences, broadcasts, segments, templates, autopilot, webhooks, and transactional email.
|
|
5
5
|
Project-URL: Homepage, https://sendbroadcast.net
|
|
6
6
|
Project-URL: Repository, https://github.com/send-broadcast/broadcast-python
|
|
@@ -26,20 +26,12 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
Official Python client for [Broadcast](https://sendbroadcast.net), the self-hosted email marketing platform.
|
|
28
28
|
|
|
29
|
-
Works with any Broadcast instance — self-hosted or SaaS. Covers **
|
|
29
|
+
Works with any Broadcast instance — self-hosted or SaaS. Covers **117/117 API operations**, verified against the API's generated OpenAPI document.
|
|
30
30
|
|
|
31
31
|
📖 **[Python SDK documentation](https://sendbroadcast.net/docs/python-sdk)** · [API reference](https://sendbroadcast.net/docs/api-authentication) · [All docs](https://sendbroadcast.net/docs)
|
|
32
32
|
|
|
33
33
|
Also available: [Ruby](https://github.com/send-broadcast/broadcast-ruby) · [PHP](https://github.com/send-broadcast/broadcast-php) · [Node/TypeScript](https://github.com/send-broadcast/broadcast-node)
|
|
34
34
|
|
|
35
|
-
> **Not yet on PyPI.** The package is complete and tested but unpublished, so
|
|
36
|
-
> `pip install broadcast-python` will not resolve. Install from the repository
|
|
37
|
-
> until it lands:
|
|
38
|
-
>
|
|
39
|
-
> ```bash
|
|
40
|
-
> pip install git+https://github.com/send-broadcast/broadcast-python
|
|
41
|
-
> ```
|
|
42
|
-
|
|
43
35
|
## Installation
|
|
44
36
|
|
|
45
37
|
```bash
|
|
@@ -254,6 +246,19 @@ client.opt_in_forms.duplicate(id, label="Copy")
|
|
|
254
246
|
|
|
255
247
|
Reading a segment recounts its members server-side, so `segments.get` is not free.
|
|
256
248
|
|
|
249
|
+
### Channel design (brand kit)
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
kit = client.channel_design.get()
|
|
253
|
+
kit["colors"]["accent"] # "#2563eb"
|
|
254
|
+
kit["typography"]["font_stack"] # email-safe CSS font stack
|
|
255
|
+
kit["brand"]["logo_url"] # public URL, or None
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Read-only, for the token's own channel, fully resolved (defaults filled in).
|
|
259
|
+
Needs `templates_read`. Block emails built with the drag-and-drop editor inherit
|
|
260
|
+
these values.
|
|
261
|
+
|
|
257
262
|
### Email servers
|
|
258
263
|
|
|
259
264
|
**Credential redaction guard.** The API returns credentials bullet-masked
|
|
@@ -268,6 +273,39 @@ client.email_servers.update(id, name="Renamed", smtp_password=server["smtp_passw
|
|
|
268
273
|
# -> sends only {"name": "Renamed"}, warns about the dropped field
|
|
269
274
|
```
|
|
270
275
|
|
|
276
|
+
### Users
|
|
277
|
+
|
|
278
|
+
Requires an **admin API token**. Sudo users are read-only through this API —
|
|
279
|
+
update, deactivate, activate, delete, and permission writes on one raise
|
|
280
|
+
`AuthorizationError`. Sudo access itself can never be granted through the API.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
client.users.list(status="active", q="ada")
|
|
284
|
+
user = client.users.create(email="ada@example.com", first_name="Ada", last_name="L", password="x")
|
|
285
|
+
client.users.update(user["id"], first_name="Grace")
|
|
286
|
+
client.users.deactivate(user["id"])
|
|
287
|
+
client.users.activate(user["id"])
|
|
288
|
+
client.users.delete(user["id"])
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Channel and system permissions:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
client.users.channel_permissions(user["id"])
|
|
295
|
+
|
|
296
|
+
# PUT replaces the whole channel record -- pass exactly one of
|
|
297
|
+
# permissions, role, or preset_id
|
|
298
|
+
client.users.set_channel_permissions(user["id"], channel_id, role="Editor")
|
|
299
|
+
client.users.set_channel_permissions(user["id"], channel_id, permissions={"subscribers_read": True})
|
|
300
|
+
client.users.remove_channel_permissions(user["id"], channel_id)
|
|
301
|
+
|
|
302
|
+
# Same rule, applied to several channels at once
|
|
303
|
+
client.users.bulk_channel_permissions(user["id"], [channel_id, other_channel_id], role="Viewer")
|
|
304
|
+
|
|
305
|
+
client.users.system_permissions(user["id"])
|
|
306
|
+
client.users.update_system_permissions(user["id"], {"user_management": True})
|
|
307
|
+
```
|
|
308
|
+
|
|
271
309
|
### Autopilot
|
|
272
310
|
|
|
273
311
|
```python
|
|
@@ -325,6 +363,11 @@ client.migration.manifest()
|
|
|
325
363
|
for sub in client.migration.each_record("subscribers"):
|
|
326
364
|
... # auto-pages; advances by the limit the server actually applied
|
|
327
365
|
|
|
366
|
+
# Suppressed addresses live in two lists; export both:
|
|
367
|
+
# suppressions = global suppression list, unsubscribed_emails = the channel's own
|
|
368
|
+
for row in client.migration.each_record("unsubscribed_emails"):
|
|
369
|
+
... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
|
|
370
|
+
|
|
328
371
|
data = client.migration.download_file_asset(id) # bytes
|
|
329
372
|
```
|
|
330
373
|
|
|
@@ -370,7 +413,9 @@ every rejection rather than distinguishing them.
|
|
|
370
413
|
Pass the **raw** request body. Re-serialising a parsed dict changes the bytes
|
|
371
414
|
and verification will fail.
|
|
372
415
|
|
|
373
|
-
`broadcast_python.EVENT_TYPES` lists all
|
|
416
|
+
`broadcast_python.EVENT_TYPES` lists all 34 event names. `SUBSCRIBER_EVENTS`
|
|
417
|
+
includes `subscribers.purged` and `subscribers.purge_failed`: a purge of the
|
|
418
|
+
whole list sends one of these instead of a `subscriber.deleted` per subscriber.
|
|
374
419
|
|
|
375
420
|
---
|
|
376
421
|
|
|
@@ -386,11 +431,12 @@ integration requires.
|
|
|
386
431
|
| Sequences | `sequences_read` -- list, get, list steps | `sequences_write` -- create, update, delete, manage steps, enroll subscribers |
|
|
387
432
|
| Broadcasts | `broadcasts_read` -- list, get, statistics | `broadcasts_write` -- create, update, delete, send, schedule |
|
|
388
433
|
| Segments | `segments_read` -- list, get | `segments_write` -- create, update, delete |
|
|
389
|
-
| Templates | `templates_read` -- list, get | `templates_write` -- create, update, delete |
|
|
434
|
+
| Templates | `templates_read` -- list, get, channel_design.get | `templates_write` -- create, update, delete |
|
|
390
435
|
| Opt-In Forms | `opt_in_forms_read` -- list, get, analytics | `opt_in_forms_write` -- create, update, delete, create_variant, duplicate |
|
|
391
436
|
| Email Servers | `email_servers_read` -- list, get | `email_servers_write` -- create, update, delete, test_connection, copy_to_channel (admin) |
|
|
392
437
|
| Webhook Endpoints | `webhook_endpoints_read` -- list, get, deliveries | `webhook_endpoints_write` -- create, update, delete, test |
|
|
393
438
|
| Autopilot | `autopilot_read` -- list, get, runs | `autopilot_write` -- create, update, delete, activate, pause, deactivate, trigger_run |
|
|
439
|
+
| Users (admin token only) | `users_read` -- list, get, channel_permissions, system_permissions | `users_write` -- create, update, deactivate, activate, delete, permission writes |
|
|
394
440
|
|
|
395
441
|
---
|
|
396
442
|
|
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Official Python client for [Broadcast](https://sendbroadcast.net), the self-hosted email marketing platform.
|
|
4
4
|
|
|
5
|
-
Works with any Broadcast instance — self-hosted or SaaS. Covers **
|
|
5
|
+
Works with any Broadcast instance — self-hosted or SaaS. Covers **117/117 API operations**, verified against the API's generated OpenAPI document.
|
|
6
6
|
|
|
7
7
|
📖 **[Python SDK documentation](https://sendbroadcast.net/docs/python-sdk)** · [API reference](https://sendbroadcast.net/docs/api-authentication) · [All docs](https://sendbroadcast.net/docs)
|
|
8
8
|
|
|
9
9
|
Also available: [Ruby](https://github.com/send-broadcast/broadcast-ruby) · [PHP](https://github.com/send-broadcast/broadcast-php) · [Node/TypeScript](https://github.com/send-broadcast/broadcast-node)
|
|
10
10
|
|
|
11
|
-
> **Not yet on PyPI.** The package is complete and tested but unpublished, so
|
|
12
|
-
> `pip install broadcast-python` will not resolve. Install from the repository
|
|
13
|
-
> until it lands:
|
|
14
|
-
>
|
|
15
|
-
> ```bash
|
|
16
|
-
> pip install git+https://github.com/send-broadcast/broadcast-python
|
|
17
|
-
> ```
|
|
18
|
-
|
|
19
11
|
## Installation
|
|
20
12
|
|
|
21
13
|
```bash
|
|
@@ -230,6 +222,19 @@ client.opt_in_forms.duplicate(id, label="Copy")
|
|
|
230
222
|
|
|
231
223
|
Reading a segment recounts its members server-side, so `segments.get` is not free.
|
|
232
224
|
|
|
225
|
+
### Channel design (brand kit)
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
kit = client.channel_design.get()
|
|
229
|
+
kit["colors"]["accent"] # "#2563eb"
|
|
230
|
+
kit["typography"]["font_stack"] # email-safe CSS font stack
|
|
231
|
+
kit["brand"]["logo_url"] # public URL, or None
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Read-only, for the token's own channel, fully resolved (defaults filled in).
|
|
235
|
+
Needs `templates_read`. Block emails built with the drag-and-drop editor inherit
|
|
236
|
+
these values.
|
|
237
|
+
|
|
233
238
|
### Email servers
|
|
234
239
|
|
|
235
240
|
**Credential redaction guard.** The API returns credentials bullet-masked
|
|
@@ -244,6 +249,39 @@ client.email_servers.update(id, name="Renamed", smtp_password=server["smtp_passw
|
|
|
244
249
|
# -> sends only {"name": "Renamed"}, warns about the dropped field
|
|
245
250
|
```
|
|
246
251
|
|
|
252
|
+
### Users
|
|
253
|
+
|
|
254
|
+
Requires an **admin API token**. Sudo users are read-only through this API —
|
|
255
|
+
update, deactivate, activate, delete, and permission writes on one raise
|
|
256
|
+
`AuthorizationError`. Sudo access itself can never be granted through the API.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
client.users.list(status="active", q="ada")
|
|
260
|
+
user = client.users.create(email="ada@example.com", first_name="Ada", last_name="L", password="x")
|
|
261
|
+
client.users.update(user["id"], first_name="Grace")
|
|
262
|
+
client.users.deactivate(user["id"])
|
|
263
|
+
client.users.activate(user["id"])
|
|
264
|
+
client.users.delete(user["id"])
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Channel and system permissions:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
client.users.channel_permissions(user["id"])
|
|
271
|
+
|
|
272
|
+
# PUT replaces the whole channel record -- pass exactly one of
|
|
273
|
+
# permissions, role, or preset_id
|
|
274
|
+
client.users.set_channel_permissions(user["id"], channel_id, role="Editor")
|
|
275
|
+
client.users.set_channel_permissions(user["id"], channel_id, permissions={"subscribers_read": True})
|
|
276
|
+
client.users.remove_channel_permissions(user["id"], channel_id)
|
|
277
|
+
|
|
278
|
+
# Same rule, applied to several channels at once
|
|
279
|
+
client.users.bulk_channel_permissions(user["id"], [channel_id, other_channel_id], role="Viewer")
|
|
280
|
+
|
|
281
|
+
client.users.system_permissions(user["id"])
|
|
282
|
+
client.users.update_system_permissions(user["id"], {"user_management": True})
|
|
283
|
+
```
|
|
284
|
+
|
|
247
285
|
### Autopilot
|
|
248
286
|
|
|
249
287
|
```python
|
|
@@ -301,6 +339,11 @@ client.migration.manifest()
|
|
|
301
339
|
for sub in client.migration.each_record("subscribers"):
|
|
302
340
|
... # auto-pages; advances by the limit the server actually applied
|
|
303
341
|
|
|
342
|
+
# Suppressed addresses live in two lists; export both:
|
|
343
|
+
# suppressions = global suppression list, unsubscribed_emails = the channel's own
|
|
344
|
+
for row in client.migration.each_record("unsubscribed_emails"):
|
|
345
|
+
... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
|
|
346
|
+
|
|
304
347
|
data = client.migration.download_file_asset(id) # bytes
|
|
305
348
|
```
|
|
306
349
|
|
|
@@ -346,7 +389,9 @@ every rejection rather than distinguishing them.
|
|
|
346
389
|
Pass the **raw** request body. Re-serialising a parsed dict changes the bytes
|
|
347
390
|
and verification will fail.
|
|
348
391
|
|
|
349
|
-
`broadcast_python.EVENT_TYPES` lists all
|
|
392
|
+
`broadcast_python.EVENT_TYPES` lists all 34 event names. `SUBSCRIBER_EVENTS`
|
|
393
|
+
includes `subscribers.purged` and `subscribers.purge_failed`: a purge of the
|
|
394
|
+
whole list sends one of these instead of a `subscriber.deleted` per subscriber.
|
|
350
395
|
|
|
351
396
|
---
|
|
352
397
|
|
|
@@ -362,11 +407,12 @@ integration requires.
|
|
|
362
407
|
| Sequences | `sequences_read` -- list, get, list steps | `sequences_write` -- create, update, delete, manage steps, enroll subscribers |
|
|
363
408
|
| Broadcasts | `broadcasts_read` -- list, get, statistics | `broadcasts_write` -- create, update, delete, send, schedule |
|
|
364
409
|
| Segments | `segments_read` -- list, get | `segments_write` -- create, update, delete |
|
|
365
|
-
| Templates | `templates_read` -- list, get | `templates_write` -- create, update, delete |
|
|
410
|
+
| Templates | `templates_read` -- list, get, channel_design.get | `templates_write` -- create, update, delete |
|
|
366
411
|
| Opt-In Forms | `opt_in_forms_read` -- list, get, analytics | `opt_in_forms_write` -- create, update, delete, create_variant, duplicate |
|
|
367
412
|
| Email Servers | `email_servers_read` -- list, get | `email_servers_write` -- create, update, delete, test_connection, copy_to_channel (admin) |
|
|
368
413
|
| Webhook Endpoints | `webhook_endpoints_read` -- list, get, deliveries | `webhook_endpoints_write` -- create, update, delete, test |
|
|
369
414
|
| Autopilot | `autopilot_read` -- list, get, runs | `autopilot_write` -- create, update, delete, activate, pause, deactivate, trigger_run |
|
|
415
|
+
| Users (admin token only) | `users_read` -- list, get, channel_permissions, system_permissions | `users_write` -- create, update, deactivate, activate, delete, permission writes |
|
|
370
416
|
|
|
371
417
|
---
|
|
372
418
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "broadcast-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python client for the Broadcast email platform. Subscribers, sequences, broadcasts, segments, templates, autopilot, webhooks, and transactional email."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -5,15 +5,19 @@ from .configuration import Configuration
|
|
|
5
5
|
from .connection import Connection
|
|
6
6
|
from .resources.autopilots import Autopilots
|
|
7
7
|
from .resources.broadcasts import Broadcasts
|
|
8
|
+
from .resources.channel_design import ChannelDesign
|
|
8
9
|
from .resources.discovery import Discovery
|
|
9
10
|
from .resources.email_servers import EmailServers
|
|
11
|
+
from .resources.global_suppressions import GlobalSuppressions
|
|
10
12
|
from .resources.migration import Migration
|
|
11
13
|
from .resources.opt_in_forms import OptInForms
|
|
12
14
|
from .resources.segments import Segments
|
|
13
15
|
from .resources.sequences import Sequences
|
|
14
16
|
from .resources.subscribers import Subscribers
|
|
17
|
+
from .resources.suppressions import Suppressions
|
|
15
18
|
from .resources.templates import Templates
|
|
16
19
|
from .resources.transactionals import Transactionals
|
|
20
|
+
from .resources.users import Users
|
|
17
21
|
from .resources.webhook_endpoints import WebhookEndpoints
|
|
18
22
|
|
|
19
23
|
Id = Union[str, int]
|
|
@@ -37,14 +41,25 @@ class Broadcast:
|
|
|
37
41
|
self.broadcasts = Broadcasts(self)
|
|
38
42
|
self.segments = Segments(self)
|
|
39
43
|
self.templates = Templates(self)
|
|
44
|
+
#: The channel's brand kit (Settings -> Design). Read-only.
|
|
45
|
+
self.channel_design = ChannelDesign(self)
|
|
40
46
|
self.webhook_endpoints = WebhookEndpoints(self)
|
|
41
47
|
self.transactionals = Transactionals(self)
|
|
42
48
|
self.opt_in_forms = OptInForms(self)
|
|
43
49
|
self.email_servers = EmailServers(self)
|
|
44
50
|
self.autopilots = Autopilots(self)
|
|
45
51
|
self.discovery = Discovery(self)
|
|
52
|
+
#: The current channel's suppression list (plus ``check``, which
|
|
53
|
+
#: reads the global list too).
|
|
54
|
+
self.suppressions = Suppressions(self)
|
|
55
|
+
#: The installation-wide suppression list. Requires an admin (system)
|
|
56
|
+
#: API token.
|
|
57
|
+
self.global_suppressions = GlobalSuppressions(self)
|
|
46
58
|
#: Read-only export endpoints. Requires an admin (system) API token.
|
|
47
59
|
self.migration = Migration(self)
|
|
60
|
+
#: Installation users and their permissions. Requires an admin
|
|
61
|
+
#: (system) API token; sudo users are read-only through this API.
|
|
62
|
+
self.users = Users(self)
|
|
48
63
|
|
|
49
64
|
# --- Channel scoping (admin/system tokens) ---
|
|
50
65
|
|
|
@@ -97,6 +112,9 @@ class Broadcast:
|
|
|
97
112
|
def skill(self) -> str:
|
|
98
113
|
return self.discovery.skill()
|
|
99
114
|
|
|
115
|
+
def openapi(self) -> str:
|
|
116
|
+
return self.discovery.openapi()
|
|
117
|
+
|
|
100
118
|
# --- Internal ---
|
|
101
119
|
|
|
102
120
|
def request(
|
|
@@ -25,6 +25,9 @@ class BaseResource:
|
|
|
25
25
|
def _patch(self, path: str, body: Optional[Dict[str, Any]] = None) -> Any:
|
|
26
26
|
return self._client.request("PATCH", path, body or {})
|
|
27
27
|
|
|
28
|
+
def _put(self, path: str, body: Optional[Dict[str, Any]] = None) -> Any:
|
|
29
|
+
return self._client.request("PUT", path, body or {})
|
|
30
|
+
|
|
28
31
|
def _delete(self, path: str, body: Optional[Dict[str, Any]] = None) -> Any:
|
|
29
32
|
return self._client.request("DELETE", path, body)
|
|
30
33
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .base import BaseResource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChannelDesign(BaseResource):
|
|
7
|
+
"""The brand kit of the token's channel (Settings -> Design). Read-only.
|
|
8
|
+
|
|
9
|
+
Requires the ``templates_read`` permission: the kit is design metadata for
|
|
10
|
+
templates. There is no channel argument; the server reads the channel the
|
|
11
|
+
token resolves to (for an admin token, the one set with ``with_channel`` or
|
|
12
|
+
``broadcast_channel_id``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def get(self) -> Any:
|
|
16
|
+
"""The channel's brand kit, fully resolved (defaults filled in).
|
|
17
|
+
|
|
18
|
+
Returns ``colors``, ``typography`` (``font`` key and the email-safe
|
|
19
|
+
``font_stack``), ``layout`` (``width``, ``radius``), and ``brand``
|
|
20
|
+
(``logo_url`` as a public URL or None, ``logo_width``, ``website_url``,
|
|
21
|
+
``social_links``, ``social_icon_style``). Block emails built with the
|
|
22
|
+
drag-and-drop editor inherit these values.
|
|
23
|
+
"""
|
|
24
|
+
return self._get("/api/v1/channel/design")
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/discovery.py
RENAMED
|
@@ -33,3 +33,17 @@ class Discovery(BaseResource):
|
|
|
33
33
|
Returns a ``str``, not a dict — this endpoint serves ``text/plain``.
|
|
34
34
|
"""
|
|
35
35
|
return self._get("/api/v1/skill", raw=True)
|
|
36
|
+
|
|
37
|
+
def openapi(self) -> str:
|
|
38
|
+
"""This installation's own OpenAPI document, as YAML.
|
|
39
|
+
|
|
40
|
+
Returns a ``str``, not a dict — this endpoint serves
|
|
41
|
+
``application/yaml``.
|
|
42
|
+
|
|
43
|
+
The server URL inside the document is rewritten by the installation to
|
|
44
|
+
the host that served it, so the result feeds a client generator or an
|
|
45
|
+
API explorer without hand-editing. Preferable to a spec copied from
|
|
46
|
+
elsewhere: a 2.28 install serves the 2.28 surface, so the document
|
|
47
|
+
cannot drift from the routes it describes.
|
|
48
|
+
"""
|
|
49
|
+
return self._get("/api/v1/openapi", raw=True)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The installation-wide suppression list.
|
|
2
|
+
|
|
3
|
+
Addresses on it never receive mail from any channel. All operations require an
|
|
4
|
+
admin (system) API token — a channel token gets a 401.
|
|
5
|
+
|
|
6
|
+
There is deliberately no ``check`` here: checking is a per-channel question
|
|
7
|
+
(it reads the channel list too), so it lives on ``suppressions``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, List
|
|
11
|
+
|
|
12
|
+
from .base import BaseResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlobalSuppressions(BaseResource):
|
|
16
|
+
def list(self, **params: Any) -> Any:
|
|
17
|
+
"""List global suppressions (250 per page, with ``pagination``
|
|
18
|
+
metadata; pass ``page``). Optional ``email`` filters by partial
|
|
19
|
+
match."""
|
|
20
|
+
return self._get("/api/v1/global_suppressions.json", params)
|
|
21
|
+
|
|
22
|
+
def add(self, email: str) -> Any:
|
|
23
|
+
"""Add an address to the global list. Already-suppressed is a success
|
|
24
|
+
(200 instead of 201)."""
|
|
25
|
+
return self._post("/api/v1/global_suppressions.json", {"email": email})
|
|
26
|
+
|
|
27
|
+
def remove(self, email: str) -> Any:
|
|
28
|
+
"""Remove an address from the global list only. Channels that
|
|
29
|
+
suppressed the same address on their own account keep their block."""
|
|
30
|
+
return self._delete("/api/v1/global_suppressions.json", {"email": email})
|
|
31
|
+
|
|
32
|
+
def bulk_add(self, emails: List[str]) -> Any:
|
|
33
|
+
"""Add up to 10,000 addresses at once. Idempotent. Returns ``added``,
|
|
34
|
+
``already_suppressed``, and ``invalid`` counts."""
|
|
35
|
+
return self._post("/api/v1/global_suppressions/bulk.json", {"emails": emails})
|
|
36
|
+
|
|
37
|
+
def bulk_remove(self, emails: List[str]) -> Any:
|
|
38
|
+
"""Remove up to 10,000 addresses at once. Returns ``removed`` and
|
|
39
|
+
``not_found`` counts."""
|
|
40
|
+
return self._delete("/api/v1/global_suppressions/bulk.json", {"emails": emails})
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/migration.py
RENAMED
|
@@ -35,6 +35,7 @@ COLLECTIONS = (
|
|
|
35
35
|
"webhook_endpoints",
|
|
36
36
|
"tokens",
|
|
37
37
|
"suppressions",
|
|
38
|
+
"unsubscribed_emails",
|
|
38
39
|
"tags",
|
|
39
40
|
"users",
|
|
40
41
|
"link_redirects",
|
|
@@ -106,7 +107,7 @@ def _make_collection_method(name: str):
|
|
|
106
107
|
return method
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
# Generated rather than hand-written:
|
|
110
|
+
# Generated rather than hand-written: 19 near-identical methods invite the kind
|
|
110
111
|
# of copy-paste drift this whole SDK family exists to prevent. Declared in
|
|
111
112
|
# .api-coverage.yml so the coverage report still counts them.
|
|
112
113
|
for _collection in COLLECTIONS:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The current channel's suppression list.
|
|
2
|
+
|
|
3
|
+
Addresses on it never receive broadcasts, sequences, or transactionals from
|
|
4
|
+
this channel. The installation-wide list is a separate resource — see
|
|
5
|
+
``global_suppressions`` — but ``check`` reads across both on purpose: it
|
|
6
|
+
answers the question an integration actually asks, "will this address receive
|
|
7
|
+
mail?".
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, List
|
|
11
|
+
|
|
12
|
+
from .base import BaseResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Suppressions(BaseResource):
|
|
16
|
+
def list(self, **params: Any) -> Any:
|
|
17
|
+
"""List the channel's suppressions (250 per page, with ``pagination``
|
|
18
|
+
metadata; pass ``page``). Optional ``email`` filters by partial,
|
|
19
|
+
case-insensitive match."""
|
|
20
|
+
return self._get("/api/v1/suppressions.json", params)
|
|
21
|
+
|
|
22
|
+
def add(self, email: str) -> Any:
|
|
23
|
+
"""Add an address to the channel's suppression list.
|
|
24
|
+
|
|
25
|
+
Adding an address that is already suppressed is a success (the server
|
|
26
|
+
answers 200 instead of 201), so callers do not have to check first.
|
|
27
|
+
"""
|
|
28
|
+
return self._post("/api/v1/suppressions.json", {"email": email})
|
|
29
|
+
|
|
30
|
+
def remove(self, email: str) -> Any:
|
|
31
|
+
"""Remove an address from the channel's suppression list. Returns
|
|
32
|
+
``removed: False`` (not an error) when the address was not on it.
|
|
33
|
+
Does not touch the global list."""
|
|
34
|
+
return self._delete("/api/v1/suppressions.json", {"email": email})
|
|
35
|
+
|
|
36
|
+
def bulk_add(self, emails: List[str]) -> Any:
|
|
37
|
+
"""Add up to 10,000 addresses at once. Idempotent: a retried batch
|
|
38
|
+
cannot duplicate. Returns ``added``, ``already_suppressed``, and
|
|
39
|
+
``invalid`` counts."""
|
|
40
|
+
return self._post("/api/v1/suppressions/bulk.json", {"emails": emails})
|
|
41
|
+
|
|
42
|
+
def bulk_remove(self, emails: List[str]) -> Any:
|
|
43
|
+
"""Remove up to 10,000 addresses at once. Returns ``removed`` and
|
|
44
|
+
``not_found`` counts."""
|
|
45
|
+
return self._delete("/api/v1/suppressions/bulk.json", {"emails": emails})
|
|
46
|
+
|
|
47
|
+
def check(self, email: str) -> Any:
|
|
48
|
+
"""Will this address receive mail? Reads across both the global and
|
|
49
|
+
the channel list — a globally blocked address reports
|
|
50
|
+
``suppressed: True`` here even though it is absent from the channel's
|
|
51
|
+
own list. The response's ``scope`` says which list matched."""
|
|
52
|
+
return self._get("/api/v1/suppressions/check.json", {"email": email})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from .base import BaseResource, compact
|
|
4
|
+
|
|
5
|
+
Id = Union[str, int]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Users(BaseResource):
|
|
9
|
+
"""Manage installation users and their permissions.
|
|
10
|
+
|
|
11
|
+
Every operation here requires an **admin API token** — a channel token
|
|
12
|
+
gets ``403 {"error": "Admin API token required for user management"}``.
|
|
13
|
+
Reads need the ``users_read`` scope, writes need ``users_write``.
|
|
14
|
+
|
|
15
|
+
Sudo users are **read-only** through this API: update, deactivate,
|
|
16
|
+
activate, delete, and permission writes on a sudo user all raise
|
|
17
|
+
``AuthorizationError`` (403). Sudo access itself can never be granted
|
|
18
|
+
through the API, whether creating or updating a user or writing system
|
|
19
|
+
permissions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def list(
|
|
23
|
+
self,
|
|
24
|
+
limit: Optional[int] = None,
|
|
25
|
+
offset: Optional[int] = None,
|
|
26
|
+
q: Optional[str] = None,
|
|
27
|
+
status: Optional[str] = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""List users. ``q`` searches email/name, ``status`` is ``"active"``
|
|
30
|
+
or ``"inactive"``."""
|
|
31
|
+
params = compact({"limit": limit, "offset": offset, "q": q, "status": status})
|
|
32
|
+
return self._get("/api/v1/users", params)
|
|
33
|
+
|
|
34
|
+
def get(self, id: Id) -> Any: # noqa: A002
|
|
35
|
+
return self._get("/api/v1/users/{}".format(id))
|
|
36
|
+
|
|
37
|
+
def create(self, **attrs: Any) -> Any:
|
|
38
|
+
"""Create a user. Requires either ``password`` or
|
|
39
|
+
``send_password_reset=True``."""
|
|
40
|
+
return self._post("/api/v1/users", {"user": attrs})
|
|
41
|
+
|
|
42
|
+
def update(self, id: Id, **attrs: Any) -> Any: # noqa: A002
|
|
43
|
+
"""Update a user. Raises ``AuthorizationError`` if the user is sudo."""
|
|
44
|
+
return self._patch("/api/v1/users/{}".format(id), {"user": attrs})
|
|
45
|
+
|
|
46
|
+
def deactivate(self, id: Id) -> Any: # noqa: A002
|
|
47
|
+
return self._post("/api/v1/users/{}/deactivate".format(id))
|
|
48
|
+
|
|
49
|
+
def activate(self, id: Id) -> Any: # noqa: A002
|
|
50
|
+
"""Reactivate a user. Also clears any account lockout."""
|
|
51
|
+
return self._post("/api/v1/users/{}/activate".format(id))
|
|
52
|
+
|
|
53
|
+
def delete(self, id: Id) -> Any: # noqa: A002
|
|
54
|
+
return self._delete("/api/v1/users/{}".format(id))
|
|
55
|
+
|
|
56
|
+
def channel_permissions(self, id: Id) -> Any: # noqa: A002
|
|
57
|
+
return self._get("/api/v1/users/{}/channel_permissions".format(id))
|
|
58
|
+
|
|
59
|
+
def set_channel_permissions(
|
|
60
|
+
self,
|
|
61
|
+
id: Id, # noqa: A002
|
|
62
|
+
broadcast_channel_id: Id,
|
|
63
|
+
permissions: Optional[Dict[str, bool]] = None,
|
|
64
|
+
role: Optional[str] = None,
|
|
65
|
+
preset_id: Optional[Id] = None,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""Replace the user's whole permission record for one channel.
|
|
68
|
+
|
|
69
|
+
This is a PUT: it replaces the record rather than merging into it, so
|
|
70
|
+
passing ``permissions`` sets any flag not named to ``False``. Pass
|
|
71
|
+
exactly one of ``permissions``, ``role``, or ``preset_id``.
|
|
72
|
+
"""
|
|
73
|
+
body = _one_of(permissions=permissions, role=role, preset_id=preset_id)
|
|
74
|
+
return self._put("/api/v1/users/{}/channel_permissions/{}".format(id, broadcast_channel_id), body)
|
|
75
|
+
|
|
76
|
+
def remove_channel_permissions(self, id: Id, broadcast_channel_id: Id) -> Any: # noqa: A002
|
|
77
|
+
return self._delete("/api/v1/users/{}/channel_permissions/{}".format(id, broadcast_channel_id))
|
|
78
|
+
|
|
79
|
+
def bulk_channel_permissions(
|
|
80
|
+
self,
|
|
81
|
+
id: Id, # noqa: A002
|
|
82
|
+
broadcast_channel_ids: List[Id],
|
|
83
|
+
permissions: Optional[Dict[str, bool]] = None,
|
|
84
|
+
role: Optional[str] = None,
|
|
85
|
+
preset_id: Optional[Id] = None,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Apply the same permission record to several channels at once.
|
|
88
|
+
|
|
89
|
+
Pass exactly one of ``permissions``, ``role``, or ``preset_id``.
|
|
90
|
+
Returns ``{"applied": [...], "failed": [...]}`` — a failure on one
|
|
91
|
+
channel does not roll back the others.
|
|
92
|
+
"""
|
|
93
|
+
body = _one_of(permissions=permissions, role=role, preset_id=preset_id)
|
|
94
|
+
body["broadcast_channel_ids"] = broadcast_channel_ids
|
|
95
|
+
return self._post("/api/v1/users/{}/channel_permissions/bulk".format(id), body)
|
|
96
|
+
|
|
97
|
+
def system_permissions(self, id: Id) -> Any: # noqa: A002
|
|
98
|
+
return self._get("/api/v1/users/{}/system_permissions".format(id))
|
|
99
|
+
|
|
100
|
+
def update_system_permissions(self, id: Id, permissions: Dict[str, bool]) -> Any: # noqa: A002
|
|
101
|
+
"""Update only the named system permission flags.
|
|
102
|
+
|
|
103
|
+
``sudo_access`` is never present and sending it raises a 422 —
|
|
104
|
+
sudo can never be granted through the API.
|
|
105
|
+
"""
|
|
106
|
+
return self._patch("/api/v1/users/{}/system_permissions".format(id), {"permissions": permissions})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _one_of(**kwargs: Any) -> Dict[str, Any]:
|
|
110
|
+
given = {k: v for k, v in kwargs.items() if v is not None}
|
|
111
|
+
if len(given) != 1:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"Pass exactly one of {} (got {})".format(", ".join(kwargs.keys()), len(given))
|
|
114
|
+
)
|
|
115
|
+
return given
|
|
@@ -30,6 +30,9 @@ SUBSCRIBER_EVENTS = (
|
|
|
30
30
|
"subscriber.unsubscribed",
|
|
31
31
|
"subscriber.bounced",
|
|
32
32
|
"subscriber.complained",
|
|
33
|
+
# One event for a whole-list purge, in place of a subscriber.deleted per row
|
|
34
|
+
"subscribers.purged",
|
|
35
|
+
"subscribers.purge_failed",
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
BROADCAST_EVENTS = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/__init__.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/autopilots.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/broadcasts.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/email_servers.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/opt_in_forms.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/segments.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/sequences.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/subscribers.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/templates.py
RENAMED
|
File without changes
|
{broadcast_python-0.1.0 → broadcast_python-0.3.0}/src/broadcast_python/resources/transactionals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|