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.
Files changed (40) hide show
  1. {sendora-2.3.0 → sendora-2.4.0}/CHANGELOG.md +6 -0
  2. {sendora-2.3.0 → sendora-2.4.0}/PKG-INFO +26 -3
  3. {sendora-2.3.0 → sendora-2.4.0}/README.md +25 -2
  4. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_client.py +5 -0
  5. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_version.py +1 -1
  6. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/__init__.py +3 -0
  7. sendora-2.4.0/src/sendora/resources/tracking_opt_outs.py +226 -0
  8. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/skills/sendora/SKILL.md +5 -1
  9. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/types.py +41 -0
  10. {sendora-2.3.0 → sendora-2.4.0}/.gitignore +0 -0
  11. {sendora-2.3.0 → sendora-2.4.0}/LICENSE +0 -0
  12. {sendora-2.3.0 → sendora-2.4.0}/pyproject.toml +0 -0
  13. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/__init__.py +0 -0
  14. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_account.py +0 -0
  15. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_answers.py +0 -0
  16. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_errors.py +0 -0
  17. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_json.py +0 -0
  18. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_keys.py +0 -0
  19. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_model.py +0 -0
  20. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_options.py +0 -0
  21. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_pages.py +0 -0
  22. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_retry.py +0 -0
  23. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_transport.py +0 -0
  24. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_webhooks.py +0 -0
  25. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/_wire.py +0 -0
  26. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/py.typed +0 -0
  27. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/broadcasts.py +0 -0
  28. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/domains.py +0 -0
  29. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/email.py +0 -0
  30. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/erasures.py +0 -0
  31. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/inbound.py +0 -0
  32. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/inbound_domains.py +0 -0
  33. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/messages.py +0 -0
  34. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/server.py +0 -0
  35. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/servers.py +0 -0
  36. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/streams.py +0 -0
  37. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/suppressions.py +0 -0
  38. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/tokens.py +0 -0
  39. {sendora-2.3.0 → sendora-2.4.0}/src/sendora/resources/tracking_domains.py +0 -0
  40. {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.0
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)
@@ -1,3 +1,3 @@
1
1
  """The package's version: the build reads it, and every request names it."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.4.0"
@@ -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`, `tokens`,
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