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.
- nowtempmail-0.1.2/.gitignore +8 -0
- nowtempmail-0.1.2/CHANGELOG.md +93 -0
- nowtempmail-0.1.2/LICENSE +21 -0
- nowtempmail-0.1.2/PKG-INFO +251 -0
- nowtempmail-0.1.2/README.md +198 -0
- nowtempmail-0.1.2/examples/signup_otp.py +62 -0
- nowtempmail-0.1.2/examples/test_signup_pytest.py +67 -0
- nowtempmail-0.1.2/plugin/LICENSE +21 -0
- nowtempmail-0.1.2/plugin/README.md +39 -0
- nowtempmail-0.1.2/pyproject.toml +89 -0
- nowtempmail-0.1.2/spec/openapi.json +3798 -0
- nowtempmail-0.1.2/src/nowtempmail/__init__.py +129 -0
- nowtempmail-0.1.2/src/nowtempmail/_http.py +603 -0
- nowtempmail-0.1.2/src/nowtempmail/client.py +413 -0
- nowtempmail-0.1.2/src/nowtempmail/errors.py +353 -0
- nowtempmail-0.1.2/src/nowtempmail/mailbox.py +437 -0
- nowtempmail-0.1.2/src/nowtempmail/message.py +103 -0
- nowtempmail-0.1.2/src/nowtempmail/py.typed +0 -0
- nowtempmail-0.1.2/src/nowtempmail/pytest_plugin.py +539 -0
- nowtempmail-0.1.2/src/nowtempmail/types.py +388 -0
- nowtempmail-0.1.2/tests/conftest.py +51 -0
- nowtempmail-0.1.2/tests/core/test_client.py +241 -0
- nowtempmail-0.1.2/tests/core/test_http.py +355 -0
- nowtempmail-0.1.2/tests/core/test_mailbox.py +369 -0
- nowtempmail-0.1.2/tests/core/test_message.py +156 -0
- nowtempmail-0.1.2/tests/helpers/mock_api.py +504 -0
- nowtempmail-0.1.2/tests/plugin/test_fixtures.py +398 -0
- nowtempmail-0.1.2/tests/plugin/test_inert.py +128 -0
- nowtempmail-0.1.2/tests/plugin/test_redaction.py +99 -0
- nowtempmail-0.1.2/tests/plugin/test_watermark.py +88 -0
|
@@ -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())
|