sendly-python 1.1.0__py3-none-any.whl → 1.2.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.
sendly/__init__.py CHANGED
@@ -50,6 +50,7 @@ from sendly.resources.validation import ValidationResource
50
50
  from sendly.resources.verify import VerifyResource
51
51
  from sendly.resources.webhooks import WebhooksResource
52
52
  from sendly.resources.workflows import WorkflowsResource
53
+ from sendly.types import SendlyFile
53
54
  from sendly.webhook_utils import DEFAULT_TOLERANCE_MS, construct_event, verify_signature
54
55
 
55
56
  __version__ = SDK_VERSION
@@ -74,6 +75,7 @@ __all__ = [
74
75
  "SendlyConflictError",
75
76
  "SendlyConnectionError",
76
77
  "SendlyError",
78
+ "SendlyFile",
77
79
  "SendlyNotFoundError",
78
80
  "SendlyPermissionError",
79
81
  "SendlyRateLimitError",
sendly/client.py CHANGED
@@ -53,6 +53,7 @@ from sendly.resources.validation import ValidationResource
53
53
  from sendly.resources.verify import VerifyResource
54
54
  from sendly.resources.webhooks import WebhooksResource
55
55
  from sendly.resources.workflows import WorkflowsResource
56
+ from sendly.types import SendlyFile
56
57
 
57
58
  if TYPE_CHECKING:
58
59
  from collections.abc import Mapping
@@ -63,7 +64,7 @@ if TYPE_CHECKING:
63
64
  __all__ = ["DEFAULT_BASE_URL", "SDK_VERSION", "Sendly"]
64
65
 
65
66
  #: Package version. Kept in sync with ``pyproject.toml``.
66
- SDK_VERSION = "1.1.0"
67
+ SDK_VERSION = "1.2.0"
67
68
 
68
69
  #: Default production API base. Override via ``base_url`` for staging/self-hosted.
69
70
  DEFAULT_BASE_URL = "https://api.sendly.now"
@@ -174,30 +175,7 @@ class Sendly:
174
175
  success body, or ``None`` for 204 / ``no_content``. Errors are raised as
175
176
  :class:`SendlyError` subclasses based on status.
176
177
  """
177
- url = self._build_url(path, query)
178
- request_headers: dict[str, str] = {
179
- "Authorization": f"Bearer {self._api_key}",
180
- "Accept": "application/json",
181
- "User-Agent": USER_AGENT,
182
- **self._default_headers,
183
- **(dict(headers) if headers else {}),
184
- }
185
-
186
- content: str | None = None
187
- if body is not None:
188
- request_headers["Content-Type"] = "application/json"
189
- content = json.dumps(body)
190
-
191
- try:
192
- response = self._client.request(
193
- method,
194
- url,
195
- content=content,
196
- headers=request_headers,
197
- timeout=self._timeout,
198
- )
199
- except httpx.HTTPError as exc:
200
- raise SendlyConnectionError(f"Sendly request failed: {exc}", exc) from exc
178
+ response = self._send(method=method, path=path, body=body, query=query, headers=headers)
201
179
 
202
180
  # 204 No Content or caller-forced no-content (DELETE endpoints, which
203
181
  # now respond 200 with an id body the SDK intentionally discards).
@@ -229,6 +207,67 @@ class Sendly:
229
207
 
230
208
  return parsed
231
209
 
210
+ def request_bytes(
211
+ self,
212
+ *,
213
+ method: str,
214
+ path: str,
215
+ query: Query | None = None,
216
+ headers: Mapping[str, str] | None = None,
217
+ ) -> SendlyFile:
218
+ """Like :meth:`request`, for an operation whose success body is a file.
219
+
220
+ Returns the raw bytes with the response's own ``Content-Type`` and
221
+ ``Content-Disposition``. A failure is still a JSON error document and is
222
+ raised as the same :class:`SendlyError` subclasses. Reading the body as
223
+ text, as :meth:`request` does, would mangle any file that is not UTF-8.
224
+ """
225
+ response = self._send(
226
+ method=method, path=path, query=query, headers={"Accept": "*/*", **(headers or {})}
227
+ )
228
+ if not response.is_success:
229
+ self._raise_for_error(response)
230
+ return SendlyFile(
231
+ data=response.content,
232
+ content_type=response.headers.get("content-type"),
233
+ content_disposition=response.headers.get("content-disposition"),
234
+ )
235
+
236
+ def _send(
237
+ self,
238
+ *,
239
+ method: str,
240
+ path: str,
241
+ body: Body | None = None,
242
+ query: Query | None = None,
243
+ headers: Mapping[str, str] | None = None,
244
+ ) -> httpx.Response:
245
+ """Issue the request; errors and parsing are the caller's."""
246
+ url = self._build_url(path, query)
247
+ request_headers: dict[str, str] = {
248
+ "Authorization": f"Bearer {self._api_key}",
249
+ "Accept": "application/json",
250
+ "User-Agent": USER_AGENT,
251
+ **self._default_headers,
252
+ **(dict(headers) if headers else {}),
253
+ }
254
+
255
+ content: str | None = None
256
+ if body is not None:
257
+ request_headers["Content-Type"] = "application/json"
258
+ content = json.dumps(body)
259
+
260
+ try:
261
+ return self._client.request(
262
+ method,
263
+ url,
264
+ content=content,
265
+ headers=request_headers,
266
+ timeout=self._timeout,
267
+ )
268
+ except httpx.HTTPError as exc:
269
+ raise SendlyConnectionError(f"Sendly request failed: {exc}", exc) from exc
270
+
232
271
  def unwrap(self, envelope: Any) -> Any:
233
272
  """Return the ``data`` field of a ``{success, data}`` envelope, else the
234
273
  value unchanged. Centralizing this keeps resource code clean.
@@ -16,6 +16,8 @@ if TYPE_CHECKING:
16
16
  DomainDeletedV1,
17
17
  DomainListResponse,
18
18
  DomainListV1,
19
+ DomainLogoReadinessV1,
20
+ DomainLogoV1,
19
21
  DomainRecord,
20
22
  DomainSetupSession,
21
23
  DomainV1,
@@ -208,3 +210,59 @@ class DomainsResource:
208
210
  method="DELETE", path=f"/api/v1/domains/{encode_path_segment(id)}"
209
211
  )
210
212
  return response
213
+
214
+ def get_logo_v1(self, id: str) -> DomainLogoV1:
215
+ """The logo this identity serves to inboxes (BIMI), the
216
+ ``default._bimi`` TXT record that makes them fetch it (``bimi_record``),
217
+ and a fresh DNS reading of whether they will show it today.
218
+
219
+ The URL never changes, so the record is published once. Until a logo is
220
+ uploaded it serves a default (the domain's initial), and it serves
221
+ nothing until the identity verifies. Providers decide whether to show a
222
+ logo; Gmail also needs a VMC or CMC certificate, which Sendly neither
223
+ issues nor checks. :meth:`get_logo_readiness_v1` is the full checklist.
224
+ """
225
+ response: DomainLogoV1 = self._client.request(
226
+ method="GET", path=f"/api/v1/domains/{encode_path_segment(id)}/logo"
227
+ )
228
+ return response
229
+
230
+ def upload_logo_v1(self, id: str, body: Body) -> DomainLogoV1:
231
+ """Replace the logo with an SVG: ``{"svg": "<svg ...>"}``.
232
+
233
+ It is rebuilt into SVG Tiny PS, the profile BIMI requires: square, shapes
234
+ rather than ``<text>``, nothing referenced outside itself, at most 32 KB
235
+ once rebuilt. A file that cannot be rebuilt answers 422
236
+ ``validation_error`` with a ``detail`` naming what to change, and the
237
+ previous logo stays in place.
238
+ """
239
+ response: DomainLogoV1 = self._client.request(
240
+ method="PUT", path=f"/api/v1/domains/{encode_path_segment(id)}/logo", body=body
241
+ )
242
+ return response
243
+
244
+ def delete_logo_v1(self, id: str) -> DomainLogoV1:
245
+ """Remove the uploaded logo, so the URL serves the default again.
246
+
247
+ Returns the logo now served; removing one that was never uploaded is not
248
+ an error. The DNS record does not change.
249
+ """
250
+ response: DomainLogoV1 = self._client.request(
251
+ method="DELETE", path=f"/api/v1/domains/{encode_path_segment(id)}/logo"
252
+ )
253
+ return response
254
+
255
+ def get_logo_readiness_v1(self, id: str) -> DomainLogoReadinessV1:
256
+ """How ready the identity is to show its logo as the inbox avatar.
257
+
258
+ Each step (DMARC at enforcement on the host and its organizational
259
+ domain, the BIMI record, the logo file, the mark certificate) comes back
260
+ ``met``, ``missing``, ``unknown`` or ``not_applicable`` with a
261
+ plain-English ``reason``, plus one checklist per inbox (Gmail, Yahoo,
262
+ Apple Mail, Fastmail). For the certificate only "the URL answers with a
263
+ PEM file" is checked; its chain is not validated.
264
+ """
265
+ response: DomainLogoReadinessV1 = self._client.request(
266
+ method="GET", path=f"/api/v1/domains/{encode_path_segment(id)}/logo/readiness"
267
+ )
268
+ return response
@@ -7,8 +7,21 @@ from typing import TYPE_CHECKING
7
7
  from sendly.resources._helpers import encode_path_segment
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Iterator
11
+
10
12
  from sendly.client import Sendly
11
- from sendly.types import AppPasswordList, Body, JSONDict, MailboxDetail, MailboxList
13
+ from sendly.types import (
14
+ AppPasswordList,
15
+ Body,
16
+ JSONDict,
17
+ MailboxConversation,
18
+ MailboxConversationPage,
19
+ MailboxDetail,
20
+ MailboxList,
21
+ MailboxMessageList,
22
+ Query,
23
+ SendlyFile,
24
+ )
12
25
 
13
26
 
14
27
  class MailboxesResource:
@@ -42,8 +55,8 @@ class MailboxesResource:
42
55
  project's whole allowance, and they are still returned here: a project
43
56
  with a run of failed provisions can therefore list more than 10.
44
57
 
45
- This lists the mailboxes themselves, never their contents: received
46
- messages are not part of the public API.
58
+ This lists the mailboxes themselves, never their contents: the mail is
59
+ :meth:`list_conversations`, under a scope of its own.
47
60
  """
48
61
  envelope = self._client.request(method="GET", path="/api/mailboxes")
49
62
  records: MailboxList = self._client.unwrap(envelope)
@@ -131,3 +144,103 @@ class MailboxesResource:
131
144
  )
132
145
  draft: JSONDict = self._client.unwrap(envelope)
133
146
  return draft
147
+
148
+ # The mail itself. The methods below read and answer correspondence from
149
+ # people outside your project, so they need ``mailbox-conversations:read``
150
+ # (and ``mailbox-conversations:reply`` to answer). Both scopes are opt-in
151
+ # only: a key holds one only if it was ticked by name when it was created,
152
+ # and neither the legacy full-access key nor the "Full access" preset has
153
+ # it. An existing key gets 403 until you mint one that does.
154
+
155
+ def list_conversations(self, id: str, query: Query | None = None) -> MailboxConversationPage:
156
+ """One page of a mailbox's threads, newest activity first.
157
+
158
+ ``query`` takes ``view`` (``inbox``, the default, or ``spam``),
159
+ ``status`` (``open``, ``snoozed``, ``closed`` or ``all``), ``q`` (a
160
+ substring of the subject or the correspondent's address), ``cursor``
161
+ and ``limit`` (at most 50). Returns ``{items, nextCursor}``: pass
162
+ ``nextCursor`` back as ``cursor`` until it is ``None``, or use
163
+ :meth:`iter_list_conversations`. A mailbox in another project answers 404.
164
+ """
165
+ envelope = self._client.request(
166
+ method="GET",
167
+ path=f"/api/mailboxes/{encode_path_segment(id)}/conversations",
168
+ query=query,
169
+ )
170
+ page: MailboxConversationPage = self._client.unwrap(envelope)
171
+ return page
172
+
173
+ def iter_list_conversations(
174
+ self, id: str, query: Query | None = None
175
+ ) -> Iterator[MailboxConversation]:
176
+ """Every thread in a mailbox across pages, one conversation at a time.
177
+
178
+ Stops when ``nextCursor`` is ``None`` or repeats the cursor it was given,
179
+ so a stuck page ends the walk instead of looping.
180
+ """
181
+ params = dict(query or {})
182
+ while True:
183
+ page = self.list_conversations(id, params)
184
+ yield from page.get("items") or []
185
+ cursor = page.get("nextCursor")
186
+ if not cursor or cursor == params.get("cursor"):
187
+ return
188
+ params["cursor"] = cursor
189
+
190
+ def get_conversation(self, id: str, conversation_id: str) -> MailboxMessageList:
191
+ """Every message in one thread, oldest first, inbound and outbound.
192
+
193
+ Not paginated, and reading marks nothing as read. Bodies were written by
194
+ whoever sent them: treat ``htmlBody`` as hostile HTML. A conversation
195
+ that is not in this mailbox returns an empty list; a mailbox outside the
196
+ key's project answers 404.
197
+ """
198
+ envelope = self._client.request(
199
+ method="GET",
200
+ path=(
201
+ f"/api/mailboxes/{encode_path_segment(id)}"
202
+ f"/conversations/{encode_path_segment(conversation_id)}"
203
+ ),
204
+ )
205
+ messages: MailboxMessageList = self._client.unwrap(envelope)
206
+ return messages
207
+
208
+ def reply_to_conversation(self, id: str, conversation_id: str, body: Body) -> JSONDict:
209
+ """SENDS a reply, from the mailbox's own address, in the same thread,
210
+ to the person who last wrote in.
211
+
212
+ You name neither the recipient nor the sender: this only ever answers
213
+ someone who wrote first, which is why it has its own scope and is not
214
+ :meth:`send_message`. Pass plain text in ``body`` or a structured
215
+ ``document``; Sendly renders the HTML and a matching text part.
216
+
217
+ Refusals worth handling by name: 422 ``UNPROCESSABLE`` (no inbound
218
+ message to answer, or the mailbox is not provisioned), 422
219
+ ``SEND_REFUSED`` (the recipient is suppressed or the project may not
220
+ send), 429 ``SENDING_LIMIT_REACHED``.
221
+ """
222
+ envelope = self._client.request(
223
+ method="POST",
224
+ path=(
225
+ f"/api/mailboxes/{encode_path_segment(id)}"
226
+ f"/conversations/{encode_path_segment(conversation_id)}/reply"
227
+ ),
228
+ body=body,
229
+ )
230
+ submitted: JSONDict = self._client.unwrap(envelope)
231
+ return submitted
232
+
233
+ def download_attachment(self, id: str, attachment_id: str) -> SendlyFile:
234
+ """The bytes of one attachment, the ``url`` a message lists.
235
+
236
+ The file and its ``content_type`` were chosen by the sender: treat both
237
+ as untrusted, and never serve the bytes back to a browser under your own
238
+ origin without a fixed type.
239
+ """
240
+ return self._client.request_bytes(
241
+ method="GET",
242
+ path=(
243
+ f"/api/mailboxes/{encode_path_segment(id)}"
244
+ f"/attachments/{encode_path_segment(attachment_id)}"
245
+ ),
246
+ )
sendly/types.py CHANGED
@@ -15,6 +15,7 @@ same across both SDKs.
15
15
  from __future__ import annotations
16
16
 
17
17
  from collections.abc import Mapping
18
+ from dataclasses import dataclass
18
19
  from typing import Any
19
20
 
20
21
  # ---------- Generic JSON ----------
@@ -84,6 +85,26 @@ AppPasswordRecord = JSONDict
84
85
  #: types here sidesteps the shadowing and keeps the annotations readable.
85
86
  MailboxList = list[JSONDict]
86
87
  AppPasswordList = list[JSONDict]
88
+ #: One page of a mailbox's threads: ``{items, nextCursor}``.
89
+ MailboxConversationPage = JSONDict
90
+ #: One thread in a mailbox: correspondent, subject, status, last activity.
91
+ MailboxConversation = JSONDict
92
+ #: A thread's messages, oldest first. Bodies are the sender's and untrusted.
93
+ MailboxMessageList = list[JSONDict]
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class SendlyFile:
98
+ """A file the API answered with, as :meth:`Sendly.request_bytes` returns it."""
99
+
100
+ #: The body, byte for byte.
101
+ data: bytes
102
+ #: As the server declared it. For a mailbox attachment that is the SENDER's
103
+ #: claim: do not trust it.
104
+ content_type: str | None
105
+ #: ``attachment; filename="..."`` or ``inline; ...``, verbatim.
106
+ content_disposition: str | None
107
+
87
108
 
88
109
  # ---------- Projects (v1) ----------
89
110
 
@@ -193,6 +214,10 @@ TemplateDeletedV1 = JSONDict
193
214
  DomainV1 = JSONDict
194
215
  DomainListV1 = CursorList
195
216
  DomainDeletedV1 = JSONDict
217
+ #: The logo an identity serves to inboxes, its BIMI record, and a DNS readiness reading.
218
+ DomainLogoV1 = JSONDict
219
+ #: The per-step, per-inbox checklist for showing the logo as the inbox avatar.
220
+ DomainLogoReadinessV1 = JSONDict
196
221
 
197
222
  # ---------- Webhooks (v1) ----------
198
223
 
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sendly-python
3
- Version: 1.1.0
4
- Summary: Official Sendly Python SDK
3
+ Version: 1.2.0
4
+ Summary: Official Python SDK for Sendly (sendly.now), the email API and platform for developers by Devino Solutions: transactional email, contacts, templates and webhooks.
5
5
  Project-URL: Homepage, https://sendly.now
6
6
  Project-URL: Documentation, https://docs.sendly.now
7
7
  Project-URL: Repository, https://github.com/DevinoSolutions/sendly-python
8
8
  Author-email: Devino Solutions <dev@devino.ca>
9
9
  License: MIT
10
10
  License-File: LICENSE
11
- Keywords: api,email,sdk,sendly,transactional-email,webhooks
11
+ Keywords: api,email,email-api,sdk,sendly,sendly.now,transactional-email,webhooks
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
@@ -32,6 +32,18 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  # Sendly Python SDK
34
34
 
35
+ **Sendly is an email platform for developers: one REST API for transactional email, marketing
36
+ campaigns and visual automation workflows, built on AWS SES, with contacts, segments, templates,
37
+ webhooks and an event stream behind the same account.** It offers a free tier and metered
38
+ pay-as-you-go sending. Sendly is made by Devino Solutions.
39
+
40
+ What Sendly is not: not a multi-channel messaging suite (email is the only channel; no SMS, push or
41
+ in-app messages), not a marketing suite (no landing pages, forms, ads or CRM), and not related to
42
+ other products or companies that use the word "Sendly". The PyPI package `sendly` is not ours;
43
+ this SDK is published as `sendly-python`.
44
+
45
+ Canonical site: https://sendly.now · Docs: https://docs.sendly.now · API: https://api.sendly.now
46
+
35
47
  Official Python SDK for the [Sendly](https://sendly.now) REST API —
36
48
  transactional email, contacts, lists, topics, events, domains, templates,
37
49
  snippets, email verification, address validation, deliverability reporting,
@@ -265,6 +277,13 @@ receipt = sendly.emails.send({"from": "a@you.com", "to": "b@them.com", "subject"
265
277
  sendly.emails.send_legacy({"from": "a@you.com", "to": ["b@them.com", "c@them.com"],
266
278
  "subject": "Hi", "body": "<p>Hi</p>"})
267
279
 
280
+ # Your own one-click unsubscribe (https only). Sendly writes the RFC 8058
281
+ # List-Unsubscribe headers, so the mail client shows its unsubscribe button; the
282
+ # endpoint gets a cookie-less POST and must unsubscribe the reader on it alone.
283
+ sendly.emails.send({"from": "notify@you.com", "to": "b@them.com", "subject": "New reply",
284
+ "body": "<p>Someone replied.</p>",
285
+ "listUnsubscribe": "https://you.com/unsubscribe?token=abc123"})
286
+
268
287
  # Batch send (up to 100)
269
288
  sendly.emails.batch({"emails": [{"from": "a@you.com", "to": "b@them.com", "subject": "Hi", "body": "<p>Hi</p>"}]})
270
289
 
@@ -464,6 +483,28 @@ neither is a spelling of the other. `verify_v1` verifies nothing itself:
464
483
  verification happens in the domain's DNS when its owner publishes the DKIM
465
484
  records SES minted at creation, and this call asks SES what it currently sees.
466
485
 
486
+ #### The sender logo (BIMI)
487
+
488
+ Each identity serves a logo that BIMI-capable inboxes show beside its mail, once
489
+ its DNS is ready:
490
+
491
+ ```python
492
+ logo = sendly.domains.get_logo_v1("d_123")
493
+ print(logo["bimi_record"]) # publish this TXT record once; the URL never changes
494
+
495
+ sendly.domains.upload_logo_v1("d_123", {"svg": svg_source}) # square, shapes not <text>
496
+ for item in sendly.domains.get_logo_readiness_v1("d_123")["items"]:
497
+ print(item["key"], item["status"], item["reason"])
498
+ ```
499
+
500
+ The upload is rebuilt into SVG Tiny PS, the profile BIMI requires. A file that
501
+ cannot be rebuilt (text not converted to outlines, a raster image, a canvas that
502
+ is not square, over 32 KB once cleaned) is a 422 `validation_error` naming what to
503
+ change, and the previous logo stays. `delete_logo_v1` goes back to the default
504
+ (the domain's initial). Providers still decide whether to show a logo, and Gmail
505
+ and Apple Mail need a VMC or CMC certificate, which Sendly neither issues nor
506
+ validates.
507
+
467
508
  ### Mailboxes
468
509
 
469
510
  Receiving mailboxes on the project's verified domains. The reads are reads; the
@@ -484,8 +525,8 @@ for pw in sendly.mailboxes.list_app_passwords("mb_123"):
484
525
  print(pw["name"], pw["lastFour"], pw["lastUsedAt"])
485
526
  ```
486
527
 
487
- This lists the mailboxes themselves, never their contents — received messages
488
- are not part of the public API. The mailbox **password** is never returned by
528
+ `list` and `get` describe the mailboxes themselves, never their contents (the
529
+ mail is below, under a scope of its own). The mailbox **password** is never returned by
489
530
  any of these reads; mailbox credentials are app passwords, created from the
490
531
  dashboard and shown once. `list_app_passwords` returns only the passwords that
491
532
  are still active — a revoked one drops out, so this is not an audit history.
@@ -541,6 +582,29 @@ treated strictly as data describing what to write, never as instructions to the
541
582
  model. Capped at 120 requests an hour per project; 502 means the model was
542
583
  unreachable.
543
584
 
585
+ #### The mail itself: read, reply, download
586
+
587
+ These read correspondence from people outside your project, so they need
588
+ `mailbox-conversations:read`, and answering needs `mailbox-conversations:reply`.
589
+ Both scopes are **opt-in only**: a key holds one only if it was ticked by name
590
+ when the key was created, and neither the legacy full-access key nor the "Full
591
+ access" preset carries it. An existing key gets 403 until you mint one that does.
592
+
593
+ ```python
594
+ for thread in sendly.mailboxes.iter_list_conversations("mb_123", {"status": "open"}):
595
+ messages = sendly.mailboxes.get_conversation("mb_123", thread["id"]) # oldest first
596
+
597
+ sendly.mailboxes.reply_to_conversation("mb_123", thread_id, {"body": "Sorted, thanks."})
598
+
599
+ file = sendly.mailboxes.download_attachment("mb_123", attachment_id)
600
+ # file.data is bytes; file.content_type is the SENDER's claim.
601
+ ```
602
+
603
+ A reply names neither the recipient nor the sender: it answers whoever last wrote
604
+ in, from the mailbox's own address, in the same thread. Message bodies and
605
+ attachments were written by the sender, so treat `htmlBody`, the bytes and their
606
+ content type as untrusted. Reading marks nothing as read.
607
+
544
608
  ### Templates and snippets
545
609
 
546
610
  ```python
@@ -1,8 +1,8 @@
1
- sendly/__init__.py,sha256=64KKJ8Iha5nWhKFjV82adsgYD-kN-w7dsKkPV5DB-eM,3089
2
- sendly/client.py,sha256=nEnHn0WVAqvXp0U4G6baUnA1x6ujE4M8JQMhU5bi2kw,12077
1
+ sendly/__init__.py,sha256=KNGjrzKPY88Qo8J4LCWxa4Ae0Dwk1JIREQ9IFF2R8bY,3143
2
+ sendly/client.py,sha256=a_2zC-EdBlvAyOk01iBIwG0jSzQwxwM9fJL2dlHLPXA,13536
3
3
  sendly/errors.py,sha256=urF33X-d39OEiasLh6C7PlrrUmmrS3XIMvFYj20rUy8,7002
4
4
  sendly/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- sendly/types.py,sha256=ug4nOQcQtCDcNkuUmlDtft586d1gsLUR242T8mioXLI,6629
5
+ sendly/types.py,sha256=bFEjl4twmwtCUdw5zO5QF8C3d3dn1s63cghJCrMzkXM,7625
6
6
  sendly/webhook_utils.py,sha256=epmK860gLknaElxfXdVD3h_9ASkjWQsyl1exUC8qBL4,4762
7
7
  sendly/resources/__init__.py,sha256=Nf7zjcFjsztXsGtrvxq3KGacX7SwDgSf3r0nAVHLvPY,73
8
8
  sendly/resources/_helpers.py,sha256=W6AlzXuqH53Vk1dIxuQm_-Vpq9gmZvLzKPRionjH9wQ,550
@@ -11,11 +11,11 @@ sendly/resources/analytics.py,sha256=7w4A-w-dXZkgLi2j1dO6X88V-T9itlF0rWjdLiFAStc
11
11
  sendly/resources/campaigns.py,sha256=r9XQcw9z1aCAkQic56vajh-Rscbc8jA0YjV4kIBalW4,7382
12
12
  sendly/resources/contacts.py,sha256=JTmj5jAu46i6suz16U3cubr1P4sM0lkpHo8AcK5JDcM,7624
13
13
  sendly/resources/deliverability.py,sha256=e69_THrIZMN7BP6i8IvTcSbeX7t3FoFZwhj3BpgW-ZE,4877
14
- sendly/resources/domains.py,sha256=oTLXLG6yY5GeNltuEOR0qZVMUl1YYd8Nmzt-xeO33As,9140
14
+ sendly/resources/domains.py,sha256=ZRH0KmtaELJkbNTg-P-T5lJAHAgxbQe1LiMd9pqxfZM,11946
15
15
  sendly/resources/emails.py,sha256=ukaXSBRV3kDXism4ndfIYIluoubChH4VdH65q0A9hvI,5227
16
16
  sendly/resources/events.py,sha256=hvJpX7Q2HmUkO1mxnzFBYhoP5NfWDz2TUb2OJ1bCaQ8,4071
17
17
  sendly/resources/lists.py,sha256=rwBLIk9Q5w_TPlDtCZ4a8yne1JQnZ0U4qa1mMq-1tGs,6044
18
- sendly/resources/mailboxes.py,sha256=Jnr5g3gmyFUm9TJnX3lbyDL1wyOofqJHl3Q46ilRn2c,6095
18
+ sendly/resources/mailboxes.py,sha256=QZmwd1sADTLjoPgvb8t4t4sQeoYjGPo5aE0Tnv80Wj4,10903
19
19
  sendly/resources/projects.py,sha256=g694LMqh7cI2qCD1nZ6QQEfOvT42A8EBltbEJawc2P8,1096
20
20
  sendly/resources/segments.py,sha256=dc4KzorLRSfkZe63J144wm6W5bjVaDA4U8ID7HyyK_U,3944
21
21
  sendly/resources/snippets.py,sha256=BjuMWSprTAtzF6nrqd04iC9lRc68Lf3kKMZWeNrUyMA,2645
@@ -27,7 +27,7 @@ sendly/resources/validation.py,sha256=sS0gAs5Kph-QK73YZ7dkZCUP2ZPiDtyO1DLlQuZAle
27
27
  sendly/resources/verify.py,sha256=QyVNmrzZ4Nj87YWxMPt9wlovcCyZutV9eGmn49c0EEg,818
28
28
  sendly/resources/webhooks.py,sha256=w0ZwueAGKTP6Du3b6jMr24C9nv_a_vnDExYYy3N1pfA,8036
29
29
  sendly/resources/workflows.py,sha256=wZUpL57lrKmgcdjNaLS89lyEG2D1Ov2EKfXSlcQFn74,9768
30
- sendly_python-1.1.0.dist-info/METADATA,sha256=Yfup-6i5MQfru2u4CA6E6MxTJ_U09vXWIKeRSGO_9z4,56285
31
- sendly_python-1.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
32
- sendly_python-1.1.0.dist-info/licenses/LICENSE,sha256=TZHT_f_vV-xujWjSy7EkTybNT6iivrfGHLd-rR1Xl7c,1073
33
- sendly_python-1.1.0.dist-info/RECORD,,
30
+ sendly_python-1.2.0.dist-info/METADATA,sha256=ZrtlQzGFqwMHFh0H9j8pUeZyA8P9zLs5rE839FedC_M,59817
31
+ sendly_python-1.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
32
+ sendly_python-1.2.0.dist-info/licenses/LICENSE,sha256=TZHT_f_vV-xujWjSy7EkTybNT6iivrfGHLd-rR1Xl7c,1073
33
+ sendly_python-1.2.0.dist-info/RECORD,,