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.
Files changed (66) hide show
  1. {sendly_python-0.1.0 → sendly_python-1.0.0}/.github/workflows/ci.yml +18 -6
  2. sendly_python-1.0.0/CHANGELOG.md +138 -0
  3. sendly_python-1.0.0/PKG-INFO +653 -0
  4. sendly_python-1.0.0/README.md +621 -0
  5. {sendly_python-0.1.0 → sendly_python-1.0.0}/pyproject.toml +1 -1
  6. sendly_python-1.0.0/scripts/sync_spec.py +301 -0
  7. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/__init__.py +25 -1
  8. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/client.py +52 -8
  9. sendly_python-1.0.0/src/sendly/errors.py +196 -0
  10. sendly_python-1.0.0/src/sendly/resources/_pagination.py +49 -0
  11. sendly_python-1.0.0/src/sendly/resources/analytics.py +49 -0
  12. sendly_python-1.0.0/src/sendly/resources/campaigns.py +135 -0
  13. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/domains.py +16 -0
  14. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/emails.py +50 -3
  15. sendly_python-1.0.0/src/sendly/resources/events.py +98 -0
  16. sendly_python-1.0.0/src/sendly/resources/lists.py +59 -0
  17. sendly_python-1.0.0/src/sendly/resources/mailboxes.py +75 -0
  18. sendly_python-1.0.0/src/sendly/resources/projects.py +33 -0
  19. sendly_python-1.0.0/src/sendly/resources/segments.py +106 -0
  20. sendly_python-1.0.0/src/sendly/resources/usage.py +27 -0
  21. sendly_python-1.0.0/src/sendly/resources/workflows.py +139 -0
  22. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/types.py +65 -0
  23. sendly_python-1.0.0/tests/fixtures/openapi.json +14791 -0
  24. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/support.py +39 -0
  25. sendly_python-1.0.0/tests/test_analytics.py +70 -0
  26. sendly_python-1.0.0/tests/test_campaigns.py +262 -0
  27. sendly_python-1.0.0/tests/test_contract.py +480 -0
  28. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_domains.py +17 -0
  29. sendly_python-1.0.0/tests/test_emails.py +225 -0
  30. sendly_python-1.0.0/tests/test_errors_problem.py +198 -0
  31. sendly_python-1.0.0/tests/test_events.py +192 -0
  32. sendly_python-1.0.0/tests/test_lists.py +123 -0
  33. sendly_python-1.0.0/tests/test_mailboxes.py +99 -0
  34. sendly_python-1.0.0/tests/test_projects.py +58 -0
  35. sendly_python-1.0.0/tests/test_segments.py +125 -0
  36. sendly_python-1.0.0/tests/test_usage.py +56 -0
  37. sendly_python-1.0.0/tests/test_workflows.py +159 -0
  38. sendly_python-0.1.0/CHANGELOG.md +0 -29
  39. sendly_python-0.1.0/PKG-INFO +0 -311
  40. sendly_python-0.1.0/README.md +0 -279
  41. sendly_python-0.1.0/scripts/sync_spec.py +0 -140
  42. sendly_python-0.1.0/src/sendly/errors.py +0 -92
  43. sendly_python-0.1.0/src/sendly/resources/events.py +0 -26
  44. sendly_python-0.1.0/tests/fixtures/openapi.json +0 -5729
  45. sendly_python-0.1.0/tests/test_contract.py +0 -255
  46. sendly_python-0.1.0/tests/test_emails.py +0 -94
  47. sendly_python-0.1.0/tests/test_events.py +0 -63
  48. {sendly_python-0.1.0 → sendly_python-1.0.0}/.github/workflows/release.yml +0 -0
  49. {sendly_python-0.1.0 → sendly_python-1.0.0}/.gitignore +0 -0
  50. {sendly_python-0.1.0 → sendly_python-1.0.0}/LICENSE +0 -0
  51. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/py.typed +0 -0
  52. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/__init__.py +0 -0
  53. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/_helpers.py +0 -0
  54. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/contacts.py +0 -0
  55. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/suppression.py +0 -0
  56. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/templates.py +0 -0
  57. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/verify.py +0 -0
  58. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/resources/webhooks.py +0 -0
  59. {sendly_python-0.1.0 → sendly_python-1.0.0}/src/sendly/webhook_utils.py +0 -0
  60. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_client.py +0 -0
  61. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_contacts.py +0 -0
  62. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_suppression.py +0 -0
  63. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_templates.py +0 -0
  64. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_verify.py +0 -0
  65. {sendly_python-0.1.0 → sendly_python-1.0.0}/tests/test_webhook_verify.py +0 -0
  66. {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 live OpenAPI drift even without a push.
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 live API so drift shows
42
- # up as a warning annotation without failing the build. Runs once (on the
43
- # newest interpreter) to avoid duplicate live fetches and annotations.
44
- - name: Live OpenAPI drift check (non-blocking)
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 live spec at https://api.sendly.now/api/openapi.json. Run 'python scripts/sync_spec.py' and commit the refreshed copy."
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.