sendora 2.3.0__tar.gz → 2.4.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.
- {sendora-2.3.0 → sendora-2.4.0}/CHANGELOG.md +6 -0
- {sendora-2.3.0 → sendora-2.4.0}/PKG-INFO +26 -3
- {sendora-2.3.0 → sendora-2.4.0}/README.md +25 -2
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_client.py +5 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_version.py +1 -1
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/__init__.py +3 -0
- sendora-2.4.0/src/sendora/resources/tracking_opt_outs.py +226 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/skills/sendora/SKILL.md +5 -1
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/types.py +41 -0
- {sendora-2.3.0 → sendora-2.4.0}/.gitignore +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/LICENSE +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/pyproject.toml +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/__init__.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_account.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_answers.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_errors.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_json.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_keys.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_model.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_options.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_pages.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_retry.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_transport.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_webhooks.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_wire.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/py.typed +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/broadcasts.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/domains.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/email.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/erasures.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/inbound.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/inbound_domains.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/messages.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/server.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/servers.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/streams.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/suppressions.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/tokens.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/tracking_domains.py +0 -0
- {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/webhooks.py +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
- `sendora.tracking_opt_outs` manages a server's not-tracked list: `list`,
|
|
6
|
+
`list_all`, `add` and `delete`. An entry names its `address`, which is
|
|
7
|
+
`None` once an erasure has left only a keyed hash, and its `source`.
|
|
8
|
+
|
|
3
9
|
## 2.3.0
|
|
4
10
|
|
|
5
11
|
- Two events, `OpenedEvent` and `ClickedEvent`, for `opened` and `clicked`:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sendora
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The Python client for Sendora's email API.
|
|
5
5
|
Project-URL: Homepage, https://sendora.se/docs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,7 +33,7 @@ pip install sendora
|
|
|
33
33
|
|
|
34
34
|
Two kinds of key, two clients. `Sendora` takes a server key (`sk_…`) and
|
|
35
35
|
does everything inside one server: sending, broadcasts, the log, received
|
|
36
|
-
mail, streams, suppressions, tokens, webhooks and the server's own limits.
|
|
36
|
+
mail, streams, suppressions, the not-tracked list, tokens, webhooks and the server's own limits.
|
|
37
37
|
`SendoraAccount` takes an account key (`ak_…`) and manages the account:
|
|
38
38
|
its servers, their keys, its sending and tracking domains. `AsyncSendora` and
|
|
39
39
|
`AsyncSendoraAccount` are the same clients for asyncio. Both keys are
|
|
@@ -246,6 +246,29 @@ request. A refused send carries `stream_id` beside `suppressed`.
|
|
|
246
246
|
sendora.suppressions.delete(address="anna@example.com")
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
## The not-tracked list
|
|
250
|
+
|
|
251
|
+
Addresses the server never tracks, on any of its streams. Mail to one still
|
|
252
|
+
goes out, untracked, and its opens and clicks of mail already sent are not
|
|
253
|
+
recorded. Tracking is not open to accounts yet, so the list changes nothing
|
|
254
|
+
until it is.
|
|
255
|
+
|
|
256
|
+
| Call | Answers |
|
|
257
|
+
| ------------------------------------------------ | -------------------------------------------------------- |
|
|
258
|
+
| `sendora.tracking_opt_outs.list(limit=, after=)` | `TrackingOptOutPage` with `tracking_opt_outs` and `next` |
|
|
259
|
+
| `sendora.tracking_opt_outs.list_all()` | Every entry, as an iterator |
|
|
260
|
+
| `sendora.tracking_opt_outs.add(address=)` | `TrackingOptOut`, as it stands if the address was listed |
|
|
261
|
+
| `sendora.tracking_opt_outs.delete(address=)` | `None`; `not_found` if the address is not listed |
|
|
262
|
+
|
|
263
|
+
An entry's `source` is `manual`, or `recipient` if the recipient asked for it.
|
|
264
|
+
Remove a recipient's own entry only after new consent. An entry's `address` is
|
|
265
|
+
`None` once an erasure has left only a keyed hash of it. `delete` still finds
|
|
266
|
+
that entry by its address, and nothing removes it without the address.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
sendora.tracking_opt_outs.add(address="anna@example.com")
|
|
270
|
+
```
|
|
271
|
+
|
|
249
272
|
## Tokens
|
|
250
273
|
|
|
251
274
|
The server's own keys, from its own key. Every key of a server has the
|
|
@@ -532,7 +555,7 @@ except SendoraError as error:
|
|
|
532
555
|
| `account_not_active` | 403 | The account is not approved to send, or not active for an inbound stream. |
|
|
533
556
|
| `spam_complaint_locked` | 403 | Only support can lift a spam-complaint suppression. |
|
|
534
557
|
| `unsubscribe_locked` | 403 | Only support can lift a recipient's own unsubscribe. |
|
|
535
|
-
| `not_found` | 404 | No such id on this server, or no such suppressed address.
|
|
558
|
+
| `not_found` | 404 | No such id on this server, or no such suppressed or not-tracked address. |
|
|
536
559
|
| `domain_exists` | 409 | The account already has the domain; `existing_id` names it. |
|
|
537
560
|
| `domain_reserved` | 400 | The name is Sendora's own; no account can receive on it. |
|
|
538
561
|
| `tracking_domain_exists` | 409 | The account already has the host, or one on that sending domain; `existing_id` names it. |
|
|
@@ -11,7 +11,7 @@ pip install sendora
|
|
|
11
11
|
|
|
12
12
|
Two kinds of key, two clients. `Sendora` takes a server key (`sk_…`) and
|
|
13
13
|
does everything inside one server: sending, broadcasts, the log, received
|
|
14
|
-
mail, streams, suppressions, tokens, webhooks and the server's own limits.
|
|
14
|
+
mail, streams, suppressions, the not-tracked list, tokens, webhooks and the server's own limits.
|
|
15
15
|
`SendoraAccount` takes an account key (`ak_…`) and manages the account:
|
|
16
16
|
its servers, their keys, its sending and tracking domains. `AsyncSendora` and
|
|
17
17
|
`AsyncSendoraAccount` are the same clients for asyncio. Both keys are
|
|
@@ -224,6 +224,29 @@ request. A refused send carries `stream_id` beside `suppressed`.
|
|
|
224
224
|
sendora.suppressions.delete(address="anna@example.com")
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
## The not-tracked list
|
|
228
|
+
|
|
229
|
+
Addresses the server never tracks, on any of its streams. Mail to one still
|
|
230
|
+
goes out, untracked, and its opens and clicks of mail already sent are not
|
|
231
|
+
recorded. Tracking is not open to accounts yet, so the list changes nothing
|
|
232
|
+
until it is.
|
|
233
|
+
|
|
234
|
+
| Call | Answers |
|
|
235
|
+
| ------------------------------------------------ | -------------------------------------------------------- |
|
|
236
|
+
| `sendora.tracking_opt_outs.list(limit=, after=)` | `TrackingOptOutPage` with `tracking_opt_outs` and `next` |
|
|
237
|
+
| `sendora.tracking_opt_outs.list_all()` | Every entry, as an iterator |
|
|
238
|
+
| `sendora.tracking_opt_outs.add(address=)` | `TrackingOptOut`, as it stands if the address was listed |
|
|
239
|
+
| `sendora.tracking_opt_outs.delete(address=)` | `None`; `not_found` if the address is not listed |
|
|
240
|
+
|
|
241
|
+
An entry's `source` is `manual`, or `recipient` if the recipient asked for it.
|
|
242
|
+
Remove a recipient's own entry only after new consent. An entry's `address` is
|
|
243
|
+
`None` once an erasure has left only a keyed hash of it. `delete` still finds
|
|
244
|
+
that entry by its address, and nothing removes it without the address.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
sendora.tracking_opt_outs.add(address="anna@example.com")
|
|
248
|
+
```
|
|
249
|
+
|
|
227
250
|
## Tokens
|
|
228
251
|
|
|
229
252
|
The server's own keys, from its own key. Every key of a server has the
|
|
@@ -510,7 +533,7 @@ except SendoraError as error:
|
|
|
510
533
|
| `account_not_active` | 403 | The account is not approved to send, or not active for an inbound stream. |
|
|
511
534
|
| `spam_complaint_locked` | 403 | Only support can lift a spam-complaint suppression. |
|
|
512
535
|
| `unsubscribe_locked` | 403 | Only support can lift a recipient's own unsubscribe. |
|
|
513
|
-
| `not_found` | 404 | No such id on this server, or no such suppressed address.
|
|
536
|
+
| `not_found` | 404 | No such id on this server, or no such suppressed or not-tracked address. |
|
|
514
537
|
| `domain_exists` | 409 | The account already has the domain; `existing_id` names it. |
|
|
515
538
|
| `domain_reserved` | 400 | The name is Sendora's own; no account can receive on it. |
|
|
516
539
|
| `tracking_domain_exists` | 409 | The account already has the host, or one on that sending domain; `existing_id` names it. |
|
|
@@ -14,6 +14,7 @@ from .resources.server import AsyncThisServer, ThisServer
|
|
|
14
14
|
from .resources.streams import AsyncStreams, Streams
|
|
15
15
|
from .resources.suppressions import AsyncSuppressions, Suppressions
|
|
16
16
|
from .resources.tokens import AsyncTokens, Tokens
|
|
17
|
+
from .resources.tracking_opt_outs import AsyncTrackingOptOuts, TrackingOptOuts
|
|
17
18
|
from .resources.webhooks import AsyncWebhooks, Webhooks
|
|
18
19
|
|
|
19
20
|
_SERVER_KEY = "SENDORA_API_TOKEN"
|
|
@@ -31,6 +32,7 @@ class Sendora:
|
|
|
31
32
|
broadcasts: Sends one message to many on a broadcast stream.
|
|
32
33
|
streams: The server's streams.
|
|
33
34
|
suppressions: The addresses each stream no longer sends to.
|
|
35
|
+
tracking_opt_outs: The addresses the server never tracks.
|
|
34
36
|
tokens: The server's keys.
|
|
35
37
|
server: The key's own server, its mode and its limits.
|
|
36
38
|
webhooks: Where the server's events are posted, and their deliveries.
|
|
@@ -100,6 +102,7 @@ class Sendora:
|
|
|
100
102
|
self.broadcasts: Broadcasts = Broadcasts(transport)
|
|
101
103
|
self.streams: Streams = Streams(transport)
|
|
102
104
|
self.suppressions: Suppressions = Suppressions(transport)
|
|
105
|
+
self.tracking_opt_outs: TrackingOptOuts = TrackingOptOuts(transport)
|
|
103
106
|
self.tokens: Tokens = Tokens(transport)
|
|
104
107
|
self.server: ThisServer = ThisServer(transport)
|
|
105
108
|
self.webhooks: Webhooks = Webhooks(transport)
|
|
@@ -157,6 +160,7 @@ class AsyncSendora:
|
|
|
157
160
|
broadcasts: Sends one message to many on a broadcast stream.
|
|
158
161
|
streams: The server's streams.
|
|
159
162
|
suppressions: The addresses each stream no longer sends to.
|
|
163
|
+
tracking_opt_outs: The addresses the server never tracks.
|
|
160
164
|
tokens: The server's keys.
|
|
161
165
|
server: The key's own server, its mode and its limits.
|
|
162
166
|
webhooks: Where the server's events are posted, and their deliveries.
|
|
@@ -226,6 +230,7 @@ class AsyncSendora:
|
|
|
226
230
|
self.broadcasts: AsyncBroadcasts = AsyncBroadcasts(transport)
|
|
227
231
|
self.streams: AsyncStreams = AsyncStreams(transport)
|
|
228
232
|
self.suppressions: AsyncSuppressions = AsyncSuppressions(transport)
|
|
233
|
+
self.tracking_opt_outs: AsyncTrackingOptOuts = AsyncTrackingOptOuts(transport)
|
|
229
234
|
self.tokens: AsyncTokens = AsyncTokens(transport)
|
|
230
235
|
self.server: AsyncThisServer = AsyncThisServer(transport)
|
|
231
236
|
self.webhooks: AsyncWebhooks = AsyncWebhooks(transport)
|
|
@@ -13,6 +13,7 @@ from .streams import AsyncStreams, Streams
|
|
|
13
13
|
from .suppressions import AsyncSuppressions, Suppressions
|
|
14
14
|
from .tokens import AsyncTokens, Tokens
|
|
15
15
|
from .tracking_domains import AsyncTrackingDomains, TrackingDomains
|
|
16
|
+
from .tracking_opt_outs import AsyncTrackingOptOuts, TrackingOptOuts
|
|
16
17
|
from .webhooks import AsyncWebhooks, Webhooks
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
@@ -30,6 +31,7 @@ __all__ = [
|
|
|
30
31
|
"AsyncThisServer",
|
|
31
32
|
"AsyncTokens",
|
|
32
33
|
"AsyncTrackingDomains",
|
|
34
|
+
"AsyncTrackingOptOuts",
|
|
33
35
|
"AsyncWebhooks",
|
|
34
36
|
"Broadcasts",
|
|
35
37
|
"Domains",
|
|
@@ -45,5 +47,6 @@ __all__ = [
|
|
|
45
47
|
"ThisServer",
|
|
46
48
|
"Tokens",
|
|
47
49
|
"TrackingDomains",
|
|
50
|
+
"TrackingOptOuts",
|
|
48
51
|
"Webhooks",
|
|
49
52
|
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""The addresses the key's server never tracks, one list per server."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Iterator
|
|
4
|
+
|
|
5
|
+
from sendora._pages import iterate, iterate_async
|
|
6
|
+
from sendora._transport import AsyncTransport, Call, SyncTransport
|
|
7
|
+
from sendora._wire import decode
|
|
8
|
+
from sendora.types import TrackingOptOut, TrackingOptOutPage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrackingOptOuts:
|
|
12
|
+
"""Reads the server's not-tracked list, adds an address and removes one."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, transport: SyncTransport) -> None:
|
|
15
|
+
"""Takes the client's transport."""
|
|
16
|
+
self._transport = transport
|
|
17
|
+
|
|
18
|
+
def list(
|
|
19
|
+
self, *, limit: int | None = None, after: str | None = None
|
|
20
|
+
) -> TrackingOptOutPage:
|
|
21
|
+
"""One page of the server's not-tracked list, newest first.
|
|
22
|
+
|
|
23
|
+
An entry whose ``address`` is ``None`` was erased, and only a keyed
|
|
24
|
+
hash of it remains.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
limit: The page size, 1 to 1000; 100 when left out.
|
|
28
|
+
after: The ``next`` of the previous page.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The page.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> page = sendora.tracking_opt_outs.list(limit=200)
|
|
35
|
+
"""
|
|
36
|
+
return decode(
|
|
37
|
+
TrackingOptOutPage, self._transport.send(_list_call(limit, after))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def list_all(
|
|
41
|
+
self, *, limit: int | None = None, after: str | None = None
|
|
42
|
+
) -> Iterator[TrackingOptOut]:
|
|
43
|
+
"""Every entry of the list, page by page, from ``after`` on.
|
|
44
|
+
|
|
45
|
+
Each page is a request of its own, repeated on its own when it
|
|
46
|
+
fails.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
limit: The page size, 1 to 1000; 100 when left out.
|
|
50
|
+
after: The cursor to begin after.
|
|
51
|
+
|
|
52
|
+
Yields:
|
|
53
|
+
Each entry.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> for entry in sendora.tracking_opt_outs.list_all():
|
|
57
|
+
... print(entry.address, entry.source)
|
|
58
|
+
"""
|
|
59
|
+
yield from iterate(
|
|
60
|
+
lambda cursor: self.list(limit=limit, after=cursor),
|
|
61
|
+
_entries_of,
|
|
62
|
+
_next_of,
|
|
63
|
+
after,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def add(self, *, address: str) -> TrackingOptOut:
|
|
67
|
+
"""Puts an address on the server's list, so that it is never tracked.
|
|
68
|
+
|
|
69
|
+
That holds for mail already sent too. An address already on the list
|
|
70
|
+
changes nothing, and the entry comes back as it stands.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
address: The address never to track.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The entry.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> entry = sendora.tracking_opt_outs.add(address="anna@example.com")
|
|
80
|
+
"""
|
|
81
|
+
return decode(TrackingOptOut, self._transport.send(_add_call(address)))
|
|
82
|
+
|
|
83
|
+
def delete(self, *, address: str) -> None:
|
|
84
|
+
"""Takes an address off the server's list, so it can be tracked again.
|
|
85
|
+
|
|
86
|
+
Remove an entry whose ``source`` is ``recipient`` only after new
|
|
87
|
+
consent. An erased entry is found by its address too. A delete
|
|
88
|
+
repeated after a lost answer whose first attempt took effect raises
|
|
89
|
+
``not_found``.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
address: The address to track again.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
NotFoundError: The address is not on the list.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> sendora.tracking_opt_outs.delete(address="anna@example.com")
|
|
99
|
+
"""
|
|
100
|
+
self._transport.send(_delete_call(address))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncTrackingOptOuts:
|
|
104
|
+
"""Reads the server's not-tracked list, adds an address and removes one."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
107
|
+
"""Takes the client's transport."""
|
|
108
|
+
self._transport = transport
|
|
109
|
+
|
|
110
|
+
async def list(
|
|
111
|
+
self, *, limit: int | None = None, after: str | None = None
|
|
112
|
+
) -> TrackingOptOutPage:
|
|
113
|
+
"""One page of the server's not-tracked list, newest first.
|
|
114
|
+
|
|
115
|
+
An entry whose ``address`` is ``None`` was erased, and only a keyed
|
|
116
|
+
hash of it remains.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
limit: The page size, 1 to 1000; 100 when left out.
|
|
120
|
+
after: The ``next`` of the previous page.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The page.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> page = await sendora.tracking_opt_outs.list(limit=200)
|
|
127
|
+
"""
|
|
128
|
+
return decode(
|
|
129
|
+
TrackingOptOutPage, await self._transport.send(_list_call(limit, after))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def list_all(
|
|
133
|
+
self, *, limit: int | None = None, after: str | None = None
|
|
134
|
+
) -> AsyncIterator[TrackingOptOut]:
|
|
135
|
+
"""Every entry of the list, page by page, from ``after`` on.
|
|
136
|
+
|
|
137
|
+
Each page is a request of its own, repeated on its own when it
|
|
138
|
+
fails.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
limit: The page size, 1 to 1000; 100 when left out.
|
|
142
|
+
after: The cursor to begin after.
|
|
143
|
+
|
|
144
|
+
Yields:
|
|
145
|
+
Each entry.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
>>> async for entry in sendora.tracking_opt_outs.list_all():
|
|
149
|
+
... print(entry.address, entry.source)
|
|
150
|
+
"""
|
|
151
|
+
async for entry in iterate_async(
|
|
152
|
+
lambda cursor: self.list(limit=limit, after=cursor),
|
|
153
|
+
_entries_of,
|
|
154
|
+
_next_of,
|
|
155
|
+
after,
|
|
156
|
+
):
|
|
157
|
+
yield entry
|
|
158
|
+
|
|
159
|
+
async def add(self, *, address: str) -> TrackingOptOut:
|
|
160
|
+
"""Puts an address on the server's list, so that it is never tracked.
|
|
161
|
+
|
|
162
|
+
That holds for mail already sent too. An address already on the list
|
|
163
|
+
changes nothing, and the entry comes back as it stands.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
address: The address never to track.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The entry.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> entry = await sendora.tracking_opt_outs.add(address="anna@example.com")
|
|
173
|
+
"""
|
|
174
|
+
return decode(TrackingOptOut, await self._transport.send(_add_call(address)))
|
|
175
|
+
|
|
176
|
+
async def delete(self, *, address: str) -> None:
|
|
177
|
+
"""Takes an address off the server's list, so it can be tracked again.
|
|
178
|
+
|
|
179
|
+
Remove an entry whose ``source`` is ``recipient`` only after new
|
|
180
|
+
consent. An erased entry is found by its address too. A delete
|
|
181
|
+
repeated after a lost answer whose first attempt took effect raises
|
|
182
|
+
``not_found``.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
address: The address to track again.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
NotFoundError: The address is not on the list.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> await sendora.tracking_opt_outs.delete(address="anna@example.com")
|
|
192
|
+
"""
|
|
193
|
+
await self._transport.send(_delete_call(address))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _list_call(limit: int | None, after: str | None) -> Call:
|
|
197
|
+
return Call(
|
|
198
|
+
"GET",
|
|
199
|
+
"/v1/tracking-opt-outs",
|
|
200
|
+
idempotent=True,
|
|
201
|
+
query={"limit": limit, "after": after},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _add_call(address: str) -> Call:
|
|
206
|
+
return Call(
|
|
207
|
+
"POST", "/v1/tracking-opt-outs", idempotent=True, body={"address": address}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _delete_call(address: str) -> Call:
|
|
212
|
+
return Call(
|
|
213
|
+
"POST",
|
|
214
|
+
"/v1/tracking-opt-outs/delete",
|
|
215
|
+
idempotent=True,
|
|
216
|
+
body={"address": address},
|
|
217
|
+
expect="none",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _entries_of(page: TrackingOptOutPage) -> list[TrackingOptOut]:
|
|
222
|
+
return page.tracking_opt_outs
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _next_of(page: TrackingOptOutPage) -> str | None:
|
|
226
|
+
return page.next
|
|
@@ -14,7 +14,8 @@ and every event is in it.
|
|
|
14
14
|
|
|
15
15
|
- Two kinds of key, two clients. A server key (`sk_…`) drives
|
|
16
16
|
`Sendora()`: `email`, `broadcasts`, `messages`, `inbound`,
|
|
17
|
-
`inbound_domains`, `server`, `streams`, `suppressions`,
|
|
17
|
+
`inbound_domains`, `server`, `streams`, `suppressions`,
|
|
18
|
+
`tracking_opt_outs`, `tokens`,
|
|
18
19
|
`webhooks`, everything inside one server. An account key (`ak_…`)
|
|
19
20
|
drives `SendoraAccount()`: `servers`, `servers.tokens`, `domains` and
|
|
20
21
|
`tracking_domains`, and it never sends. A key of the wrong kind raises `wrong_token_kind`.
|
|
@@ -63,6 +64,9 @@ and every event is in it.
|
|
|
63
64
|
comparing `event.event`, and handle `UnknownEvent`, an event newer than
|
|
64
65
|
the release. Answer 2xx quickly and do the work afterwards. Deliveries
|
|
65
66
|
arrive at least once, so key handling on `event.id`.
|
|
67
|
+
- `tracking_opt_outs` is the server's list of addresses that are never
|
|
68
|
+
tracked. Remove an entry whose `source` is `recipient` only after new
|
|
69
|
+
consent. An erased entry has `address=None`, and only its address removes it.
|
|
66
70
|
- Addresses on the suppression list are refused with
|
|
67
71
|
`recipient_suppressed`; lift a hard bounce with
|
|
68
72
|
`suppressions.delete(address=...)`; a spam complaint or an unsubscribe
|
|
@@ -143,6 +143,9 @@ __all__ = [
|
|
|
143
143
|
"TrackingDomain",
|
|
144
144
|
"TrackingDomainList",
|
|
145
145
|
"TrackingDomainState",
|
|
146
|
+
"TrackingOptOut",
|
|
147
|
+
"TrackingOptOutPage",
|
|
148
|
+
"TrackingOptOutSource",
|
|
146
149
|
"TrackingReason",
|
|
147
150
|
"TrackingSkipped",
|
|
148
151
|
"TxtRecord",
|
|
@@ -1045,6 +1048,44 @@ class SuppressionPage(Answer):
|
|
|
1045
1048
|
next: str | None
|
|
1046
1049
|
|
|
1047
1050
|
|
|
1051
|
+
TrackingOptOutSource = Literal["manual", "recipient"]
|
|
1052
|
+
"""How an address came onto a server's not-tracked list."""
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@model
|
|
1056
|
+
class TrackingOptOut(Answer):
|
|
1057
|
+
"""An address that the server never tracks.
|
|
1058
|
+
|
|
1059
|
+
Attributes:
|
|
1060
|
+
address: The address, lower-cased. ``None`` once an erasure has left
|
|
1061
|
+
only a keyed hash.
|
|
1062
|
+
source: ``manual`` if added through the API or the dashboard,
|
|
1063
|
+
``recipient`` if the recipient asked for it. Remove a recipient's
|
|
1064
|
+
own entry only after new consent.
|
|
1065
|
+
message_id: The message that a recipient asked from. ``None`` on a
|
|
1066
|
+
manual entry, and once that message or the address has gone.
|
|
1067
|
+
created_at: When it was added.
|
|
1068
|
+
"""
|
|
1069
|
+
|
|
1070
|
+
address: str | None
|
|
1071
|
+
source: TrackingOptOutSource
|
|
1072
|
+
message_id: str | None
|
|
1073
|
+
created_at: datetime
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
@model
|
|
1077
|
+
class TrackingOptOutPage(Answer):
|
|
1078
|
+
"""One page of a server's not-tracked list, newest first.
|
|
1079
|
+
|
|
1080
|
+
Attributes:
|
|
1081
|
+
tracking_opt_outs: The page's entries.
|
|
1082
|
+
next: Pass as ``after`` for the next page; ``None`` on the last.
|
|
1083
|
+
"""
|
|
1084
|
+
|
|
1085
|
+
tracking_opt_outs: list[TrackingOptOut]
|
|
1086
|
+
next: str | None
|
|
1087
|
+
|
|
1088
|
+
|
|
1048
1089
|
@model
|
|
1049
1090
|
class Token(Answer):
|
|
1050
1091
|
"""A key of a server; its value is shown only when it is created.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|