broadcast-python 0.3.0__tar.gz → 0.5.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 (34) hide show
  1. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/CHANGELOG.md +38 -0
  2. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/PKG-INFO +36 -2
  3. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/README.md +35 -1
  4. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/pyproject.toml +1 -1
  5. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/__init__.py +2 -0
  6. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/client.py +2 -0
  7. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/autopilots.py +2 -1
  8. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/opt_in_forms.py +9 -0
  9. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/subscribers.py +13 -4
  10. broadcast_python-0.5.0/src/broadcast_python/resources/topics.py +29 -0
  11. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/version.py +1 -1
  12. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/webhook.py +3 -0
  13. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/.gitignore +0 -0
  14. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/LICENSE +0 -0
  15. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/configuration.py +0 -0
  16. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/connection.py +0 -0
  17. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/errors.py +0 -0
  18. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/py.typed +0 -0
  19. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/__init__.py +0 -0
  20. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/base.py +0 -0
  21. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/broadcasts.py +0 -0
  22. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/channel_design.py +0 -0
  23. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/discovery.py +0 -0
  24. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/email_servers.py +0 -0
  25. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/global_suppressions.py +0 -0
  26. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/migration.py +0 -0
  27. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/segments.py +0 -0
  28. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/sequences.py +0 -0
  29. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/suppressions.py +0 -0
  30. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/templates.py +0 -0
  31. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/transactionals.py +0 -0
  32. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/users.py +0 -0
  33. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/resources/webhook_endpoints.py +0 -0
  34. {broadcast_python-0.3.0 → broadcast_python-0.5.0}/src/broadcast_python/response.py +0 -0
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.0] - 2026-10-10
8
+
9
+ ### Added
10
+
11
+ - `email.send_delayed` and `broadcast.batch_completed` in `EVENT_TYPES` (and in
12
+ `EMAIL_EVENTS` / `BROADCAST_EVENTS`), matching the server (Broadcast 2.43.0).
13
+ `email.send_delayed` fires when a transactional email waits for room under
14
+ its server's hourly limit.
15
+ - `excluded_segment_ids` on broadcasts, sequences and autopilots: segments a
16
+ send never reaches. The server refuses a segment that is both sent to and
17
+ excluded with 422.
18
+ - `subscribers.create(unsubscribed_at=...)`: creates a subscriber who has
19
+ already unsubscribed, in one request, with any token. Before, the server
20
+ ignored the field and a migration had to create, then unsubscribe.
21
+
22
+ - `TRIGGER_FREQUENCIES`: the words `trigger_settings["frequency"]` accepts
23
+ (`always`, `every_visit`, `once_per_session`, `once_per_day`,
24
+ `once_per_week`, `once`).
25
+
26
+ ### Changed
27
+
28
+ - Opt-in forms: the server now refuses an unknown `trigger_settings`
29
+ frequency word with 422 (`ValidationError`). Before, it saved any word, and
30
+ an unknown one showed the popup on every visit. The client still sends what
31
+ it is given.
32
+
33
+ ## [0.4.0] - 2026-10-06
34
+
35
+ ### Added
36
+
37
+ - `client.topics` for `/api/v1/topics` (subscriber topics): `list`, `get`,
38
+ `create`, `update`, `delete`. Topics use the subscriber permissions.
39
+ Broadcasts and sequences accept `topic_id`.
40
+ - `subscribers.update(email, custom_data_mode="merge", ...)`: change only the
41
+ custom_data keys sent; `None` deletes a key. The default stays replace.
42
+ - `subscriber.preferences_updated` webhook event type, in `SUBSCRIBER_EVENTS`
43
+ and `EVENT_TYPES` (now 35).
44
+
7
45
  ## [0.3.0] - 2026-10-03
8
46
 
9
47
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: broadcast-python
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -240,12 +240,46 @@ client.sequences.move_step(id, step_id, under_id)
240
240
  client.segments.create(name="VIPs")
241
241
  client.templates.create(label="Welcome", subject="Hi", body="...")
242
242
  client.opt_in_forms.analytics(id, start_date=date(2026, 1, 1))
243
+ # How often the same visitor sees a popup: one of TRIGGER_FREQUENCIES, or ValidationError (422).
244
+ client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_day"})
243
245
  client.opt_in_forms.create_variant(id, name="B", weight=50)
244
246
  client.opt_in_forms.duplicate(id, label="Copy")
