sendly-python 0.1.0__tar.gz → 1.0.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.
- {sendly_python-0.1.0 → sendly_python-1.0.0}/.github/workflows/ci.yml +18 -6
- sendly_python-1.0.0/CHANGELOG.md +138 -0
- sendly_python-1.0.0/PKG-INFO +653 -0
- sendly_python-1.0.0/README.md +621 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/pyproject.toml +1 -1
- sendly_python-1.0.0/scripts/sync_spec.py +301 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/__init__.py +25 -1
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/client.py +52 -8
- sendly_python-1.0.0/src/sendly/errors.py +196 -0
- sendly_python-1.0.0/src/sendly/resources/_pagination.py +49 -0
- sendly_python-1.0.0/src/sendly/resources/analytics.py +49 -0
- sendly_python-1.0.0/src/sendly/resources/campaigns.py +135 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/domains.py +16 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/emails.py +50 -3
- sendly_python-1.0.0/src/sendly/resources/events.py +98 -0
- sendly_python-1.0.0/src/sendly/resources/lists.py +59 -0
- sendly_python-1.0.0/src/sendly/resources/mailboxes.py +75 -0
- sendly_python-1.0.0/src/sendly/resources/projects.py +33 -0
- sendly_python-1.0.0/src/sendly/resources/segments.py +106 -0
- sendly_python-1.0.0/src/sendly/resources/usage.py +27 -0
- sendly_python-1.0.0/src/sendly/resources/workflows.py +139 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/types.py +65 -0
- sendly_python-1.0.0/tests/fixtures/openapi.json +14791 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/support.py +39 -0
- sendly_python-1.0.0/tests/test_analytics.py +70 -0
- sendly_python-1.0.0/tests/test_campaigns.py +262 -0
- sendly_python-1.0.0/tests/test_contract.py +480 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_domains.py +17 -0
- sendly_python-1.0.0/tests/test_emails.py +225 -0
- sendly_python-1.0.0/tests/test_errors_problem.py +198 -0
- sendly_python-1.0.0/tests/test_events.py +192 -0
- sendly_python-1.0.0/tests/test_lists.py +123 -0
- sendly_python-1.0.0/tests/test_mailboxes.py +99 -0
- sendly_python-1.0.0/tests/test_projects.py +58 -0
- sendly_python-1.0.0/tests/test_segments.py +125 -0
- sendly_python-1.0.0/tests/test_usage.py +56 -0
- sendly_python-1.0.0/tests/test_workflows.py +159 -0
- sendly_python-0.1.0/CHANGELOG.md +0 -29
- sendly_python-0.1.0/PKG-INFO +0 -311
- sendly_python-0.1.0/README.md +0 -279
- sendly_python-0.1.0/scripts/sync_spec.py +0 -140
- sendly_python-0.1.0/src/sendly/errors.py +0 -92
- sendly_python-0.1.0/src/sendly/resources/events.py +0 -26
- sendly_python-0.1.0/tests/fixtures/openapi.json +0 -5729
- sendly_python-0.1.0/tests/test_contract.py +0 -255
- sendly_python-0.1.0/tests/test_emails.py +0 -94
- sendly_python-0.1.0/tests/test_events.py +0 -63
- {sendly_python-0.1.0 → sendly_python-1.0.0}/.github/workflows/release.yml +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/.gitignore +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/LICENSE +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/py.typed +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/__init__.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/_helpers.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/contacts.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/suppression.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/templates.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/verify.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/webhooks.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/webhook_utils.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_client.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_contacts.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_suppression.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_templates.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_verify.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_webhook_verify.py +0 -0
- {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_webhooks.py +0 -0
|
@@ -5,7 +5,9 @@ on:
|
|
|
5
5
|
branches: [main]
|
|
6
6
|
pull_request:
|
|
7
7
|
schedule:
|
|
8
|
-
# Mondays 06:00 UTC -- surface
|
|
8
|
+
# Mondays 06:00 UTC -- surface OpenAPI drift even without a push. Only does
|
|
9
|
+
# anything once the SENDLY_OPENAPI_URL repository variable is set; see the
|
|
10
|
+
# drift step at the end of this file.
|
|
9
11
|
- cron: "0 6 * * 1"
|
|
10
12
|
|
|
11
13
|
# Public repo -> GitHub-hosted standard runners (free for public repos).
|
|
@@ -38,15 +40,25 @@ jobs:
|
|
|
38
40
|
run: pytest tests/test_contract.py
|
|
39
41
|
- name: Pytest
|
|
40
42
|
run: pytest
|
|
41
|
-
# Non-blocking: diff the vendored spec against the
|
|
42
|
-
# up as a warning annotation without failing the build. Runs
|
|
43
|
-
# newest interpreter) to avoid duplicate
|
|
44
|
-
|
|
43
|
+
# Non-blocking: diff the vendored spec against the reference contract so
|
|
44
|
+
# drift shows up as a warning annotation without failing the build. Runs
|
|
45
|
+
# once (on the newest interpreter) to avoid duplicate reads and annotations.
|
|
46
|
+
#
|
|
47
|
+
# This step previously fetched https://api.sendly.now on every push, every
|
|
48
|
+
# pull request (forks included) and every weekly cron. Syncing the SDK spec
|
|
49
|
+
# from production is forbidden, so the source is now explicit: the step
|
|
50
|
+
# compares against whatever the SENDLY_OPENAPI_URL repository variable
|
|
51
|
+
# names, and SKIPS with a notice when that variable is unset. Until a
|
|
52
|
+
# maintainer sets it to a non-production contract, this step and the weekly
|
|
53
|
+
# schedule above are inert by design.
|
|
54
|
+
- name: OpenAPI drift check (non-blocking)
|
|
45
55
|
id: spec_drift
|
|
46
56
|
if: matrix.python-version == '3.13'
|
|
47
57
|
continue-on-error: true
|
|
58
|
+
env:
|
|
59
|
+
SENDLY_OPENAPI_URL: ${{ vars.SENDLY_OPENAPI_URL }}
|
|
48
60
|
run: python scripts/sync_spec.py --check
|
|
49
61
|
- name: Annotate OpenAPI drift
|
|
50
62
|
if: matrix.python-version == '3.13' && steps.spec_drift.outcome == 'failure'
|
|
51
63
|
run: |
|
|
52
|
-
echo "::warning title=OpenAPI spec drift::Vendored tests/fixtures/openapi.json differs from the
|
|
64
|
+
echo "::warning title=OpenAPI spec drift::Vendored tests/fixtures/openapi.json differs from the contract at \$SENDLY_OPENAPI_URL. Run 'SENDLY_OPENAPI_URL=... python scripts/sync_spec.py' and commit the refreshed copy."
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `sendly-python` are documented here. This project adheres to
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [1.0.0] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
The default send moves to the versioned API. Everything else in this release is
|
|
9
|
+
additive: the operations an API key can actually reach that the SDK did not yet
|
|
10
|
+
expose.
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **`emails.send()` now posts to `POST /api/v1/emails`** and returns the bare
|
|
15
|
+
`202` receipt, `{id, status, to, from}`, where `status` is a real delivery
|
|
16
|
+
state. Before 1.0 it posted to the legacy `POST /api/emails`, which answered
|
|
17
|
+
with row ids and **no** delivery status, and fanned an array `to` out to
|
|
18
|
+
several recipients. What changes for a caller:
|
|
19
|
+
- one recipient in `to`, with `cc` / `bcc` to copy others (an array `to` is
|
|
20
|
+
no longer accepted);
|
|
21
|
+
- the result is the receipt, not `{emails, timestamp}` — read
|
|
22
|
+
`receipt["id"]` and `receipt["status"]` instead of
|
|
23
|
+
`result["emails"][0]["email"]`;
|
|
24
|
+
- failures arrive as RFC 9457 problem documents, raised as the **same**
|
|
25
|
+
exception classes, so `except` blocks are unchanged; `err.error_code` is now
|
|
26
|
+
the lowercase v1 registry value (`validation_error`, not
|
|
27
|
+
`VALIDATION_ERROR`) and `err.request_id` / `err.field_errors` are populated.
|
|
28
|
+
|
|
29
|
+
The pre-1.0 behaviour is kept, unchanged, as **`emails.send_legacy()`** — the
|
|
30
|
+
escape hatch for a caller that depends on the fan-out or the envelope.
|
|
31
|
+
Renaming a call from `send` to `send_legacy` is a complete migration; adopting
|
|
32
|
+
the new default means reading the receipt instead of the envelope.
|
|
33
|
+
|
|
34
|
+
Why now: the legacy send cannot tell a caller whether a message went anywhere,
|
|
35
|
+
and the versioned one can. Nothing is published against 0.x, so the cost of
|
|
36
|
+
the move is lowest today and only rises.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`emails.send_legacy()`** — the pre-1.0 `send()`, byte for byte. See Breaking.
|
|
41
|
+
- **`emails.send_test()`** — sandbox test send. The sandbox address is the
|
|
42
|
+
*sender*; the mail lands in the project owner's own verified inbox. Naming a
|
|
43
|
+
`from` is refused rather than ignored. Takes no `idempotency_key`.
|
|
44
|
+
- **`mailboxes` resource, reads only** — `list()`, `get(id)` (which carries the
|
|
45
|
+
IMAP/SMTP `settings` a mail client needs) and `list_app_passwords(id)`
|
|
46
|
+
(metadata only; the secret is never returned). The mailbox *writes* are not
|
|
47
|
+
missing but unreachable — see Notes.
|
|
48
|
+
- **`projects.get()`** — the project the credential resolves to. Takes no id.
|
|
49
|
+
Carries `sandbox_address`, which no public route published before.
|
|
50
|
+
- **`domains.start_setup(id)`** — begins the guided DNS hand-off and returns the
|
|
51
|
+
route's own `{token, connectUrl, expiresAt}`. Finishing setup means a person
|
|
52
|
+
opening `connectUrl`, so the SDK hands back the link rather than modelling the
|
|
53
|
+
flow behind it.
|
|
54
|
+
|
|
55
|
+
### Notes
|
|
56
|
+
|
|
57
|
+
- **`send_v1` and `send_test_v1` never shipped.** They existed briefly on `main`
|
|
58
|
+
between 0.2.0 and this release as the additive step before the repoint, and
|
|
59
|
+
are folded into `send` and `send_test` here. If you installed from GitHub in
|
|
60
|
+
that window, rename the calls.
|
|
61
|
+
- **Some operations are permanently not SDK-callable.** Creating and deleting a
|
|
62
|
+
mailbox, creating and revoking an app password, the API-key operations, and
|
|
63
|
+
creating a project all resolve the acting user from a session and answer `401`
|
|
64
|
+
to any API key. They are recorded in `tests/test_contract.py`'s
|
|
65
|
+
`NOT_SDK_CALLABLE`, which the suite asserts equals the set the contract itself
|
|
66
|
+
declares — in both directions. Use the dashboard or an OAuth connection.
|
|
67
|
+
- **A project is capped at 10 mailboxes**, counting only ``PROVISIONING``,
|
|
68
|
+
``ACTIVE`` and ``SUSPENDED``. ``FAILED`` rows are excluded from the cap but
|
|
69
|
+
are still returned by ``mailboxes.list()``, so a project that has had failed
|
|
70
|
+
provisions can list more than 10 — the ``list()`` docstring said "at most 10"
|
|
71
|
+
without that distinction and now states it.
|
|
72
|
+
|
|
73
|
+
## [0.2.0]
|
|
74
|
+
|
|
75
|
+
Adds Sendly's `/api/v1` surface. Purely additive — every existing method keeps
|
|
76
|
+
its name, signature, and behaviour.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **New resources for the `/api/v1` surface**, wired onto the same client:
|
|
81
|
+
`sendly.campaigns`, `sendly.segments`, `sendly.workflows`, `sendly.analytics`
|
|
82
|
+
and `sendly.usage`, covering all 33 v1 operations. Unlike the legacy `/api/*`
|
|
83
|
+
resources, these return **bare resource bodies** — there is no
|
|
84
|
+
`{success, data}` envelope to unwrap.
|
|
85
|
+
- **v1 methods on the existing `events` resource**: `events.record` (the v1
|
|
86
|
+
counterpart of `events.track`, which is unchanged), `events.list`,
|
|
87
|
+
`events.list_names` and `events.stats`. `record` takes no `idempotency_key`:
|
|
88
|
+
events are append-only and the API deliberately does not ledger them.
|
|
89
|
+
- **Auto-pagination.** Each of the six cursor-paginated v1 listings gains an
|
|
90
|
+
`iter_*` companion yielding individual items and following the cursor for you:
|
|
91
|
+
`campaigns.iter_list`, `segments.iter_list`, `segments.iter_list_contacts`,
|
|
92
|
+
`workflows.iter_list`, `workflows.iter_list_executions`, `events.iter_list`.
|
|
93
|
+
The v1 list envelope is `{data, has_more, next_cursor}` with `limit` (1-100,
|
|
94
|
+
default 20) and `after` — no total, deliberately. Changing filters
|
|
95
|
+
mid-pagination invalidates the cursor and returns `422 validation_error`, so
|
|
96
|
+
the iterators hold the query fixed and only advance `after`.
|
|
97
|
+
- **RFC 9457 error support.** `application/problem+json` responses from `/api/v1`
|
|
98
|
+
map to the **same** exception classes as the legacy envelope, keyed off the
|
|
99
|
+
same statuses — existing `except` blocks are unaffected. The problem's `code`
|
|
100
|
+
becomes `err.error_code` (e.g. `scope_missing`, `quota_exhausted`,
|
|
101
|
+
`idempotency_key_reused`) and its `detail` (falling back to `title`) becomes
|
|
102
|
+
`err.message`. Two fields are new on `SendlyError`:
|
|
103
|
+
- `err.request_id` — correlation id from the problem document, `None` on the
|
|
104
|
+
legacy surface;
|
|
105
|
+
- `err.field_errors` — per-field `{pointer, code, message}` entries from a v1
|
|
106
|
+
`validation_error`, `None` when absent. The legacy per-field breakdown stays
|
|
107
|
+
at `err.body["error"]["details"]["errors"]`.
|
|
108
|
+
- **`sendly.lists`** — `lists.subscribe(id, body)` and
|
|
109
|
+
`lists.unsubscribe(id, body)` wrap the newly published
|
|
110
|
+
`POST /api/lists/{id}/subscribe` and `.../unsubscribe` operations. Both accept
|
|
111
|
+
sending-only (`pk_*`) keys so they can back a public form. On a double opt-in
|
|
112
|
+
list, subscribe returns `PENDING` with a `confirmToken` and Sendly does **not**
|
|
113
|
+
send the confirmation email — the caller delivers it. Re-subscribing an address
|
|
114
|
+
that opted out needs `allowResubscribe: true` or fails with
|
|
115
|
+
`409 RESUBSCRIBE_CONFIRMATION_REQUIRED`.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- Re-synced the vendored OpenAPI spec (`tests/fixtures/openapi.json`) to the
|
|
120
|
+
committed monorepo contract. The client stays thin (opaque `Mapping` bodies),
|
|
121
|
+
so these are contract/behaviour clarifications rather than method-signature
|
|
122
|
+
changes:
|
|
123
|
+
- **Deletes now return HTTP `200` with the deleted resource's id** (was `204`
|
|
124
|
+
No Content) for `contacts.delete` and `templates.delete`. The SDK still
|
|
125
|
+
discards the body and returns `None` — no consumer change.
|
|
126
|
+
- **Invalid input now raises `SendlyValidationError` from HTTP `422`**
|
|
127
|
+
(`error_code == "VALIDATION_ERROR"`) with a per-field breakdown at
|
|
128
|
+
`err.body["error"]["details"]["errors"]`. Previously invalid input came back
|
|
129
|
+
as `400`. Both `400` and `422` map to `SendlyValidationError`, so
|
|
130
|
+
`except SendlyValidationError` continues to catch validation failures.
|
|
131
|
+
- **Contacts bulk ops (`bulk_create`, `bulk_delete`) against an unresolved
|
|
132
|
+
project now return `422 VALIDATION_ERROR`** (was a `NO_PROJECT` error).
|
|
133
|
+
- **`templates.list` is cursor-paginated** (`limit` / `cursor`) — the former
|
|
134
|
+
`page` / `pageSize` query params are gone. `contacts.list` was already
|
|
135
|
+
cursor-based and is unchanged.
|
|
136
|
+
- Error envelopes on migrated routes now include `success: false` alongside
|
|
137
|
+
`error.{message,code}`; error parsing reads `message`/`code` and is
|
|
138
|
+
unaffected by the additive fields.
|