broadcast-python 0.6.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.
Files changed (37) hide show
  1. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/CHANGELOG.md +18 -0
  2. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/PKG-INFO +3 -28
  3. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/README.md +2 -27
  4. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/pyproject.toml +1 -1
  5. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/__init__.py +0 -2
  6. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/client.py +0 -2
  7. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/opt_in_forms.py +12 -1
  8. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/version.py +1 -1
  9. broadcast_python-0.6.0/src/broadcast_python/resources/migration.py +0 -114
  10. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/.gitignore +0 -0
  11. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/LICENSE +0 -0
  12. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/configuration.py +0 -0
  13. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/connection.py +0 -0
  14. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/errors.py +0 -0
  15. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/multipart.py +0 -0
  16. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/py.typed +0 -0
  17. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/__init__.py +0 -0
  18. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/autopilots.py +0 -0
  19. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/base.py +0 -0
  20. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/broadcasts.py +0 -0
  21. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_design.py +0 -0
  22. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/channel_settings.py +0 -0
  23. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/discovery.py +0 -0
  24. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/email_servers.py +0 -0
  25. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/file_assets.py +0 -0
  26. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/global_suppressions.py +0 -0
  27. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/segments.py +0 -0
  28. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/sequences.py +0 -0
  29. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/subscribers.py +0 -0
  30. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/suppressions.py +0 -0
  31. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/templates.py +0 -0
  32. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/topics.py +0 -0
  33. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/transactionals.py +0 -0
  34. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/users.py +0 -0
  35. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/resources/webhook_endpoints.py +0 -0
  36. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/response.py +0 -0
  37. {broadcast_python-0.6.0 → broadcast_python-0.8.0}/src/broadcast_python/webhook.py +0 -0
@@ -4,6 +4,24 @@ 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
+
16
+ ## [0.7.0] - 2026-10-17
17
+
18
+ ### Added
19
+
20
+ - `TRIGGER_FREQUENCIES` includes `once_per_days` (Broadcast 2.49): the popup shows
21
+ again after `trigger_settings.frequency_cookie_days` days, a whole number, 1 or
22
+ more; blank means 30. The client still sends any word as it is, so nothing
23
+ changes for a server before 2.49, which refuses the word with 422.
24
+
7
25
  ## [0.6.0] - 2026-10-16
8
26
 
9
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: broadcast-python
3
- Version: 0.6.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
@@ -242,6 +242,8 @@ client.templates.create(label="Welcome", subject="Hi", body="...")
242
242
  client.opt_in_forms.analytics(id, start_date=date(2026, 1, 1))
243
243
  # How often the same visitor sees a popup: one of TRIGGER_FREQUENCIES, or ValidationError (422).
244
244
  client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_day"})
245
+ # Every N days (Broadcast 2.49 and later): frequency_cookie_days is a whole number, 1 or more; blank means 30.
246
+ client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_days", "frequency_cookie_days": 7})
245
247
  client.opt_in_forms.create_variant(id, name="B", weight=50)
246
248
  client.opt_in_forms.duplicate(id, label="Copy")
247
249
  ```
@@ -463,30 +465,6 @@ client.prime() # full capability manifest
463
465
  client.skill() # plain-text agent skill manifest (a str)
464
466
  ```
465
467
 
466
- ### Migration / export
467
-
468
- Read-only. **Admin tokens only**, and every call needs a `broadcast_channel_id`.
469
-
470
- ```python
471
- client = Broadcast(..., broadcast_channel_id=42)
472
-
473
- client.migration.manifest()
474
-
475
- for sub in client.migration.each_record("subscribers"):
476
- ... # auto-pages; advances by the limit the server actually applied
477
-
478
- # Suppressed addresses live in two lists; export both:
479
- # suppressions = global suppression list, unsubscribed_emails = the channel's own
480
- for row in client.migration.each_record("unsubscribed_emails"):
481
- ... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
482
-
483
- data = client.migration.download_file_asset(id) # bytes
484
- ```
485
-
486
- On a demo instance this entire API returns **403 for every request**, valid
487
- token or not — deliberately, so a public demo cannot be used as a token oracle.
488
- It surfaces as `AuthorizationError`.
489
-
490
468
  ---