245
247
  ```
246
248
 
247
249
  Reading a segment recounts its members server-side, so `segments.get` is not free.
248
250
 
251
+ ### Topics
252
+
253
+ A topic is one kind of email subscribers opt in to or out of (webinars, offers).
254
+ Its value lives in a top-level `custom_data` key (`True`, `False`, or no value)
255
+ or in a tag. `unset_receives` decides, at send time, whether a subscriber with no
256
+ value receives it; Broadcast never writes a default. Topics use the subscriber
257
+ permissions.
258
+
259
+ ```python
260
+ topic = client.topics.create(name="Webinars", custom_data_key="sub_webinars")
261
+ client.topics.create(name="News", storage="tag", tag_name="news")
262
+ client.topics.list()
263
+ client.topics.update(topic["id"], unset_receives=False)
264
+ client.topics.delete(topic["id"]) # 422 while a broadcast or sequence uses it
265
+
266
+ # Send to a topic: (segments) AND topic
267
+ client.broadcasts.create(subject="Webinar", body="...", segment_ids=[2], topic_id=topic["id"])
268
+
269
+ # Send to everyone except a segment (also on sequences and autopilots)
270
+ client.broadcasts.create(subject="Offer", body="...", excluded_segment_ids=[competitors["id"]])
271
+
272
+ # Migrate someone who already unsubscribed, in one request
273
+ client.subscribers.create(email="opted-out@example.com", unsubscribed_at="2025-03-14T09:30:00Z")
274
+
275
+ # Change only the keys you send (None deletes a key); the default replaces custom_data
276
+ client.subscribers.update("jane@example.com", custom_data={"sub_webinars": False}, custom_data_mode="merge")
277
+ ```
278
+
279
+ The subscriber JSON has `topics`: each topic's stored value (`True`, `False`, or
280
+ `None`). Topic changes fire `subscriber.preferences_updated` as well as
281
+ `subscriber.updated`.
282
+
249
283
  ### Channel design (brand kit)
250
284
 
251
285
  ```python
@@ -413,7 +447,7 @@ every rejection rather than distinguishing them.
413
447
  Pass the **raw** request body. Re-serialising a parsed dict changes the bytes
414
448
  and verification will fail.
415
449
 
416
- `broadcast_python.EVENT_TYPES` lists all 34 event names. `SUBSCRIBER_EVENTS`
450
+ `broadcast_python.EVENT_TYPES` lists all 37 event names. `SUBSCRIBER_EVENTS`
417
451
  includes `subscribers.purged` and `subscribers.purge_failed`: a purge of the
418
452
  whole list sends one of these instead of a `subscriber.deleted` per subscriber.
419
453
 
@@ -216,12 +216,46 @@ client.sequences.move_step(id, step_id, under_id)
216
216
  client.segments.create(name="VIPs")
217
217
  client.templates.create(label="Welcome", subject="Hi", body="...")
218
218
  client.opt_in_forms.analytics(id, start_date=date(2026, 1, 1))
219
+ # How often the same visitor sees a popup: one of TRIGGER_FREQUENCIES, or ValidationError (422).
220
+ client.opt_in_forms.update(id, trigger_settings={"frequency": "once_per_day"})
219
221
  client.opt_in_forms.create_variant(id, name="B", weight=50)
220
222
  client.opt_in_forms.duplicate(id, label="Copy")
221
223
  ```
222
224
 
223
225
  Reading a segment recounts its members server-side, so `segments.get` is not free.
224
226
 
227
+ ### Topics
228
+
229
+ A topic is one kind of email subscribers opt in to or out of (webinars, offers).
230
+ Its value lives in a top-level `custom_data` key (`True`, `False`, or no value)
231
+ or in a tag. `unset_receives` decides, at send time, whether a subscriber with no
232
+ value receives it; Broadcast never writes a default. Topics use the subscriber
233
+ permissions.
234
+
235
+ ```python
236
+ topic = client.topics.create(name="Webinars", custom_data_key="sub_webinars")
237
+ client.topics.create(name="News", storage="tag", tag_name="news")
238
+ client.topics.list()
239
+ client.topics.update(topic["id"], unset_receives=False)
240
+ client.topics.delete(topic["id"]) # 422 while a broadcast or sequence uses it
241
+
242
+ # Send to a topic: (segments) AND topic
243
+ client.broadcasts.create(subject="Webinar", body="...", segment_ids=[2], topic_id=topic["id"])
244
+
245
+ # Send to everyone except a segment (also on sequences and autopilots)
246
+ client.broadcasts.create(subject="Offer", body="...", excluded_segment_ids=[competitors["id"]])
247
+
248
+ # Migrate someone who already unsubscribed, in one request
249
+ client.subscribers.create(email="opted-out@example.com", unsubscribed_at="2025-03-14T09:30:00Z")
250
+
251
+ # Change only the keys you send (None deletes a key); the default replaces custom_data
252
+ client.subscribers.update("jane@example.com", custom_data={"sub_webinars": False}, custom_data_mode="merge")
253
+ ```
254
+
255
+ The subscriber JSON has `topics`: each topic's stored value (`True`, `False`, or
256
+ `None`). Topic changes fire `subscriber.preferences_updated` as well as
257
+ `subscriber.updated`.
258
+
225
259
  ### Channel design (brand kit)
226
260
 
227
261
  ```python
