broadcast-python 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

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.
@@ -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,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
@@ -1,12 +1,12 @@
1
- broadcast_python/__init__.py,sha256=ZDjU3aAHHMONoEKnAPZpralqN1a3Z3Ddv73mkQWt4U0,1715
2
- broadcast_python/client.py,sha256=wHBjIxgDPp91I-y2VFxpACsd2DZ0GnDQzEIy6mq4NLw,6002
1
+ broadcast_python/__init__.py,sha256=1zEuVr5Kk1jC8Zo0Zfj2sSh3y9jIqkP_b_TiLY8nnpg,1651
2
+ broadcast_python/client.py,sha256=zmGwp4Fh95ZWQlss23bJpcqTYg3lbuc8Yzs4ptHkzPQ,5918
3
3
  broadcast_python/configuration.py,sha256=kXheoYhwRJo6STIL9Wmhl_MOEdWfaV3CBETEh1SbEcI,3961
4
4
  broadcast_python/connection.py,sha256=_Tlm3gbvHSCAuGPzX5StOlu6MJgpXFnGFrFWybQoC44,14500
5
5
  broadcast_python/errors.py,sha256=NFJ69Oy_FtYZ3-BEq8Q1m5wLqcufRZLPDYuJlgBfGpc,1993
6
6
  broadcast_python/multipart.py,sha256=VUILWzkJVWk6tL6GR92STnzPs4XiE58LeLSL_L5hBqQ,2155
7
7
  broadcast_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  broadcast_python/response.py,sha256=fIBzxi4a2Sc7sNSOb2_t85Y3Opdet89uNbHM5CX3YXw,5511
9
- broadcast_python/version.py,sha256=wpqv8V3pu9rnD9tshDmoTOEfiWXIB8UxkvQXqVk9wKU,169
9
+ broadcast_python/version.py,sha256=F386N9CM9EZn34Bvz-gSO53tGlBdyJK20DYT8qcA4X4,169
10
10
  broadcast_python/webhook.py,sha256=2OtgY4IqKcf73hyUcCwZWpS_VURPjU2ZOgizl8Wz3Dg,3776
11
11
  broadcast_python/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  broadcast_python/resources/autopilots.py,sha256=PVOetU4DwF5ISZKuvN1UYr5a6hnwiWJLt1AcAo6wMbc,3488
@@ -18,8 +18,7 @@ broadcast_python/resources/discovery.py,sha256=B0BKq9hpRwd9gN4MWoPArz2dli4bQLZs9
18
18
  broadcast_python/resources/email_servers.py,sha256=1_ZJEPwVjFi5rN6S_0n7WfFfmF5y3JDIcW0Ixak0Z24,2830
19
19
  broadcast_python/resources/file_assets.py,sha256=-FGwd4mLNetO1MkamZ2BqW8bkEdlwjcV3mPviJONk_4,4189
20
20
  broadcast_python/resources/global_suppressions.py,sha256=5Nghq0AfQYLPEL_IAc_tmwGfovA5jpLmdw8yUxg6HpA,1730
21
- broadcast_python/resources/migration.py,sha256=EvqUvn254Tr_mMVM7Ki3MZb0o0c8_PO4ZUo-lHnvBTA,4126
22
- broadcast_python/resources/opt_in_forms.py,sha256=271hVuzmS1Cda9cT0UZLkeuUaYH1kJZYw-NpUV_g1pE,3083
21
+ broadcast_python/resources/opt_in_forms.py,sha256=YALMAkpQedGXcpvIX8ZfCD4SF2R1reSA9t5l_8wW2qw,3308
23
22
  broadcast_python/resources/segments.py,sha256=ZfUJxiWPhERWzkzuRGVu09uWqbwyUbdr3NsKOEeuW8o,857
24
23
  broadcast_python/resources/sequences.py,sha256=9IuKw13e_LYZMLZiBYrr0b-BPhAAAucdt8DUjQvbP80,2467
25
24
  broadcast_python/resources/subscribers.py,sha256=OMcdlfDMgs-gUXIYGLhw3IoRQbBlChFT6EcJOJ2FeVE,3786
@@ -29,7 +28,7 @@ broadcast_python/resources/topics.py,sha256=jgspk04PgpxzGpehP9DO_AjKk361pgsbaYFR
29
28
  broadcast_python/resources/transactionals.py,sha256=iwe9aTHKpeEMtDxd_S2TVzdUVm5Phno-Vinrxu9bn6w,2940
30
29
  broadcast_python/resources/users.py,sha256=25gPmWLI1rqQnS9gCOi_2esUjdz_e0Zfi7_7Y2x91mg,4813
31
30
  broadcast_python/resources/webhook_endpoints.py,sha256=Llkry10QT3JavkmKok3IAayYKM1PD1-iI51YhW3RbpE,1180
32
- broadcast_python-0.6.0.dist-info/METADATA,sha256=C9qbRKdtHq-76BUpqv2DvuPCuDw0nGjQo7JeIY-pYUE,24904
33
- broadcast_python-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
- broadcast_python-0.6.0.dist-info/licenses/LICENSE,sha256=cnoPKJuOFIyR8mBe7kmEcUQelniJaujjGl21QWqzlKE,1067
35
- broadcast_python-0.6.0.dist-info/RECORD,,
31
+ broadcast_python-0.8.0.dist-info/METADATA,sha256=wNHJlEYslNI-CMoS7ulr-_XqViaZVIPbidoS-1dY_tU,24159
32
+ broadcast_python-0.8.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
33
+ broadcast_python-0.8.0.dist-info/licenses/LICENSE,sha256=cnoPKJuOFIyR8mBe7kmEcUQelniJaujjGl21QWqzlKE,1067
34
+ broadcast_python-0.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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))