491
469
 
492
470
  ## Channel Scoping
@@ -569,9 +547,6 @@ The token is valid but lacks the permission for that call. Check the table
569
547
  above, then re-issue the key with the resource enabled — permissions are fixed
570
548
  at creation.
571
549
 
572
- On a demo instance, the entire migration API returns 403 for every request,
573
- valid token or not.
574
-
575
550
  ### `ValidationError` (422) on a repeated send
576
551
 
577
552
  If you reused an `idempotency_key` with a *different* payload, the API rejects
@@ -218,6 +218,8 @@ client.templates.create(label="Welcome", subject="Hi", body="...")
218
218
  client.opt_in_forms.analytics(id, start_date=date(2026, 1, 1))
219
219
  # How often the same visitor sees a popup: one of TRIGGER_FREQUENCIES, or ValidationError (422).
220
220
  client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_day"})
221
+ # Every N days (Broadcast 2.49 and later): frequency_cookie_days is a whole number, 1 or more; blank means 30.
222
+ client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_days", "frequency_cookie_days": 7})
221
223
  client.opt_in_forms.create_variant(id, name="B", weight=50)
222
224
  client.opt_in_forms.duplicate(id, label="Copy")
223
225
  ```
@@ -439,30 +441,6 @@ client.prime() # full capability manifest
439
441
  client.skill() # plain-text agent skill manifest (a str)
440
442
  ```
441
443
 
442
- ### Migration / export
443
-
444
- Read-only. **Admin tokens only**, and every call needs a `broadcast_channel_id`.
445
-
446
- ```python
447
- client = Broadcast(..., broadcast_channel_id=42)
448
-
449
- client.migration.manifest()
450
-
451
- for sub in client.migration.each_record("subscribers"):
452
- ... # auto-pages; advances by the limit the server actually applied
453
-
454
- # Suppressed addresses live in two lists; export both:
455
- # suppressions = global suppression list, unsubscribed_emails = the channel's own
456
- for row in client.migration.each_record("unsubscribed_emails"):
457
- ... # {"id", "email", "broadcast_channel_id", "created_at", "updated_at"}
458
-
459
- data = client.migration.download_file_asset(id) # bytes
460
- ```
461
-
462
- On a demo instance this entire API returns **403 for every request**, valid
463
- token or not — deliberately, so a public demo cannot be used as a token oracle.
464
- It surfaces as `AuthorizationError`.
465
-
466
444
  ---
467
445
 
468
446
  ## Channel Scoping
@@ -545,9 +523,6 @@ The token is valid but lacks the permission for that call. Check the table
545
523
  above, then re-issue the key with the resource enabled — permissions are fixed
546
524
  at creation.
547
525
 
548
- On a demo instance, the entire migration API returns 403 for every request,
549
- valid token or not.
550
-
551
526
  ### `ValidationError` (422) on a repeated send
552
527
 
553
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.6.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)
@@ -9,7 +9,18 @@ Id = Union[str, int]
9
9
  #: visitor sees a popup. ``always`` and ``every_visit`` mean the same. The server
10
10
  #: refuses any other word with 422 (:class:`ValidationError`); the client does
11
11
  #: not check, so an older server is never refused a word it would take.
12
- TRIGGER_FREQUENCIES = ("always", "every_visit", "once_per_session", "once_per_day", "once_per_week", "once")
12
+ #: ``once_per_days`` (Broadcast 2.49 and later) shows the popup again after
13
+ #: ``trigger_settings["frequency_cookie_days"]`` days: a whole number, 1 or
14
+ #: more; blank means 30.
15
+ TRIGGER_FREQUENCIES = (
16
+ "always",
17
+ "every_visit",
18
+ "once_per_session",
19
+ "once_per_day",
20
+ "once_per_week",
21
+ "once",
22
+ "once_per_days",
23
+ )
13
24
 
14
25
 
15
26
  class OptInForms(BaseResource):
@@ -1,3 +1,3 @@
1
1
  # Kept in sync with pyproject.toml by tests/test_package.py — a User-Agent that
2
2
  # lies about its version misattributes server-side client analytics.
3
- VERSION = "0.6.0"
3
+ VERSION = "0.8.0"
@@ -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))