@@ -389,7 +423,7 @@ every rejection rather than distinguishing them.
389
423
  Pass the **raw** request body. Re-serialising a parsed dict changes the bytes
390
424
  and verification will fail.
391
425
 
392
- `broadcast_python.EVENT_TYPES` lists all 34 event names. `SUBSCRIBER_EVENTS`
426
+ `broadcast_python.EVENT_TYPES` lists all 37 event names. `SUBSCRIBER_EVENTS`
393
427
  includes `subscribers.purged` and `subscribers.purge_failed`: a purge of the
394
428
  whole list sends one of these instead of a `subscriber.deleted` per subscriber.
395
429
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "broadcast-python"
7
- version = "0.3.0"
7
+ version = "0.5.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"
@@ -28,6 +28,7 @@ from .errors import (
28
28
  )
29
29
  from .resources.email_servers import REDACTED_FIELDS
30
30
  from .resources.migration import COLLECTIONS
31
+ from .resources.opt_in_forms import TRIGGER_FREQUENCIES
31
32
  from .resources.transactionals import MAX_IDEMPOTENCY_KEY_LENGTH
32
33
  from .response import RateLimit, Response, Warning_
33
34
  from .version import VERSION
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "EVENT_TYPES",
43
44
  "MAX_IDEMPOTENCY_KEY_LENGTH",
44
45
  "REDACTED_FIELDS",
46
+ "TRIGGER_FREQUENCIES",
45
47
  "VERSION",
46
48
  "WARNINGS_MODES",
47
49
  "APIError",
@@ -16,6 +16,7 @@ from .resources.sequences import Sequences
16
16
  from .resources.subscribers import Subscribers
17
17
  from .resources.suppressions import Suppressions
18
18
  from .resources.templates import Templates
19
+ from .resources.topics import Topics
19
20
  from .resources.transactionals import Transactionals
20
21
  from .resources.users import Users
21
22
  from .resources.webhook_endpoints import WebhookEndpoints
@@ -40,6 +41,7 @@ class Broadcast:
40
41
  self.sequences = Sequences(self)
41
42
  self.broadcasts = Broadcasts(self)
42
43
  self.segments = Segments(self)
44
+ self.topics = Topics(self)
43
45
  self.templates = Templates(self)
44
46
  #: The channel's brand kit (Settings -> Design). Read-only.
45
47
  self.channel_design = ChannelDesign(self)
@@ -36,7 +36,8 @@ class Autopilots(BaseResource):
36
36
  write-only. Scheduling takes ``schedule_frequency`` (daily, weekly,
37
37
  biweekly, monthly), ``schedule_day_of_week``, ``schedule_day_of_month``,
38
38
  ``schedule_time``, ``schedule_timezone``. ``segment_ids`` restricts the
39
- newsletter's audience.
39
+ newsletter's audience; ``excluded_segment_ids`` lists segments its
40
+ drafts never reach.
40
41
  """
41
42
  return self._post("/api/v1/autopilots", {"autopilot": attrs})
42
43
 
@@ -5,6 +5,12 @@ from .base import BaseResource, compact
5
5
 
6
6
  Id = Union[str, int]
7
7
 
8
+ #: The words ``trigger_settings["frequency"]`` accepts: how often the same
9
+ #: visitor sees a popup. ``always`` and ``every_visit`` mean the same. The server
10
+ #: refuses any other word with 422 (:class:`ValidationError`); the client does
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")
13
+
8
14
 
9
15
  class OptInForms(BaseResource):
10
16
  def list(self, **params: Any) -> Any:
@@ -23,6 +29,9 @@ class OptInForms(BaseResource):
23
29
  Nested settings dicts (``theme_settings``, ``automation_settings``,
24
30
  ``security_settings``, ``trigger_settings``, ``widget_settings``) and
25
31
  the block arrays are passed through verbatim.
32
+ ``trigger_settings["frequency"]`` must be one of
33
+ :data:`TRIGGER_FREQUENCIES`; any other word raises
34
+ :class:`ValidationError` (422).
26
35
  """
