broadcast-python 0.7.0__tar.gz → 0.8.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.7.0 → broadcast_python-0.8.0}/CHANGELOG.md +9 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/PKG-INFO +1 -28
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/README.md +0 -27
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/pyproject.toml +1 -1
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/__init__.py +0 -2
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/client.py +0 -2
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/version.py +1 -1
- broadcast_python-0.7.0/src/broadcast_python/resources/migration.py +0 -114
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/.gitignore +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/LICENSE +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/configuration.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/connection.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/errors.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/multipart.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/py.typed +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/__init__.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/autopilots.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/base.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/broadcasts.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_design.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_settings.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/discovery.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/email_servers.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/file_assets.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/global_suppressions.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/opt_in_forms.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/segments.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/sequences.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/subscribers.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/suppressions.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/templates.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/topics.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/transactionals.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/users.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/webhook_endpoints.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/response.py +0 -0
- {broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/webhook.py +0 -0
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- **Breaking:** `client.migration` and the `COLLECTIONS` export: `manifest`,
|
|
12
|
+
the 19 export collections, `each_record` and `download_file_asset`.
|
|
13
|
+
Broadcast 3 removes the `/api/migration/v1` export API. A server before 3.0
|
|
14
|
+
still serves it; stay on 0.7.x to keep using it.
|
|
15
|
+
|
|
7
16
|
## [0.7.0] - 2026-10-17
|
|
8
17
|
|
|
9
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: broadcast-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -465,30 +465,6 @@ client.prime() # full capability manifest
|
|
|
465
465
|
client.skill() # plain-text agent skill manifest (a str)
|
|
466
466
|
```
|
|
467
467
|
|
|
468
|
-
### Migration / export
|
|
469
|
-
|
|
470
|
-
Read-only. **Admin tokens only**, and every call needs a `broadcast_channel_id`.
|
|
471
|
-
|
|
472
|
-
```python
|
|
473
|
-
client = Broadcast(..., broadcast_channel_id=42)
|
|
474
|
-
|
|
475
|
-
client.migration.manifest()
|
|
476
|
-
|
|
477
|
-
for sub in client.migration.each_record("subscribers"):
|
|
478
|
-
... # auto-pages; advances by the limit the server actually applied
|
|
479
|
-
|
|
480
|
-
# Suppressed addresses live in two lists; export both:
|
|
481
|
-
# suppressions = global suppression list, unsubscribed_emails = the channel's own
|
|
482
|
-
for row in client.migration.each_record("unsubscribed_emails"):
|
|
483
|
-
... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
|
|
484
|
-
|
|
485
|
-
data = client.migration.download_file_asset(id) # bytes
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
On a demo instance this entire API returns **403 for every request**, valid
|
|
489
|
-
token or not — deliberately, so a public demo cannot be used as a token oracle.
|
|
490
|
-
It surfaces as `AuthorizationError`.
|
|
491
|
-
|
|
492
468
|
---
|
|
493
469
|
|
|
494
470
|
## Channel Scoping
|
|
@@ -571,9 +547,6 @@ The token is valid but lacks the permission for that call. Check the table
|
|
|
571
547
|
above, then re-issue the key with the resource enabled — permissions are fixed
|
|
572
548
|
at creation.
|
|
573
549
|
|
|
574
|
-
On a demo instance, the entire migration API returns 403 for every request,
|
|
575
|
-
valid token or not.
|
|
576
|
-
|
|
577
550
|
### `ValidationError` (422) on a repeated send
|
|
578
551
|
|
|
579
552
|
If you reused an `idempotency_key` with a *different* payload, the API rejects
|
|
@@ -441,30 +441,6 @@ client.prime() # full capability manifest
|
|
|
441
441
|
client.skill() # plain-text agent skill manifest (a str)
|
|
442
442
|
```
|
|
443
443
|
|
|
444
|
-
### Migration / export
|
|
445
|
-
|
|
446
|
-
Read-only. **Admin tokens only**, and every call needs a `broadcast_channel_id`.
|
|
447
|
-
|
|
448
|
-
```python
|
|
449
|
-
client = Broadcast(..., broadcast_channel_id=42)
|
|
450
|
-
|
|
451
|
-
client.migration.manifest()
|
|
452
|
-
|
|
453
|
-
for sub in client.migration.each_record("subscribers"):
|
|
454
|
-
... # auto-pages; advances by the limit the server actually applied
|
|
455
|
-
|
|
456
|
-
# Suppressed addresses live in two lists; export both:
|
|
457
|
-
# suppressions = global suppression list, unsubscribed_emails = the channel's own
|
|
458
|
-
for row in client.migration.each_record("unsubscribed_emails"):
|
|
459
|
-
... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
|
|
460
|
-
|
|
461
|
-
data = client.migration.download_file_asset(id) # bytes
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
On a demo instance this entire API returns **403 for every request**, valid
|
|
465
|
-
token or not — deliberately, so a public demo cannot be used as a token oracle.
|
|
466
|
-
It surfaces as `AuthorizationError`.
|
|
467
|
-
|
|
468
444
|
---
|
|
469
445
|
|
|
470
446
|
## Channel Scoping
|
|
@@ -547,9 +523,6 @@ The token is valid but lacks the permission for that call. Check the table
|
|
|
547
523
|
above, then re-issue the key with the resource enabled — permissions are fixed
|
|
548
524
|
at creation.
|
|
549
525
|
|
|
550
|
-
On a demo instance, the entire migration API returns 403 for every request,
|
|
551
|
-
valid token or not.
|
|
552
|
-
|
|
553
526
|
### `ValidationError` (422) on a repeated send
|
|
554
527
|
|
|
555
528
|
If you reused an `idempotency_key` with a *different* payload, the API rejects
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "broadcast-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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"
|
|
@@ -27,7 +27,6 @@ from .errors import (
|
|
|
27
27
|
WarningError,
|
|
28
28
|
)
|
|
29
29
|
from .resources.email_servers import REDACTED_FIELDS
|
|
30
|
-
from .resources.migration import COLLECTIONS
|
|
31
30
|
from .resources.opt_in_forms import TRIGGER_FREQUENCIES
|
|
32
31
|
from .resources.transactionals import MAX_IDEMPOTENCY_KEY_LENGTH
|
|
33
32
|
from .response import RateLimit, Response, Warning_
|
|
@@ -37,7 +36,6 @@ from .webhook import EVENT_TYPES
|
|
|
37
36
|
__version__ = VERSION
|
|
38
37
|
|
|
39
38
|
__all__ = [
|
|
40
|
-
"COLLECTIONS",
|
|
41
39
|
"ENV_HOST",
|
|
42
40
|
"ENV_TOKEN",
|
|
43
41
|
"EVENT_TYPES",
|
|
@@ -11,7 +11,6 @@ from .resources.discovery import Discovery
|
|
|
11
11
|
from .resources.email_servers import EmailServers
|
|
12
12
|
from .resources.file_assets import FileAssets
|
|
13
13
|
from .resources.global_suppressions import GlobalSuppressions
|
|
14
|
-
from .resources.migration import Migration
|
|
15
14
|
from .resources.opt_in_forms import OptInForms
|
|
16
15
|
from .resources.segments import Segments
|
|
17
16
|
from .resources.sequences import Sequences
|
|
@@ -65,7 +64,6 @@ class Broadcast:
|
|
|
65
64
|
#: API token.
|
|
66
65
|
self.global_suppressions = GlobalSuppressions(self)
|
|
67
66
|
#: Read-only export endpoints. Requires an admin (system) API token.
|
|
68
|
-
self.migration = Migration(self)
|
|
69
67
|
#: Installation users and their permissions. Requires an admin
|
|
70
68
|
#: (system) API token; sudo users are read-only through this API.
|
|
71
69
|
self.users = Users(self)
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"""Read-only export endpoints under ``/api/migration/v1``.
|
|
2
|
-
|
|
3
|
-
Two things differ from the v1 API:
|
|
4
|
-
|
|
5
|
-
1. **Admin tokens only.** Channel-scoped tokens are rejected outright.
|
|
6
|
-
2. **broadcast_channel_id is required on every call.** Set it once via
|
|
7
|
-
``Broadcast(broadcast_channel_id=...)`` or ``client.with_channel(id)`` and it
|
|
8
|
-
is attached automatically; otherwise pass it per call.
|
|
9
|
-
|
|
10
|
-
On a demo instance (``DEMO_MODE``) this entire API returns 403 for every
|
|
11
|
-
request, valid token or not, so a public demo cannot be used as a token oracle.
|
|
12
|
-
That surfaces here as :class:`AuthorizationError`.
|
|
13
|
-
|
|
14
|
-
Every list endpoint pages with ``limit`` (1..250, default 250) and ``offset``,
|
|
15
|
-
returning ``{"data": [...], "pagination": {...}}``.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from typing import Any, Iterator, Union
|
|
19
|
-
|
|
20
|
-
from .base import BaseResource
|
|
21
|
-
|
|
22
|
-
Id = Union[str, int]
|
|
23
|
-
|
|
24
|
-
#: Endpoints that are a plain paginated list of the channel's records.
|
|
25
|
-
COLLECTIONS = (
|
|
26
|
-
"channels",
|
|
27
|
-
"subscribers",
|
|
28
|
-
"templates",
|
|
29
|
-
"segments",
|
|
30
|
-
"sequences",
|
|
31
|
-
"email_servers",
|
|
32
|
-
"opt_in_forms",
|
|
33
|
-
"broadcasts",
|
|
34
|
-
"outbound_receipts",
|
|
35
|
-
"webhook_endpoints",
|
|
36
|
-
"tokens",
|
|
37
|
-
"suppressions",
|
|
38
|
-
"unsubscribed_emails",
|
|
39
|
-
"tags",
|
|
40
|
-
"users",
|
|
41
|
-
"link_redirects",
|
|
42
|
-
"link_clicks",
|
|
43
|
-
"subscriber_histories",
|
|
44
|
-
"file_assets",
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class Migration(BaseResource):
|
|
49
|
-
def manifest(self, **params: Any) -> Any:
|
|
50
|
-
"""Export summary: format version, channel identity, per-resource counts,
|
|
51
|
-
and recent-history totals. Call this first to size an export.
|
|
52
|
-
|
|
53
|
-
``days_history`` windows the time-bounded counts; the server clamps to 1..365.
|
|
54
|
-
"""
|
|
55
|
-
return self._get("/api/migration/v1/manifest", params)
|
|
56
|
-
|
|
57
|
-
def download_file_asset(self, id: Id, **params: Any) -> bytes: # noqa: A002
|
|
58
|
-
"""Binary contents of a stored file asset — bytes, not JSON."""
|
|
59
|
-
return self._get("/api/migration/v1/file_assets/{}/download".format(id), params, raw=True)
|
|
60
|
-
|
|
61
|
-
def each_record(self, collection: str, limit: int = 250, **params: Any) -> Iterator[Any]:
|
|
62
|
-
"""Page through a collection, yielding each record.
|
|
63
|
-
|
|
64
|
-
for sub in client.migration.each_record("subscribers"):
|
|
65
|
-
...
|
|
66
|
-
|
|
67
|
-
Stops when the server reports ``has_more: False``, and advances by the
|
|
68
|
-
limit the server actually applied rather than the one requested — the
|
|
69
|
-
server clamps to 250, so trusting the request would skip records.
|
|
70
|
-
"""
|
|
71
|
-
offset = 0
|
|
72
|
-
while True:
|
|
73
|
-
page = getattr(self, collection)(limit=limit, offset=offset, **params)
|
|
74
|
-
records = page.get("data") or []
|
|
75
|
-
yield from records
|
|
76
|
-
|
|
77
|
-
pagination = page.get("pagination") or {}
|
|
78
|
-
if not pagination.get("has_more"):
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
# `pagination.get("limit") or len(records)` would be wrong: Python's
|
|
82
|
-
# `or` falls back on any falsy value, so a server-reported limit of
|
|
83
|
-
# 0 would become len(records) and the loop would never terminate.
|
|
84
|
-
# Ruby's `||` only falls back on nil, which is why the reference
|
|
85
|
-
# implementation can spell it that way and this one cannot.
|
|
86
|
-
advanced = pagination.get("limit")
|
|
87
|
-
if advanced is None:
|
|
88
|
-
advanced = len(records)
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
advanced = int(advanced)
|
|
92
|
-
except (TypeError, ValueError):
|
|
93
|
-
return
|
|
94
|
-
if advanced <= 0:
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
offset += advanced
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _make_collection_method(name: str):
|
|
101
|
-
def method(self, **params: Any) -> Any:
|
|
102
|
-
return self._get("/api/migration/v1/{}".format(name), params)
|
|
103
|
-
|
|
104
|
-
method.__name__ = name
|
|
105
|
-
method.__qualname__ = "Migration.{}".format(name)
|
|
106
|
-
method.__doc__ = "Paginated export of the channel's {}.".format(name.replace("_", " "))
|
|
107
|
-
return method
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# Generated rather than hand-written: 19 near-identical methods invite the kind
|
|
111
|
-
# of copy-paste drift this whole SDK family exists to prevent. Declared in
|
|
112
|
-
# .api-coverage.yml so the coverage report still counts them.
|
|
113
|
-
for _collection in COLLECTIONS:
|
|
114
|
-
setattr(Migration, _collection, _make_collection_method(_collection))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/__init__.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/autopilots.py
RENAMED
|
File without changes
|
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/broadcasts.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_design.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_settings.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/discovery.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/email_servers.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/file_assets.py
RENAMED
|
File without changes
|
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/opt_in_forms.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/segments.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/sequences.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/subscribers.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/suppressions.py
RENAMED
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/templates.py
RENAMED
|
File without changes
|
|
File without changes
|
{broadcast_python-0.7.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/transactionals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|