mailsocket 0.2.0__tar.gz → 0.3.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.
- {mailsocket-0.2.0 → mailsocket-0.3.0}/PKG-INFO +38 -3
- {mailsocket-0.2.0 → mailsocket-0.3.0}/README.md +37 -2
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket/__init__.py +1 -1
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket/client.py +20 -1
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket.egg-info/PKG-INFO +38 -3
- {mailsocket-0.2.0 → mailsocket-0.3.0}/pyproject.toml +1 -1
- {mailsocket-0.2.0 → mailsocket-0.3.0}/tests/test_client.py +137 -1
- {mailsocket-0.2.0 → mailsocket-0.3.0}/LICENSE +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket/errors.py +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket/py.typed +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket.egg-info/SOURCES.txt +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket.egg-info/dependency_links.txt +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket.egg-info/requires.txt +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/mailsocket.egg-info/top_level.txt +0 -0
- {mailsocket-0.2.0 → mailsocket-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsocket
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -31,17 +31,34 @@ The official Python client for the [mailsocket](https://mailsocket.app) v1 REST
|
|
|
31
31
|
Ephemeral email inboxes, message retrieval, and — the whole point — **wait for the OTP in one line**.
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
|
+
import time
|
|
35
|
+
|
|
34
36
|
from mailsocket import Client
|
|
35
37
|
|
|
36
38
|
client = Client("ms_live_...") # your API key from the dashboard
|
|
37
39
|
inbox = client.create_inbox(label="signup") # -> {"id": "inbox_...", "address": "..."}
|
|
38
40
|
|
|
41
|
+
started = time.time() # take the cutoff BEFORE the trigger
|
|
39
42
|
# hand inbox["address"] to whatever form sends the code, then:
|
|
40
|
-
result = client.wait_for_otp(inbox["id"])
|
|
43
|
+
result = client.wait_for_otp(inbox["id"], since=started) # blocks up to 60s
|
|
41
44
|
print(result.otp) # "123456"
|
|
42
45
|
print(result.confidence) # 0.95
|
|
43
46
|
```
|
|
44
47
|
|
|
48
|
+
Take `started` just before you trigger the email and pass it as `since`: a
|
|
49
|
+
code that lands before `wait_for_otp` starts is still caught, and an older
|
|
50
|
+
code in a reused inbox is not.
|
|
51
|
+
|
|
52
|
+
No email handy? `client.send_test_code(inbox_id)` drops a sample code into
|
|
53
|
+
the inbox so you can see the whole loop work:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
started = time.time()
|
|
57
|
+
client.send_test_code(inbox["id"]) # -> {"message_id": "msg_...", "sent_at": "..."}
|
|
58
|
+
result = client.wait_for_otp(inbox["id"], since=started)
|
|
59
|
+
print(result.otp) # the sample code
|
|
60
|
+
```
|
|
61
|
+
|
|
45
62
|
No polling loop. No regex over the email body. `wait_for_otp` long-polls the
|
|
46
63
|
server (which already knows how to extract the code) and hands back a
|
|
47
64
|
deterministic result with a confidence score.
|
|
@@ -69,6 +86,7 @@ client = Client(api_key, base_url="https://dash.mailsocket.app/api/v1")
|
|
|
69
86
|
| `list_messages(inbox_id, has_otp=None, subject_contains=None, sender=None, ...)` | `Page` — `sender` maps to the API's `from` filter |
|
|
70
87
|
| `get_latest(inbox_id)` | `dict` — newest message |
|
|
71
88
|
| `get_message(message_id)` | `dict` — full message |
|
|
89
|
+
| `send_test_code(inbox_id)` | `dict` — `message_id`, `sent_at` of a sample OTP email put straight into the inbox (no mail server; 5 per inbox / 20 per account per hour) |
|
|
72
90
|
|
|
73
91
|
### The moat
|
|
74
92
|
|
|
@@ -78,6 +96,14 @@ result = client.wait_for_link("inbox_abc123", timeout=60, since=None)
|
|
|
78
96
|
result = client.wait("inbox_abc123", timeout=60, min_confidence=0.0, since=None) # otp OR link
|
|
79
97
|
```
|
|
80
98
|
|
|
99
|
+
With a cutoff taken before the trigger (the usual case):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
started = time.time()
|
|
103
|
+
# trigger the email here
|
|
104
|
+
result = client.wait_for_otp("inbox_abc123", since=started)
|
|
105
|
+
```
|
|
106
|
+
|
|
81
107
|
All three return a `WaitResult` and take keyword-only arguments after
|
|
82
108
|
`inbox_id`. `timeout`/`min_confidence`/`since` shown above are the defaults —
|
|
83
109
|
call with no extra arguments for the common case.
|
|
@@ -112,11 +138,16 @@ Semantics:
|
|
|
112
138
|
`MailsocketError` (base) with subclasses:
|
|
113
139
|
|
|
114
140
|
- `AuthError` — HTTP 401 (bad/missing key).
|
|
115
|
-
- `NotFound` — HTTP 404
|
|
141
|
+
- `NotFound` — HTTP 404 (also an unknown, deleted or foreign inbox in
|
|
142
|
+
`send_test_code`).
|
|
116
143
|
- `RateLimited` — HTTP 429, carries `.subcode` (`rate_limited`,
|
|
117
144
|
`too_many_wait_requests`, `wait_capacity`) and `.retry_after`.
|
|
118
145
|
- `WaitTimeout` — no matching message within the overall deadline.
|
|
119
146
|
|
|
147
|
+
`send_test_code` on a disabled inbox raises the base `MailsocketError` with
|
|
148
|
+
`code="inbox_disabled"` and `status=409`. Over its hourly quota it raises
|
|
149
|
+
`RateLimited`; `.retry_after` says how many seconds until it frees up.
|
|
150
|
+
|
|
120
151
|
## Develop
|
|
121
152
|
|
|
122
153
|
```bash
|
|
@@ -131,6 +162,10 @@ invalid id never reaches the network.
|
|
|
131
162
|
|
|
132
163
|
## Changelog
|
|
133
164
|
|
|
165
|
+
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
166
|
+
puts one sample OTP email into your own inbox without going through the
|
|
167
|
+
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
168
|
+
take `started = time.time()` before the trigger and pass `since=started`.
|
|
134
169
|
- **0.2.0** — BEHAVIOUR CHANGE: `wait_for_otp`/`wait_for_link`/`wait` now
|
|
135
170
|
default `since=None`, which omits the param so the server uses the
|
|
136
171
|
request start time, instead of the old `since=0` ("any message already
|
|
@@ -4,17 +4,34 @@ The official Python client for the [mailsocket](https://mailsocket.app) v1 REST
|
|
|
4
4
|
Ephemeral email inboxes, message retrieval, and — the whole point — **wait for the OTP in one line**.
|
|
5
5
|
|
|
6
6
|
```python
|
|
7
|
+
import time
|
|
8
|
+
|
|
7
9
|
from mailsocket import Client
|
|
8
10
|
|
|
9
11
|
client = Client("ms_live_...") # your API key from the dashboard
|
|
10
12
|
inbox = client.create_inbox(label="signup") # -> {"id": "inbox_...", "address": "..."}
|
|
11
13
|
|
|
14
|
+
started = time.time() # take the cutoff BEFORE the trigger
|
|
12
15
|
# hand inbox["address"] to whatever form sends the code, then:
|
|
13
|
-
result = client.wait_for_otp(inbox["id"])
|
|
16
|
+
result = client.wait_for_otp(inbox["id"], since=started) # blocks up to 60s
|
|
14
17
|
print(result.otp) # "123456"
|
|
15
18
|
print(result.confidence) # 0.95
|
|
16
19
|
```
|
|
17
20
|
|
|
21
|
+
Take `started` just before you trigger the email and pass it as `since`: a
|
|
22
|
+
code that lands before `wait_for_otp` starts is still caught, and an older
|
|
23
|
+
code in a reused inbox is not.
|
|
24
|
+
|
|
25
|
+
No email handy? `client.send_test_code(inbox_id)` drops a sample code into
|
|
26
|
+
the inbox so you can see the whole loop work:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
started = time.time()
|
|
30
|
+
client.send_test_code(inbox["id"]) # -> {"message_id": "msg_...", "sent_at": "..."}
|
|
31
|
+
result = client.wait_for_otp(inbox["id"], since=started)
|
|
32
|
+
print(result.otp) # the sample code
|
|
33
|
+
```
|
|
34
|
+
|
|
18
35
|
No polling loop. No regex over the email body. `wait_for_otp` long-polls the
|
|
19
36
|
server (which already knows how to extract the code) and hands back a
|
|
20
37
|
deterministic result with a confidence score.
|
|
@@ -42,6 +59,7 @@ client = Client(api_key, base_url="https://dash.mailsocket.app/api/v1")
|
|
|
42
59
|
| `list_messages(inbox_id, has_otp=None, subject_contains=None, sender=None, ...)` | `Page` — `sender` maps to the API's `from` filter |
|
|
43
60
|
| `get_latest(inbox_id)` | `dict` — newest message |
|
|
44
61
|
| `get_message(message_id)` | `dict` — full message |
|
|
62
|
+
| `send_test_code(inbox_id)` | `dict` — `message_id`, `sent_at` of a sample OTP email put straight into the inbox (no mail server; 5 per inbox / 20 per account per hour) |
|
|
45
63
|
|
|
46
64
|
### The moat
|
|
47
65
|
|
|
@@ -51,6 +69,14 @@ result = client.wait_for_link("inbox_abc123", timeout=60, since=None)
|
|
|
51
69
|
result = client.wait("inbox_abc123", timeout=60, min_confidence=0.0, since=None) # otp OR link
|
|
52
70
|
```
|
|
53
71
|
|
|
72
|
+
With a cutoff taken before the trigger (the usual case):
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
started = time.time()
|
|
76
|
+
# trigger the email here
|
|
77
|
+
result = client.wait_for_otp("inbox_abc123", since=started)
|
|
78
|
+
```
|
|
79
|
+
|
|
54
80
|
All three return a `WaitResult` and take keyword-only arguments after
|
|
55
81
|
`inbox_id`. `timeout`/`min_confidence`/`since` shown above are the defaults —
|
|
56
82
|
call with no extra arguments for the common case.
|
|
@@ -85,11 +111,16 @@ Semantics:
|
|
|
85
111
|
`MailsocketError` (base) with subclasses:
|
|
86
112
|
|
|
87
113
|
- `AuthError` — HTTP 401 (bad/missing key).
|
|
88
|
-
- `NotFound` — HTTP 404
|
|
114
|
+
- `NotFound` — HTTP 404 (also an unknown, deleted or foreign inbox in
|
|
115
|
+
`send_test_code`).
|
|
89
116
|
- `RateLimited` — HTTP 429, carries `.subcode` (`rate_limited`,
|
|
90
117
|
`too_many_wait_requests`, `wait_capacity`) and `.retry_after`.
|
|
91
118
|
- `WaitTimeout` — no matching message within the overall deadline.
|
|
92
119
|
|
|
120
|
+
`send_test_code` on a disabled inbox raises the base `MailsocketError` with
|
|
121
|
+
`code="inbox_disabled"` and `status=409`. Over its hourly quota it raises
|
|
122
|
+
`RateLimited`; `.retry_after` says how many seconds until it frees up.
|
|
123
|
+
|
|
93
124
|
## Develop
|
|
94
125
|
|
|
95
126
|
```bash
|
|
@@ -104,6 +135,10 @@ invalid id never reaches the network.
|
|
|
104
135
|
|
|
105
136
|
## Changelog
|
|
106
137
|
|
|
138
|
+
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
139
|
+
puts one sample OTP email into your own inbox without going through the
|
|
140
|
+
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
141
|
+
take `started = time.time()` before the trigger and pass `since=started`.
|
|
107
142
|
- **0.2.0** — BEHAVIOUR CHANGE: `wait_for_otp`/`wait_for_link`/`wait` now
|
|
108
143
|
default `since=None`, which omits the param so the server uses the
|
|
109
144
|
request start time, instead of the old `since=0` ("any message already
|
|
@@ -201,6 +201,25 @@ class Client:
|
|
|
201
201
|
payload = self._request("GET", f"/messages/{_seg(message_id)}")
|
|
202
202
|
return payload["data"]
|
|
203
203
|
|
|
204
|
+
def send_test_code(self, inbox_id: str) -> dict:
|
|
205
|
+
"""Put one sample OTP email (a random 6-digit code) into ``inbox_id``.
|
|
206
|
+
|
|
207
|
+
Calls ``POST /inboxes/{id}/test-code`` and returns its ``data`` dict:
|
|
208
|
+
``{"message_id": "msg_...", "sent_at": "..."}``. The message does NOT
|
|
209
|
+
go through the mail server: it is stored and parsed like real inbound
|
|
210
|
+
mail, so a pending or later wait (with a ``since`` from before this
|
|
211
|
+
call) returns it. Use it to prove the wait loop end to end without
|
|
212
|
+
sending real email.
|
|
213
|
+
|
|
214
|
+
Shares the dashboard "Send a test code" quota (5 per inbox, 20 per
|
|
215
|
+
account per hour). Errors: :class:`NotFound` for an unknown, deleted
|
|
216
|
+
or foreign inbox; :class:`RateLimited` (with ``retry_after``) over the
|
|
217
|
+
quota; :class:`MailsocketError` with ``code="inbox_disabled"``
|
|
218
|
+
(HTTP 409) for a disabled inbox.
|
|
219
|
+
"""
|
|
220
|
+
payload = self._request("POST", f"/inboxes/{_seg(inbox_id)}/test-code")
|
|
221
|
+
return payload["data"]
|
|
222
|
+
|
|
204
223
|
# -- the moat -------------------------------------------------------------
|
|
205
224
|
|
|
206
225
|
def wait_for_otp(
|
|
@@ -320,7 +339,7 @@ class Client:
|
|
|
320
339
|
url += "?" + urllib.parse.urlencode(params, doseq=True)
|
|
321
340
|
headers = {
|
|
322
341
|
"Accept": "application/json",
|
|
323
|
-
"User-Agent": "mailsocket-python/0.
|
|
342
|
+
"User-Agent": "mailsocket-python/0.3.0",
|
|
324
343
|
}
|
|
325
344
|
headers.update(self._extra_headers)
|
|
326
345
|
# 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
|
+
Version: 0.3.0
|
|
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
|
|
@@ -31,17 +31,34 @@ The official Python client for the [mailsocket](https://mailsocket.app) v1 REST
|
|
|
31
31
|
Ephemeral email inboxes, message retrieval, and — the whole point — **wait for the OTP in one line**.
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
|
+
import time
|
|
35
|
+
|
|
34
36
|
from mailsocket import Client
|
|
35
37
|
|
|
36
38
|
client = Client("ms_live_...") # your API key from the dashboard
|
|
37
39
|
inbox = client.create_inbox(label="signup") # -> {"id": "inbox_...", "address": "..."}
|
|
38
40
|
|
|
41
|
+
started = time.time() # take the cutoff BEFORE the trigger
|
|
39
42
|
# hand inbox["address"] to whatever form sends the code, then:
|
|
40
|
-
result = client.wait_for_otp(inbox["id"])
|
|
43
|
+
result = client.wait_for_otp(inbox["id"], since=started) # blocks up to 60s
|
|
41
44
|
print(result.otp) # "123456"
|
|
42
45
|
print(result.confidence) # 0.95
|
|
43
46
|
```
|
|
44
47
|
|
|
48
|
+
Take `started` just before you trigger the email and pass it as `since`: a
|
|
49
|
+
code that lands before `wait_for_otp` starts is still caught, and an older
|
|
50
|
+
code in a reused inbox is not.
|
|
51
|
+
|
|
52
|
+
No email handy? `client.send_test_code(inbox_id)` drops a sample code into
|
|
53
|
+
the inbox so you can see the whole loop work:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
started = time.time()
|
|
57
|
+
client.send_test_code(inbox["id"]) # -> {"message_id": "msg_...", "sent_at": "..."}
|
|
58
|
+
result = client.wait_for_otp(inbox["id"], since=started)
|
|
59
|
+
print(result.otp) # the sample code
|
|
60
|
+
```
|
|
61
|
+
|
|
45
62
|
No polling loop. No regex over the email body. `wait_for_otp` long-polls the
|
|
46
63
|
server (which already knows how to extract the code) and hands back a
|
|
47
64
|
deterministic result with a confidence score.
|
|
@@ -69,6 +86,7 @@ client = Client(api_key, base_url="https://dash.mailsocket.app/api/v1")
|
|
|
69
86
|
| `list_messages(inbox_id, has_otp=None, subject_contains=None, sender=None, ...)` | `Page` — `sender` maps to the API's `from` filter |
|
|
70
87
|
| `get_latest(inbox_id)` | `dict` — newest message |
|
|
71
88
|
| `get_message(message_id)` | `dict` — full message |
|
|
89
|
+
| `send_test_code(inbox_id)` | `dict` — `message_id`, `sent_at` of a sample OTP email put straight into the inbox (no mail server; 5 per inbox / 20 per account per hour) |
|
|
72
90
|
|
|
73
91
|
### The moat
|
|
74
92
|
|
|
@@ -78,6 +96,14 @@ result = client.wait_for_link("inbox_abc123", timeout=60, since=None)
|
|
|
78
96
|
result = client.wait("inbox_abc123", timeout=60, min_confidence=0.0, since=None) # otp OR link
|
|
79
97
|
```
|
|
80
98
|
|
|
99
|
+
With a cutoff taken before the trigger (the usual case):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
started = time.time()
|
|
103
|
+
# trigger the email here
|
|
104
|
+
result = client.wait_for_otp("inbox_abc123", since=started)
|
|
105
|
+
```
|
|
106
|
+
|
|
81
107
|
All three return a `WaitResult` and take keyword-only arguments after
|
|
82
108
|
`inbox_id`. `timeout`/`min_confidence`/`since` shown above are the defaults —
|
|
83
109
|
call with no extra arguments for the common case.
|
|
@@ -112,11 +138,16 @@ Semantics:
|
|
|
112
138
|
`MailsocketError` (base) with subclasses:
|
|
113
139
|
|
|
114
140
|
- `AuthError` — HTTP 401 (bad/missing key).
|
|
115
|
-
- `NotFound` — HTTP 404
|
|
141
|
+
- `NotFound` — HTTP 404 (also an unknown, deleted or foreign inbox in
|
|
142
|
+
`send_test_code`).
|
|
116
143
|
- `RateLimited` — HTTP 429, carries `.subcode` (`rate_limited`,
|
|
117
144
|
`too_many_wait_requests`, `wait_capacity`) and `.retry_after`.
|
|
118
145
|
- `WaitTimeout` — no matching message within the overall deadline.
|
|
119
146
|
|
|
147
|
+
`send_test_code` on a disabled inbox raises the base `MailsocketError` with
|
|
148
|
+
`code="inbox_disabled"` and `status=409`. Over its hourly quota it raises
|
|
149
|
+
`RateLimited`; `.retry_after` says how many seconds until it frees up.
|
|
150
|
+
|
|
120
151
|
## Develop
|
|
121
152
|
|
|
122
153
|
```bash
|
|
@@ -131,6 +162,10 @@ invalid id never reaches the network.
|
|
|
131
162
|
|
|
132
163
|
## Changelog
|
|
133
164
|
|
|
165
|
+
- **0.3.0** — new `Client.send_test_code(inbox_id)` (`POST /inboxes/{id}/test-code`):
|
|
166
|
+
puts one sample OTP email into your own inbox without going through the
|
|
167
|
+
mail server, and returns `{"message_id", "sent_at"}`. README samples now
|
|
168
|
+
take `started = time.time()` before the trigger and pass `since=started`.
|
|
134
169
|
- **0.2.0** — BEHAVIOUR CHANGE: `wait_for_otp`/`wait_for_link`/`wait` now
|
|
135
170
|
default `since=None`, which omits the param so the server uses the
|
|
136
171
|
request start time, instead of the old `since=0` ("any message already
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mailsocket"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -557,6 +557,7 @@ def test_invalid_id_rejected_before_any_request_hits_real_server(raw):
|
|
|
557
557
|
lambda: client.wait_for_otp(raw, timeout=5),
|
|
558
558
|
lambda: client.wait_for_link(raw, timeout=5),
|
|
559
559
|
lambda: client.wait(raw, timeout=5),
|
|
560
|
+
lambda: client.send_test_code(raw),
|
|
560
561
|
]
|
|
561
562
|
for call in calls:
|
|
562
563
|
with pytest.raises(MailsocketError) as exc_info:
|
|
@@ -581,7 +582,7 @@ def test_metadata_consistency_across_files():
|
|
|
581
582
|
from pathlib import Path
|
|
582
583
|
|
|
583
584
|
root = Path(__file__).resolve().parents[1]
|
|
584
|
-
expected = "0.
|
|
585
|
+
expected = "0.3.0"
|
|
585
586
|
|
|
586
587
|
pyproject = tomllib.loads((root / "pyproject.toml").read_text())
|
|
587
588
|
assert pyproject["project"]["version"] == expected
|
|
@@ -657,3 +658,138 @@ def test_no_extra_headers_is_backwards_compatible(monkeypatch):
|
|
|
657
658
|
sent = capture.headers[0]
|
|
658
659
|
assert set(sent) >= {"authorization", "accept", "user-agent"}
|
|
659
660
|
assert "x-forwarded-proto" not in sent
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
# -- send_test_code (POST /inboxes/{id}/test-code) ----------------------------
|
|
664
|
+
|
|
665
|
+
TEST_CODE_DATA = {"message_id": "msg_test1", "sent_at": "2026-10-08T12:00:00Z"}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def test_send_test_code_posts_and_returns_data(monkeypatch):
|
|
669
|
+
client, transport = make_client([(201, {}, {"data": TEST_CODE_DATA})])
|
|
670
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
671
|
+
|
|
672
|
+
data = client.send_test_code("inbox_abc")
|
|
673
|
+
|
|
674
|
+
assert data == TEST_CODE_DATA
|
|
675
|
+
assert transport.calls == [("POST", "https://example.test/api/v1/inboxes/inbox_abc/test-code")]
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def test_send_test_code_sends_no_body(monkeypatch):
|
|
679
|
+
seen = []
|
|
680
|
+
|
|
681
|
+
def capture(request, timeout=None):
|
|
682
|
+
seen.append((request.get_method(), request.data, dict(request.header_items())))
|
|
683
|
+
|
|
684
|
+
class _Resp:
|
|
685
|
+
headers = {}
|
|
686
|
+
|
|
687
|
+
def getcode(self):
|
|
688
|
+
return 201
|
|
689
|
+
|
|
690
|
+
def read(self):
|
|
691
|
+
return json.dumps({"data": TEST_CODE_DATA}).encode()
|
|
692
|
+
|
|
693
|
+
def __enter__(self):
|
|
694
|
+
return self
|
|
695
|
+
|
|
696
|
+
def __exit__(self, *exc_info):
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
return _Resp()
|
|
700
|
+
|
|
701
|
+
monkeypatch.setattr("urllib.request.urlopen", capture)
|
|
702
|
+
Client("ms_live_test1234567890", base_url="https://example.test/api/v1").send_test_code("inbox_abc")
|
|
703
|
+
method, body, headers = seen[0]
|
|
704
|
+
assert method == "POST"
|
|
705
|
+
assert body is None
|
|
706
|
+
assert headers["Authorization"] == "Bearer ms_live_test1234567890"
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def test_send_test_code_encodes_id_as_one_segment(monkeypatch):
|
|
710
|
+
client, transport = make_client([(201, {}, {"data": TEST_CODE_DATA})])
|
|
711
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
712
|
+
|
|
713
|
+
client.send_test_code("a/b?c")
|
|
714
|
+
|
|
715
|
+
assert transport.calls[0][1] == "https://example.test/api/v1/inboxes/a%2Fb%3Fc/test-code"
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def test_send_test_code_404_raises_not_found(monkeypatch):
|
|
719
|
+
responses = [(404, {}, {"error": {"code": "not_found", "message": "Resource not found.", "fields": {}}})]
|
|
720
|
+
client, transport = make_client(responses)
|
|
721
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
722
|
+
|
|
723
|
+
with pytest.raises(NotFound) as exc_info:
|
|
724
|
+
client.send_test_code("inbox_foreign")
|
|
725
|
+
assert exc_info.value.status == 404
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def test_send_test_code_429_raises_rate_limited_with_retry_after(monkeypatch):
|
|
729
|
+
slept = []
|
|
730
|
+
monkeypatch.setattr(mailsocket.client, "_sleep", lambda s: slept.append(s))
|
|
731
|
+
responses = [
|
|
732
|
+
(
|
|
733
|
+
429,
|
|
734
|
+
{"Retry-After": "1800"},
|
|
735
|
+
{"error": {"code": "rate_limited", "message": "Too many test codes. Try again at 13:00 UTC.", "fields": {}}},
|
|
736
|
+
)
|
|
737
|
+
]
|
|
738
|
+
client, transport = make_client(responses)
|
|
739
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
740
|
+
|
|
741
|
+
with pytest.raises(RateLimited) as exc_info:
|
|
742
|
+
client.send_test_code("inbox_abc")
|
|
743
|
+
err = exc_info.value
|
|
744
|
+
assert err.retry_after == 1800.0
|
|
745
|
+
assert err.subcode == "rate_limited"
|
|
746
|
+
assert "Try again at" in str(err)
|
|
747
|
+
assert len(transport.calls) == 1 # never retried
|
|
748
|
+
assert slept == []
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def test_send_test_code_disabled_inbox_raises_mailsocket_error(monkeypatch):
|
|
752
|
+
responses = [
|
|
753
|
+
(
|
|
754
|
+
409,
|
|
755
|
+
{},
|
|
756
|
+
{"error": {"code": "inbox_disabled", "message": "This inbox is disabled. Enable it to receive a test code.", "fields": {}}},
|
|
757
|
+
)
|
|
758
|
+
]
|
|
759
|
+
client, transport = make_client(responses)
|
|
760
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
761
|
+
|
|
762
|
+
with pytest.raises(MailsocketError) as exc_info:
|
|
763
|
+
client.send_test_code("inbox_abc")
|
|
764
|
+
err = exc_info.value
|
|
765
|
+
assert type(err) is MailsocketError
|
|
766
|
+
assert err.code == "inbox_disabled"
|
|
767
|
+
assert err.status == 409
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_send_test_code_401_raises_auth_error(monkeypatch):
|
|
771
|
+
responses = [(401, {}, {"error": {"code": "authentication_required", "message": "Auth required.", "fields": {}}})]
|
|
772
|
+
client, transport = make_client(responses)
|
|
773
|
+
_monkeypatch_transport(monkeypatch, client, transport)
|
|
774
|
+
|
|
775
|
+
with pytest.raises(AuthError):
|
|
776
|
+
client.send_test_code("inbox_abc")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def test_readme_wait_samples_take_a_cutoff_before_the_trigger():
|
|
780
|
+
"""Every runnable wait sample in the README passes since=started, with
|
|
781
|
+
started taken before the trigger (ux4 SDK follow-up)."""
|
|
782
|
+
from pathlib import Path
|
|
783
|
+
|
|
784
|
+
readme = (Path(__file__).resolve().parents[1] / "README.md").read_text()
|
|
785
|
+
blocks = readme.split("```python")[1:]
|
|
786
|
+
samples = [b.split("```")[0] for b in blocks]
|
|
787
|
+
waits = [s for s in samples if "wait_for_otp(" in s and "since=None" not in s]
|
|
788
|
+
assert waits, "no wait sample found"
|
|
789
|
+
for sample in waits:
|
|
790
|
+
assert "started = time.time()" in sample
|
|
791
|
+
assert sample.index("started = time.time()") < sample.index("wait_for_otp(")
|
|
792
|
+
for line in sample.splitlines():
|
|
793
|
+
if "wait_for_otp(" in line or "wait_for_link(" in line:
|
|
794
|
+
assert "since=" in line, line
|
|
795
|
+
assert "client.send_test_code(inbox" in readme
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|