27
36
  return self._post("/api/v1/opt_in_forms", {"opt_in_form": attrs})
28
37
 
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
3
  from .base import BaseResource
4
4
 
@@ -33,7 +33,11 @@ class Subscribers(BaseResource):
33
33
  timestamp when migrating an already-confirmed list off another provider.
34
34
  It is ignored (with a warning) on update.
35
35
 
36
- ``unsubscribed_at`` is never settable here; use :meth:`unsubscribe`.
36
+ ``unsubscribed_at`` (any token, create only, ISO 8601) stores the
37
+ subscriber as unsubscribed in the same request, for migrating a list
38
+ with its opt-outs intact: no confirmation email, 422 if it is in the
39
+ future or sent with ``is_active=True``. To unsubscribe an existing
40
+ subscriber, use :meth:`unsubscribe`.
37
41
  """
38
42
  double_opt_in = attrs.pop("double_opt_in", None)
39
43
  confirmation_template_id = attrs.pop("confirmation_template_id", None)
@@ -46,8 +50,13 @@ class Subscribers(BaseResource):
46
50
 
47
51
  return self._post("/api/v1/subscribers.json", payload)
48
52
 
49
- def update(self, email: str, **attrs: Any) -> Any:
50
- return self._patch("/api/v1/subscribers.json", {"email": email, "subscriber": attrs})
53
+ def update(self, email: str, custom_data_mode: Optional[str] = None, **attrs: Any) -> Any:
54
+ """custom_data_mode: "replace" (the API default) stores custom_data as sent;
55
+ "merge" changes only the keys sent, and a None value deletes that key."""
56
+ payload: Dict[str, Any] = {"email": email, "subscriber": attrs}
57
+ if custom_data_mode:
58
+ payload["custom_data_mode"] = custom_data_mode
59
+ return self._patch("/api/v1/subscribers.json", payload)
51
60
 
52
61
  def add_tags(self, email: str, tags: List[str]) -> Any:
53
62
  return self._post("/api/v1/subscribers/add_tag.json", {"email": email, "tags": tags})
@@ -0,0 +1,29 @@
1
+ from typing import Any, Union
2
+
3
+ from .base import BaseResource
4
+
5
+ Id = Union[str, int]
6
+
7
+
8
+ class Topics(BaseResource):
9
+ """Subscriber topics: kinds of email subscribers opt in to or out of.
10
+
11
+ A topic reads a top-level custom_data key (true, false, or no value) or a
12
+ tag. Topics use the token's subscriber permissions.
13
+ """
14
+
15
+ def list(self, **params: Any) -> Any:
16
+ return self._get("/api/v1/topics.json", params)
17
+
18
+ def get(self, id: Id) -> Any: # noqa: A002
19
+ return self._get("/api/v1/topics/{}.json".format(id), {})
20
+
21
+ def create(self, **attrs: Any) -> Any:
22
+ return self._post("/api/v1/topics", {"topic": attrs})
23
+
24
+ def update(self, id: Id, **attrs: Any) -> Any: # noqa: A002
25
+ return self._patch("/api/v1/topics/{}".format(id), {"topic": attrs})
26
+
27
+ def delete(self, id: Id) -> Any: # noqa: A002
28
+ """Refused (422) while a broadcast or sequence uses the topic."""
29
+ return self._delete("/api/v1/topics/{}".format(id))
@@ -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.3.0"
3
+ VERSION = "0.5.0"
@@ -15,6 +15,7 @@ EMAIL_EVENTS = (
15
15
  "email.sent",
16
16
  "email.delivered",
17
17
  "email.delivery_delayed",
18
+ "email.send_delayed",
18
19
  "email.complained",
19
20
  "email.bounced",
20
21
  "email.opened",
@@ -25,6 +26,7 @@ EMAIL_EVENTS = (
25
26
  SUBSCRIBER_EVENTS = (
26
27
  "subscriber.created",
27
28
  "subscriber.updated",
29
+ "subscriber.preferences_updated",
28
30
  "subscriber.deleted",
29
31
  "subscriber.subscribed",
30
32
  "subscriber.unsubscribed",
@@ -44,6 +46,7 @@ BROADCAST_EVENTS = (
44
46
  "broadcast.partial_failure",
45
47
  "broadcast.aborted",
46
48
  "broadcast.paused",
49
+ "broadcast.batch_completed",
47
50
  )
48
51
 
49
52
  SEQUENCE_EVENTS = (