nowtempmail 0.1.2__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.
@@ -0,0 +1,8 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ .venv/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ `nowtempmail` and `pytest-nowtempmail` are released together from this
9
+ repository and share a version number.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.1.2] - 2026-08-30
14
+
15
+ First release where both distributions reach PyPI together. Package code is
16
+ again unchanged from 0.1.0.
17
+
18
+ ### Fixed
19
+
20
+ - **The PyPI publish step could not be retried.** It uploads both distributions
21
+ in one call and PyPI processes them one at a time, so when the second failed
22
+ the first was already published and every retry then died on the duplicate
23
+ file rather than on the real problem — leaving a version bump as the only way
24
+ forward. The TestPyPI step had `skip-existing` from the start; the PyPI step
25
+ did not, and that asymmetry cost the 0.1.1 release.
26
+
27
+ ### Note on 0.1.1
28
+
29
+ `nowtempmail` 0.1.1 is on PyPI and is a complete, working SDK. Its companion
30
+ `pytest-nowtempmail` 0.1.1 is not, because that upload failed. Install 0.1.2 or
31
+ later if you want the fixtures; nothing is wrong with 0.1.1 of the SDK itself.
32
+
33
+ ## [0.1.1] - 2026-08-30
34
+
35
+ First release on PyPI. Identical package code to 0.1.0 — the only changes are
36
+ to the release pipeline and this file.
37
+
38
+ ### Fixed
39
+
40
+ - **The release workflow's smoke check rejected a working package.** It ran
41
+ `pytest --version --trace-config | grep -q nowtempmail` to prove the
42
+ `pytest11` entry point had registered. `--version` makes pytest print the
43
+ version and exit *before* emitting any plugin trace, so the grep matched
44
+ nothing and the step failed after every real check had passed — the install
45
+ succeeded, the import succeeded, the fixtures were registered. It now asserts
46
+ against installed metadata instead: the entry point resolves, loads, and
47
+ exposes `ntm_inbox`, `ntm_fresh_inbox` and `ntm_client`.
48
+ - **`CHANGELOG.md` did not exist**, while `pyproject.toml` advertised a
49
+ Changelog URL. The PyPI project page would have carried a dead link.
50
+
51
+ ## [0.1.0] - 2026-08-30
52
+
53
+ Published to **TestPyPI only**. Its release run failed the smoke gate described
54
+ above, so it was never promoted to PyPI. Nothing is wrong with the 0.1.0
55
+ artifacts; they are simply superseded by 0.1.1.
56
+
57
+ ### Added
58
+
59
+ - **`NowTempMail` client** — `me()`, `domains.list()`,
60
+ `mailboxes.create/get/attach/delete`, `reservations`, `webhooks`. Zero runtime
61
+ dependencies, on `urllib`: this package installs into test environments that
62
+ already hold pytest, Playwright and somebody else's HTTP client, and a hard
63
+ `httpx` requirement would be a resolver conflict in exactly those places.
64
+ - **`Mailbox`** with `wait_for_code()` and `wait_for_message()`. Verification
65
+ codes are extracted server-side at ingest, so `wait_for_code()` never fetches
66
+ a message body — one request per poll, and no message content transferred.
67
+ - **pytest fixtures**, via the `pytest-nowtempmail` distribution:
68
+ `ntm_inbox` (session-scoped and shared, because mailbox creation is the only
69
+ call that spends daily quota and deleting one never refunds it),
70
+ `ntm_fresh_inbox`, `ntm_client` and `ntm_options`. Every wait is watermarked
71
+ per test, so a shared inbox cannot resolve with an earlier test's code.
72
+ - **Report redaction.** A matched message is recorded on the test and printed
73
+ on failure — sender, subject, timestamp and extracted codes, but **not**
74
+ bodies or links. A CI artifact is downloaded by anyone with repository read
75
+ access, and a password-reset link inside one is a live credential.
76
+ - Full typing with `py.typed`, and an error taxonomy that branches on
77
+ `err.code` so an unrecognised problem code from a newer API cannot break an
78
+ older client.
79
+
80
+ ### Notes
81
+
82
+ - Two error class names differ from the JavaScript SDK because the literal
83
+ translations shadow `OSError` builtins: `WaitTimeoutError` (not
84
+ `TimeoutError`) and `TransportError` (not `ConnectionError`).
85
+ - `ignore_existing` carries a one-second ambiguity that cannot be removed —
86
+ `receivedAt` on the wire has one-second granularity. It resolves by excluding,
87
+ because including risks silently returning the stale code the flag exists to
88
+ avoid. See `MessageFilter.ignore_existing`.
89
+
90
+ [Unreleased]: https://github.com/nowtempmail/nowtempmail-python/compare/v0.1.2...HEAD
91
+ [0.1.2]: https://github.com/nowtempmail/nowtempmail-python/compare/v0.1.1...v0.1.2
92
+ [0.1.1]: https://github.com/nowtempmail/nowtempmail-python/compare/v0.1.0...v0.1.1
93
+ [0.1.0]: https://github.com/nowtempmail/nowtempmail-python/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NowTempMail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,251 @@
1
+ Metadata-Version: 2.5
2
+ Name: nowtempmail
3
+ Version: 0.1.2
4
+ Summary: Disposable mailboxes and OTP codes for your tests. Verification codes are extracted server-side, so there is no regex to write.
5
+ Project-URL: Homepage, https://nowtempmail.com/developers
6
+ Project-URL: Documentation, https://nowtempmail.com/developers
7
+ Project-URL: Source, https://github.com/nowtempmail/nowtempmail-python
8
+ Project-URL: Changelog, https://github.com/nowtempmail/nowtempmail-python/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://nowtempmail.userjot.com
10
+ Author: NowTempMail
11
+ License: MIT License
12
+
13
+ Copyright (c) 2026 NowTempMail
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: disposable-email,e2e,email,otp,playwright,pytest,qa,selenium,temp-mail,testing,verification-code
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Programming Language :: Python :: 3.14
44
+ Classifier: Topic :: Communications :: Email
45
+ Classifier: Topic :: Software Development :: Testing
46
+ Classifier: Typing :: Typed
47
+ Requires-Python: >=3.10
48
+ Provides-Extra: dev
49
+ Requires-Dist: mypy>=1.11; extra == 'dev'
50
+ Requires-Dist: pytest>=8.0; extra == 'dev'
51
+ Requires-Dist: ruff>=0.6; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # nowtempmail
55
+
56
+ Disposable mailboxes and OTP codes for your tests. **Verification codes are
57
+ extracted server-side, so there is no regex to write.**
58
+
59
+ ```bash
60
+ pip install pytest-nowtempmail # the fixtures (most people want this)
61
+ pip install nowtempmail # the client on its own
62
+ ```
63
+
64
+ ```python
65
+ def test_signup(page, ntm_inbox):
66
+ page.goto("/signup")
67
+ page.fill("#email", ntm_inbox.address)
68
+ page.click("text=Create account")
69
+ page.fill("#code", ntm_inbox.wait_for_code()) # only this test's mail
70
+ ```
71
+
72
+ Get a key at <https://nowtempmail.com/developers> and set `NTM_API_KEY`. There is
73
+ no config file and no `login` command — a key on a command line ends up in `ps`
74
+ output and shell history.
75
+
76
+ **Zero runtime dependencies.** This installs into test environments that already
77
+ hold pytest, Playwright and usually an HTTP-client pin from some other SDK, so
78
+ it deliberately brings nothing that could conflict with them.
79
+
80
+ **Server-side only.** `/v1` rejects any request carrying an `Origin` header. This
81
+ is a library for test runners, CI and backends — not for anything in a page.
82
+
83
+ ---
84
+
85
+ ## The fixtures
86
+
87
+ | Fixture | Scope | What it is |
88
+ |---|---|---|
89
+ | `ntm_inbox` | session | One mailbox, shared by every test. **Start here.** |
90
+ | `ntm_fresh_inbox` | function | A mailbox of this test's own. Costs quota — see below. |
91
+ | `ntm_client` | session | The `NowTempMail` client. |
92
+ | `ntm_options` | session | Resolved configuration, for overriding in a conftest. |
93
+
94
+ ### Why `ntm_inbox` is shared, and why that is safe
95
+
96
+ `POST /v1/mailboxes` is the only endpoint that consumes daily quota, the counter
97
+ increments on creation, and **it is never decremented on delete**. A per-test
98
+ mailbox therefore spends the Free plan's 50 mailboxes/day inside a single CI
99
+ run — the suite goes green locally, then starts failing with `quota_exceeded`
100
+ partway through the third run of the afternoon, and the failure has nothing to
101
+ do with the code that triggered it.
102
+
103
+ Sharing is safe because **every wait is watermarked**. An autouse fixture
104
+ re-stamps a boundary at the start of each test, and `wait_for_code()` /
105
+ `wait_for_message()` default their `after` to it — so a code triggered by an
106
+ earlier test is not a candidate.
107
+
108
+ The watermark is floored to the whole second and set one second behind, because
109
+ `received_at` on the wire has one-second granularity and a CI runner's clock is
110
+ not the API's clock. **The consequence is a real limit:** two tests that trigger
111
+ mail within about a second of each other are not isolated from one another. Fix
112
+ that by disambiguating on content, which is what you want anyway:
113
+
114
+ ```python
115
+ code = ntm_inbox.wait_for_code(sender="github", subject="Sign in")
116
+ ```
117
+
118
+ Use `ntm_fresh_inbox` only where sharing genuinely cannot work — a test
119
+ asserting on an empty inbox, one that deletes messages out from under a sibling,
120
+ or one needing a different domain or TTL. A 40-test file using it burns 40 of
121
+ the Free plan's 50/day, and teardown does not give any of them back.
122
+
123
+ ### Configuration
124
+
125
+ ```toml
126
+ [tool.pytest.ini_options]
127
+ ntm_base_url = "https://nowtempmail.com"
128
+ ntm_attach_body = false # read "What reaches your CI report" first
129
+ ntm_watermark_skew_ms = 1000
130
+ ntm_mailbox_ttl_seconds = 3600
131
+ ntm_mailbox_domain = "" # empty = the rotation's choice
132
+ ```
133
+
134
+ There is deliberately no `ntm_api_key` option. An API key in `pyproject.toml` is
135
+ an API key in git.
136
+
137
+ ### What reaches your CI report
138
+
139
+ When a wait matches, the message is recorded on the test — and on failure it is
140
+ printed in the output, which is where "the wrong mail matched" actually gets
141
+ diagnosed.
142
+
143
+ **Bodies and links are withheld by default.** A report is zipped into an
144
+ artifact, downloaded by whoever has repo read access, and pasted into issues; a
145
+ password-reset link inside one is a live credential with a working expiry. What
146
+ you get is sender, subject, timestamp, a link *count*, and the extracted codes.
147
+
148
+ The codes are included deliberately even though they are secrets: a verification
149
+ code is single-use, already spent by the time anyone reads the report, and it is
150
+ the one value you cannot debug an OTP flow without.
151
+
152
+ Set `ntm_attach_body = true` to include bodies and links, and treat the whole
153
+ artifact as a secret if you do.
154
+
155
+ ---
156
+
157
+ ## The client on its own
158
+
159
+ ```python
160
+ from nowtempmail import NowTempMail
161
+
162
+ ntm = NowTempMail() # reads NTM_API_KEY
163
+ inbox = ntm.mailboxes.create() # returns once it can RECEIVE
164
+ sign_up(inbox.address)
165
+ code = inbox.wait_for_code(sender="acme", timeout=120)
166
+ ```
167
+
168
+ `mailboxes.create()` confirms the mailbox is readable before returning. The mail
169
+ receiver drops inbound mail if its inbox lookup misses, so "created" and "able
170
+ to receive" are not the same instant, and a handle returned before the second
171
+ one hands you a race you cannot see.
172
+
173
+ **Creation is never retried.** It is the only call that spends daily quota and
174
+ the API has no idempotency key, so a transport failure raises
175
+ `MailboxMayExistError` rather than quietly spending a second mailbox.
176
+
177
+ ### Waiting
178
+
179
+ ```python
180
+ inbox.wait_for_code(
181
+ sender="github", # substring, case-insensitive, or a compiled pattern
182
+ subject=re.compile(r"\d{6}"),
183
+ after=some_datetime, # default: the mailbox's created_at
184
+ ignore_existing=True, # for resend flows, where the stale code is the bug
185
+ timeout=120,
186
+ cancel=threading.Event(),
187
+ )
188
+ ```
189
+
190
+ `after` defaults to the mailbox's `created_at`, **not** to the moment you called.
191
+ Mail routinely lands before anyone waits for it — a form submits, mail arrives in
192
+ 300ms, and the test gets around to waiting a second later. Defaulting to call
193
+ time makes that message invisible and produces a flake nobody can reproduce.
194
+
195
+ `wait_for_code()` never fetches a body: codes ride on the message summary
196
+ because they are extracted at ingest. One request per poll, no message content
197
+ transferred.
198
+
199
+ A timeout raises `WaitTimeoutError` carrying **`messages_seen`** — the number
200
+ that separates "nothing arrived" from "mail arrived but nothing matched".
201
+
202
+ ### Errors
203
+
204
+ Branch on `err.code`, not on the class. The API may grow a problem code this
205
+ package has never heard of, and an unrecognised one arrives on the base class
206
+ carrying that code verbatim rather than being rewritten.
207
+
208
+ ```python
209
+ from nowtempmail import NowTempMailError, QuotaExceededError
210
+
211
+ try:
212
+ inbox = ntm.mailboxes.create()
213
+ except QuotaExceededError as err:
214
+ print(err.retry_after)
215
+ except NowTempMailError as err:
216
+ print(err.code, err.request_id) # request_id is what support asks for
217
+ ```
218
+
219
+ No error message, `repr()` or log line ever contains the API key.
220
+
221
+ **Two class names differ from the JavaScript SDK**, because the literal
222
+ translations shadow builtins that are `OSError` subclasses:
223
+
224
+ | JavaScript | Python |
225
+ |---|---|
226
+ | `TimeoutError` | `WaitTimeoutError` |
227
+ | `ConnectionError` | `TransportError` |
228
+
229
+ ---
230
+
231
+ ## Quota, plainly
232
+
233
+ | | |
234
+ |---|---|
235
+ | Spends daily quota | `mailboxes.create()` — nothing else |
236
+ | Refunded on delete | **No.** Never. |
237
+ | Free plan | 50 mailboxes/day, 30 requests/minute |
238
+ | Check what is left | `ntm.me().usage.mailboxes_remaining_today` |
239
+
240
+ Polling `me()` is free: it runs in peek mode server-side and does not consume
241
+ the budget it reports on.
242
+
243
+ ---
244
+
245
+ ## Links
246
+
247
+ - Docs and keys — <https://nowtempmail.com/developers>
248
+ - Source — <https://github.com/nowtempmail/nowtempmail-python>
249
+ - Issues — <https://nowtempmail.userjot.com>
250
+
251
+ MIT licensed.
@@ -0,0 +1,198 @@
1
+ # nowtempmail
2
+
3
+ Disposable mailboxes and OTP codes for your tests. **Verification codes are
4
+ extracted server-side, so there is no regex to write.**
5
+
6
+ ```bash
7
+ pip install pytest-nowtempmail # the fixtures (most people want this)
8
+ pip install nowtempmail # the client on its own
9
+ ```
10
+
11
+ ```python
12
+ def test_signup(page, ntm_inbox):
13
+ page.goto("/signup")
14
+ page.fill("#email", ntm_inbox.address)
15
+ page.click("text=Create account")
16
+ page.fill("#code", ntm_inbox.wait_for_code()) # only this test's mail
17
+ ```
18
+
19
+ Get a key at <https://nowtempmail.com/developers> and set `NTM_API_KEY`. There is
20
+ no config file and no `login` command — a key on a command line ends up in `ps`
21
+ output and shell history.
22
+
23
+ **Zero runtime dependencies.** This installs into test environments that already
24
+ hold pytest, Playwright and usually an HTTP-client pin from some other SDK, so
25
+ it deliberately brings nothing that could conflict with them.
26
+
27
+ **Server-side only.** `/v1` rejects any request carrying an `Origin` header. This
28
+ is a library for test runners, CI and backends — not for anything in a page.
29
+
30
+ ---
31
+
32
+ ## The fixtures
33
+
34
+ | Fixture | Scope | What it is |
35
+ |---|---|---|
36
+ | `ntm_inbox` | session | One mailbox, shared by every test. **Start here.** |
37
+ | `ntm_fresh_inbox` | function | A mailbox of this test's own. Costs quota — see below. |
38
+ | `ntm_client` | session | The `NowTempMail` client. |
39
+ | `ntm_options` | session | Resolved configuration, for overriding in a conftest. |
40
+
41
+ ### Why `ntm_inbox` is shared, and why that is safe
42
+
43
+ `POST /v1/mailboxes` is the only endpoint that consumes daily quota, the counter
44
+ increments on creation, and **it is never decremented on delete**. A per-test
45
+ mailbox therefore spends the Free plan's 50 mailboxes/day inside a single CI
46
+ run — the suite goes green locally, then starts failing with `quota_exceeded`
47
+ partway through the third run of the afternoon, and the failure has nothing to
48
+ do with the code that triggered it.
49
+
50
+ Sharing is safe because **every wait is watermarked**. An autouse fixture
51
+ re-stamps a boundary at the start of each test, and `wait_for_code()` /
52
+ `wait_for_message()` default their `after` to it — so a code triggered by an
53
+ earlier test is not a candidate.
54
+
55
+ The watermark is floored to the whole second and set one second behind, because
56
+ `received_at` on the wire has one-second granularity and a CI runner's clock is
57
+ not the API's clock. **The consequence is a real limit:** two tests that trigger
58
+ mail within about a second of each other are not isolated from one another. Fix
59
+ that by disambiguating on content, which is what you want anyway:
60
+
61
+ ```python
62
+ code = ntm_inbox.wait_for_code(sender="github", subject="Sign in")
63
+ ```
64
+
65
+ Use `ntm_fresh_inbox` only where sharing genuinely cannot work — a test
66
+ asserting on an empty inbox, one that deletes messages out from under a sibling,
67
+ or one needing a different domain or TTL. A 40-test file using it burns 40 of
68
+ the Free plan's 50/day, and teardown does not give any of them back.
69
+
70
+ ### Configuration
71
+
72
+ ```toml
73
+ [tool.pytest.ini_options]
74
+ ntm_base_url = "https://nowtempmail.com"
75
+ ntm_attach_body = false # read "What reaches your CI report" first
76
+ ntm_watermark_skew_ms = 1000
77
+ ntm_mailbox_ttl_seconds = 3600
78
+ ntm_mailbox_domain = "" # empty = the rotation's choice
79
+ ```
80
+
81
+ There is deliberately no `ntm_api_key` option. An API key in `pyproject.toml` is
82
+ an API key in git.
83
+
84
+ ### What reaches your CI report
85
+
86
+ When a wait matches, the message is recorded on the test — and on failure it is
87
+ printed in the output, which is where "the wrong mail matched" actually gets
88
+ diagnosed.
89
+
90
+ **Bodies and links are withheld by default.** A report is zipped into an
91
+ artifact, downloaded by whoever has repo read access, and pasted into issues; a
92
+ password-reset link inside one is a live credential with a working expiry. What
93
+ you get is sender, subject, timestamp, a link *count*, and the extracted codes.
94
+
95
+ The codes are included deliberately even though they are secrets: a verification
96
+ code is single-use, already spent by the time anyone reads the report, and it is
97
+ the one value you cannot debug an OTP flow without.
98
+
99
+ Set `ntm_attach_body = true` to include bodies and links, and treat the whole
100
+ artifact as a secret if you do.
101
+
102
+ ---
103
+
104
+ ## The client on its own
105
+
106
+ ```python
107
+ from nowtempmail import NowTempMail
108
+
109
+ ntm = NowTempMail() # reads NTM_API_KEY
110
+ inbox = ntm.mailboxes.create() # returns once it can RECEIVE
111
+ sign_up(inbox.address)
112
+ code = inbox.wait_for_code(sender="acme", timeout=120)
113
+ ```
114
+
115
+ `mailboxes.create()` confirms the mailbox is readable before returning. The mail
116
+ receiver drops inbound mail if its inbox lookup misses, so "created" and "able
117
+ to receive" are not the same instant, and a handle returned before the second
118
+ one hands you a race you cannot see.
119
+
120
+ **Creation is never retried.** It is the only call that spends daily quota and
121
+ the API has no idempotency key, so a transport failure raises
122
+ `MailboxMayExistError` rather than quietly spending a second mailbox.
123
+
124
+ ### Waiting
125
+
126
+ ```python
127
+ inbox.wait_for_code(
128
+ sender="github", # substring, case-insensitive, or a compiled pattern
129
+ subject=re.compile(r"\d{6}"),
130
+ after=some_datetime, # default: the mailbox's created_at
131
+ ignore_existing=True, # for resend flows, where the stale code is the bug
132
+ timeout=120,
133
+ cancel=threading.Event(),
134
+ )
135
+ ```
136
+
137
+ `after` defaults to the mailbox's `created_at`, **not** to the moment you called.
138
+ Mail routinely lands before anyone waits for it — a form submits, mail arrives in
139
+ 300ms, and the test gets around to waiting a second later. Defaulting to call
140
+ time makes that message invisible and produces a flake nobody can reproduce.
141
+
142
+ `wait_for_code()` never fetches a body: codes ride on the message summary
143
+ because they are extracted at ingest. One request per poll, no message content
144
+ transferred.
145
+
146
+ A timeout raises `WaitTimeoutError` carrying **`messages_seen`** — the number
147
+ that separates "nothing arrived" from "mail arrived but nothing matched".
148
+
149
+ ### Errors
150
+
151
+ Branch on `err.code`, not on the class. The API may grow a problem code this
152
+ package has never heard of, and an unrecognised one arrives on the base class
153
+ carrying that code verbatim rather than being rewritten.
154
+
155
+ ```python
156
+ from nowtempmail import NowTempMailError, QuotaExceededError
157
+
158
+ try:
159
+ inbox = ntm.mailboxes.create()
160
+ except QuotaExceededError as err:
161
+ print(err.retry_after)
162
+ except NowTempMailError as err:
163
+ print(err.code, err.request_id) # request_id is what support asks for
164
+ ```
165
+
166
+ No error message, `repr()` or log line ever contains the API key.
167
+
168
+ **Two class names differ from the JavaScript SDK**, because the literal
169
+ translations shadow builtins that are `OSError` subclasses:
170
+
171
+ | JavaScript | Python |
172
+ |---|---|
173
+ | `TimeoutError` | `WaitTimeoutError` |
174
+ | `ConnectionError` | `TransportError` |
175
+
176
+ ---
177
+
178
+ ## Quota, plainly
179
+
180
+ | | |
181
+ |---|---|
182
+ | Spends daily quota | `mailboxes.create()` — nothing else |
183
+ | Refunded on delete | **No.** Never. |
184
+ | Free plan | 50 mailboxes/day, 30 requests/minute |
185
+ | Check what is left | `ntm.me().usage.mailboxes_remaining_today` |
186
+
187
+ Polling `me()` is free: it runs in peek mode server-side and does not consume
188
+ the budget it reports on.
189
+
190
+ ---
191
+
192
+ ## Links
193
+
194
+ - Docs and keys — <https://nowtempmail.com/developers>
195
+ - Source — <https://github.com/nowtempmail/nowtempmail-python>
196
+ - Issues — <https://nowtempmail.userjot.com>
197
+
198
+ MIT licensed.
@@ -0,0 +1,62 @@
1
+ """The client without pytest: create an inbox, wait for the code, tidy up.
2
+
3
+ NTM_API_KEY=ntm_live_... python examples/signup_otp.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from nowtempmail import (
11
+ MailboxMayExistError,
12
+ NowTempMail,
13
+ QuotaExceededError,
14
+ WaitTimeoutError,
15
+ )
16
+
17
+
18
+ def main() -> int:
19
+ ntm = NowTempMail() # reads NTM_API_KEY
20
+
21
+ account = ntm.me() # free: peek mode, does not spend what it reports
22
+ left = account.usage.mailboxes_remaining_today
23
+ print(f"plan {account.plan.label}, {left} mailboxes left today")
24
+
25
+ try:
26
+ # Returns once the mailbox can actually RECEIVE, not merely once it
27
+ # exists -- mail arriving before that is dropped, not queued.
28
+ inbox = ntm.mailboxes.create(ttl_seconds=900)
29
+ except QuotaExceededError as err:
30
+ print(f"daily budget spent; resets at {err.retry_after}s", file=sys.stderr)
31
+ return 1
32
+ except MailboxMayExistError:
33
+ # The request went out and the outcome is unknowable. The SDK will not
34
+ # send a second one, because creation is the only call that spends quota
35
+ # and there is no idempotency key.
36
+ print("creation failed in transit -- check me() before retrying", file=sys.stderr)
37
+ return 1
38
+
39
+ print(f"address {inbox.address}")
40
+ print(f"expires {inbox.expires_at.isoformat()}")
41
+ if not inbox.confirmed:
42
+ print("warning: not readable yet; mail in the next second may be dropped", file=sys.stderr)
43
+
44
+ print("\nSend a message with a verification code to that address.")
45
+ try:
46
+ # Never fetches a body: codes are extracted server-side at ingest.
47
+ code = inbox.wait_for_code(timeout=180)
48
+ except WaitTimeoutError as err:
49
+ # messages_seen is the number that matters -- it separates "nothing
50
+ # arrived" from "mail arrived but nothing matched".
51
+ print(f"\ngave up: {err.message}", file=sys.stderr)
52
+ return 4
53
+
54
+ print(f"\ncode {code}")
55
+
56
+ deleted = inbox.delete()
57
+ print(f"deleted {deleted} message(s) -- quota is NOT refunded")
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())