mailsocket 0.3.0__tar.gz → 0.3.1__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.
- {mailsocket-0.3.0 → mailsocket-0.3.1}/PKG-INFO +7 -1
- {mailsocket-0.3.0 → mailsocket-0.3.1}/README.md +6 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket/__init__.py +1 -1
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket/client.py +21 -3
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket.egg-info/PKG-INFO +7 -1
- {mailsocket-0.3.0 → mailsocket-0.3.1}/pyproject.toml +1 -1
- {mailsocket-0.3.0 → mailsocket-0.3.1}/tests/test_client.py +58 -1
- {mailsocket-0.3.0 → mailsocket-0.3.1}/LICENSE +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket/errors.py +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket/py.typed +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket.egg-info/SOURCES.txt +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket.egg-info/dependency_links.txt +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket.egg-info/requires.txt +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/mailsocket.egg-info/top_level.txt +0 -0
- {mailsocket-0.3.0 → mailsocket-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsocket
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Official Python client for the mailsocket REST API — one line to wait for an OTP.
|
|
5
5
|
Author: mailsocket
|
|
6
6
|
License-Expression: MIT
|
|
@@ -162,6 +162,12 @@ invalid id never reaches the network.
|
|
|
162
162
|
|
|
163
163
|
## Changelog
|
|
164
164
|
|
|
165
|
+
- **0.3.1** — the `wait*` helpers no longer miss a message that lands between
|
|
166
|
+
two long-poll calls. When you pass no `since`, the first call still omits it
|
|
167
|
+
(the server uses the request start), and every following call sends the
|
|
168
|
+
cutoff the server reported in the `Mailsocket-Wait-Since` response header of
|
|
169
|
+
the previous call. With an older server that sends no such header, behaviour
|
|
170
|
+
is unchanged. An explicit `since` is used as before on every call.
|
|
165
171
|
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
166
172
|
puts one sample OTP email into your own inbox without going through the
|
|
167
173
|
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
@@ -135,6 +135,12 @@ invalid id never reaches the network.
|
|
|
135
135
|
|
|
136
136
|
## Changelog
|
|
137
137
|
|
|
138
|
+
- **0.3.1** — the `wait*` helpers no longer miss a message that lands between
|
|
139
|
+
two long-poll calls. When you pass no `since`, the first call still omits it
|
|
140
|
+
(the server uses the request start), and every following call sends the
|
|
141
|
+
cutoff the server reported in the `Mailsocket-Wait-Since` response header of
|
|
142
|
+
the previous call. With an older server that sends no such header, behaviour
|
|
143
|
+
is unchanged. An explicit `since` is used as before on every call.
|
|
138
144
|
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
139
145
|
puts one sample OTP email into your own inbox without going through the
|
|
140
146
|
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
@@ -18,6 +18,8 @@ DEFAULT_BASE_URL = "https://dash.mailsocket.app/api/v1"
|
|
|
18
18
|
WAIT_TIMEOUT_MAX = 25 # seconds — the server clamps the per-call wait to [1, 25]
|
|
19
19
|
WAIT_SOCKET_BUFFER = 10 # seconds of client-side slack above the server block
|
|
20
20
|
DEFAULT_RETRY_AFTER = 1.0 # seconds, when a 429 carries no Retry-After header
|
|
21
|
+
# Response header with the cutoff a wait call actually used (server >= 2026-10).
|
|
22
|
+
WAIT_SINCE_HEADER = "mailsocket-wait-since"
|
|
21
23
|
|
|
22
24
|
__all__ = ["Client", "Page", "WaitResult"]
|
|
23
25
|
|
|
@@ -289,6 +291,12 @@ class Client:
|
|
|
289
291
|
since,
|
|
290
292
|
) -> WaitResult:
|
|
291
293
|
deadline = _monotonic() + float(timeout)
|
|
294
|
+
# No caller ``since``: the first call omits it (server default = the
|
|
295
|
+
# request start), and every later call sends back the cutoff the
|
|
296
|
+
# previous response reported, so a message that lands between two
|
|
297
|
+
# calls is still matched. An older server sends no header; then each
|
|
298
|
+
# call keeps omitting ``since`` exactly as before.
|
|
299
|
+
cursor = since
|
|
292
300
|
while True:
|
|
293
301
|
remaining = deadline - _monotonic()
|
|
294
302
|
if remaining <= 0:
|
|
@@ -297,8 +305,8 @@ class Client:
|
|
|
297
305
|
)
|
|
298
306
|
server_timeout = max(1.0, min(float(WAIT_TIMEOUT_MAX), remaining))
|
|
299
307
|
params = {"timeout": server_timeout, "require": require}
|
|
300
|
-
if
|
|
301
|
-
params["since"] =
|
|
308
|
+
if cursor is not None:
|
|
309
|
+
params["since"] = cursor
|
|
302
310
|
if min_confidence is not None:
|
|
303
311
|
params["min_confidence"] = min_confidence
|
|
304
312
|
|
|
@@ -312,6 +320,7 @@ class Client:
|
|
|
312
320
|
if status == 200:
|
|
313
321
|
return WaitResult(payload["data"])
|
|
314
322
|
if status == 204:
|
|
323
|
+
cursor = self._next_cursor(since, cursor, headers)
|
|
315
324
|
continue
|
|
316
325
|
if status == 429:
|
|
317
326
|
if not self._retry_wait_on_429:
|
|
@@ -326,6 +335,15 @@ class Client:
|
|
|
326
335
|
continue
|
|
327
336
|
self._raise_for_status(status, headers, payload)
|
|
328
337
|
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _next_cursor(since, cursor, headers: dict):
|
|
340
|
+
"""The ``since`` for the next wait call: the caller's own value when
|
|
341
|
+
given, else the server-reported cutoff of this call, else unchanged
|
|
342
|
+
(an older server sends no header)."""
|
|
343
|
+
if since is not None:
|
|
344
|
+
return cursor
|
|
345
|
+
return headers.get(WAIT_SINCE_HEADER) or cursor
|
|
346
|
+
|
|
329
347
|
def _request(self, method: str, path: str, *, params: dict | None = None, json_body=None):
|
|
330
348
|
status, headers, body = self._request_http(method, path, params=params, json_body=json_body)
|
|
331
349
|
payload = self._parse_json(body)
|
|
@@ -339,7 +357,7 @@ class Client:
|
|
|
339
357
|
url += "?" + urllib.parse.urlencode(params, doseq=True)
|
|
340
358
|
headers = {
|
|
341
359
|
"Accept": "application/json",
|
|
342
|
-
"User-Agent": "mailsocket-python/0.3.
|
|
360
|
+
"User-Agent": "mailsocket-python/0.3.1",
|
|
343
361
|
}
|
|
344
362
|
headers.update(self._extra_headers)
|
|
345
363
|
# Set last so nothing in extra_headers can ever replace it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsocket
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Official Python client for the mailsocket REST API — one line to wait for an OTP.
|
|
5
5
|
Author: mailsocket
|
|
6
6
|
License-Expression: MIT
|
|
@@ -162,6 +162,12 @@ invalid id never reaches the network.
|
|
|
162
162
|
|
|
163
163
|
## Changelog
|
|
164
164
|
|
|
165
|
+
- **0.3.1** — the `wait*` helpers no longer miss a message that lands between
|
|
166
|
+
two long-poll calls. When you pass no `since`, the first call still omits it
|
|
167
|
+
(the server uses the request start), and every following call sends the
|
|
168
|
+
cutoff the server reported in the `Mailsocket-Wait-Since` response header of
|
|
169
|
+
the previous call. With an older server that sends no such header, behaviour
|
|
170
|
+
is unchanged. An explicit `since` is used as before on every call.
|
|
165
171
|
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
166
172
|
puts one sample OTP email into your own inbox without going through the
|
|
167
173
|
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mailsocket"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Official Python client for the mailsocket REST API — one line to wait for an OTP."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -193,6 +193,63 @@ def test_wait_keeps_polling_through_204s_then_returns(monkeypatch):
|
|
|
193
193
|
assert "require=otp" in url
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def _since_of(url):
|
|
197
|
+
query = urllib.parse.parse_qs(urllib.parse.urlsplit(url).query)
|
|
198
|
+
return query.get("since", [None])[0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
CURSOR_1 = "2026-10-12T03:04:05.123456Z"
|
|
202
|
+
CURSOR_2 = "2026-10-12T03:04:30.654321Z"
|
|
203
|
+
EPOCH = "1970-01-01T00:00:00.000000Z"
|
|
204
|
+
H = "Mailsocket-Wait-Since"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.parametrize(
|
|
208
|
+
"method, kwargs, headers, expected",
|
|
209
|
+
[
|
|
210
|
+
# 0.3.1 gap fix: no caller since -> call 1 omits it, call N sends the
|
|
211
|
+
# header of call N-1, so a message landing between calls is matched.
|
|
212
|
+
("wait_for_otp", {}, [{H: CURSOR_1}, {H: CURSOR_1}, {}], [None, CURSOR_1, CURSOR_1]),
|
|
213
|
+
("wait_for_link", {}, [{H: CURSOR_1}, {}], [None, CURSOR_1]),
|
|
214
|
+
("wait", {}, [{H: CURSOR_1}, {}], [None, CURSOR_1]),
|
|
215
|
+
("wait_for_otp", {}, [{H: CURSOR_1}, {H: CURSOR_2}, {}], [None, CURSOR_1, CURSOR_2]),
|
|
216
|
+
("wait_for_otp", {}, [{H.upper(): CURSOR_1}, {}], [None, CURSOR_1]),
|
|
217
|
+
# Old server (no header) or an empty header: unchanged 0.3.0 behaviour.
|
|
218
|
+
("wait_for_otp", {}, [{}, {}, {}], [None, None, None]),
|
|
219
|
+
("wait_for_otp", {}, [{H: ""}, {}], [None, None]),
|
|
220
|
+
# An explicit since is never replaced by the header.
|
|
221
|
+
("wait_for_otp", {"since": "msg_anchor"}, [{H: CURSOR_1}, {H: CURSOR_1}, {}], ["msg_anchor"] * 3),
|
|
222
|
+
("wait_for_otp", {"since": 0}, [{H: EPOCH}, {}], ["0", "0"]),
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
def test_wait_loop_cursor(monkeypatch, method, kwargs, headers, expected):
|
|
226
|
+
final = LINK_MESSAGE if method == "wait_for_link" else OTP_MESSAGE
|
|
227
|
+
responses = [(204, h, None) for h in headers[:-1]] + [(200, headers[-1], {"data": final})]
|
|
228
|
+
client, transport = make_client(responses)
|
|
229
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
230
|
+
|
|
231
|
+
getattr(client, method)("inbox_abc", timeout=60, **kwargs)
|
|
232
|
+
|
|
233
|
+
assert [_since_of(url) for _, url in transport.calls] == expected
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_wait_loop_keeps_cursor_across_a_429_retry_and_not_between_waits(monkeypatch):
|
|
237
|
+
responses = [
|
|
238
|
+
(204, {H: CURSOR_1}, None),
|
|
239
|
+
(429, {"Retry-After": "0"}, {"error": {"code": "wait_capacity", "message": "busy"}}),
|
|
240
|
+
(200, {}, {"data": OTP_MESSAGE}),
|
|
241
|
+
(200, {}, {"data": OTP_MESSAGE}),
|
|
242
|
+
]
|
|
243
|
+
client, transport = make_client(responses)
|
|
244
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
245
|
+
monkeypatch.setattr(mailsocket.client, "_sleep", lambda s: None)
|
|
246
|
+
|
|
247
|
+
client.wait_for_otp("inbox_abc", timeout=60)
|
|
248
|
+
client.wait_for_otp("inbox_abc", timeout=60) # a new wait starts fresh
|
|
249
|
+
|
|
250
|
+
assert [_since_of(url) for _, url in transport.calls] == [None, CURSOR_1, CURSOR_1, None]
|
|
251
|
+
|
|
252
|
+
|
|
196
253
|
def test_wait_respects_overall_timeout_raises_wait_timeout(monkeypatch):
|
|
197
254
|
# A 204 means "the server blocked for `timeout` seconds and found nothing".
|
|
198
255
|
# Model that with a controllable clock: each 204 advances time by the
|
|
@@ -582,7 +639,7 @@ def test_metadata_consistency_across_files():
|
|
|
582
639
|
from pathlib import Path
|
|
583
640
|
|
|
584
641
|
root = Path(__file__).resolve().parents[1]
|
|
585
|
-
expected = "0.3.
|
|
642
|
+
expected = "0.3.1"
|
|
586
643
|
|
|
587
644
|
pyproject = tomllib.loads((root / "pyproject.toml").read_text())
|
|
588
645
|
assert pyproject["project"]["version"] == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|