tempid 2.0.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.
tempid-2.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rahul Patel
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.
tempid-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,372 @@
1
+ Metadata-Version: 2.4
2
+ Name: tempid
3
+ Version: 2.0.0
4
+ Summary: Unique IDs that automatically expire — like UUID but with a TTL
5
+ Author: Rahul Patel
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/VachhaniRahul/TempID-PyPI-Repo
8
+ Project-URL: Documentation, https://github.com/VachhaniRahul/TempID-PyPI-Repo#readme
9
+ Project-URL: Issues, https://github.com/VachhaniRahul/TempID-PyPI-Repo/issues
10
+ Keywords: id,uuid,token,expiry,ttl,jwt,auth,password-reset,magic-link,otp,invite,signed-url,temporary
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Security
20
+ Classifier: Development Status :: 4 - Beta
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: cryptography>=41.0.0
25
+ Provides-Extra: redis
26
+ Requires-Dist: redis>=5.0.0; extra == "redis"
27
+ Provides-Extra: mongo
28
+ Requires-Dist: pymongo>=4.0.0; extra == "mongo"
29
+ Provides-Extra: mysql
30
+ Requires-Dist: pymysql>=1.0.0; extra == "mysql"
31
+ Requires-Dist: dbutils>=3.0.0; extra == "mysql"
32
+ Provides-Extra: postgres
33
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
34
+ Provides-Extra: async-redis
35
+ Requires-Dist: redis>=5.0.0; extra == "async-redis"
36
+ Provides-Extra: async-sqlite
37
+ Requires-Dist: aiosqlite>=0.20.0; extra == "async-sqlite"
38
+ Provides-Extra: async-mongo
39
+ Requires-Dist: motor>=3.3.0; extra == "async-mongo"
40
+ Provides-Extra: async-postgres
41
+ Requires-Dist: asyncpg>=0.29.0; extra == "async-postgres"
42
+ Provides-Extra: async-mysql
43
+ Requires-Dist: aiomysql>=0.2.0; extra == "async-mysql"
44
+ Provides-Extra: all
45
+ Requires-Dist: redis>=5.0.0; extra == "all"
46
+ Requires-Dist: pymongo>=4.0.0; extra == "all"
47
+ Requires-Dist: pymysql>=1.0.0; extra == "all"
48
+ Requires-Dist: dbutils>=3.0.0; extra == "all"
49
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
50
+ Requires-Dist: aiosqlite>=0.20.0; extra == "all"
51
+ Requires-Dist: motor>=3.3.0; extra == "all"
52
+ Requires-Dist: asyncpg>=0.29.0; extra == "all"
53
+ Requires-Dist: aiomysql>=0.2.0; extra == "all"
54
+ Provides-Extra: dev
55
+ Requires-Dist: pytest>=7; extra == "dev"
56
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
57
+ Requires-Dist: ruff; extra == "dev"
58
+ Requires-Dist: mypy; extra == "dev"
59
+ Dynamic: license-file
60
+
61
+ # tempid
62
+
63
+ > Unique IDs that automatically expire, store encrypted payloads, and strictly limit usages — built for Enterprise Python.
64
+
65
+ `tempid` gives you Stripe-like, highly secure temporary tokens (`TEMP-V2.XXXX...`) without the boilerplate.
66
+
67
+ `tempid` operates as a full **Hybrid Token Engine**. It supports embedded JSON payloads, Strict Use-Count Limits (e.g. "burn after reading"), and high-concurrency Async/Sync database backends.
68
+
69
+ ---
70
+
71
+ ## ⚡ Features at a Glance
72
+ - **Stateless by Default:** Tokens hold their own expiration time. No database required for basic time-based expiry.
73
+ - **Encrypted Payloads:** Embed JSON data directly inside the token (up to 512 bytes). Fully encrypted, users cannot read or tamper with it.
74
+ - **Strict Use Limits:** Set a `max_uses` limit on tokens (e.g., a one-time-use OTP).
75
+ - **Enterprise DB Backends:** Built-in connection pooling for Redis, PostgreSQL, MySQL, SQLite, and MongoDB.
76
+ - **Hybrid Async Engine:** First-class `async/await` support for FastAPI, Sanic, and Starlette via dedicated `AsyncBackends`.
77
+ - **Zero Dependencies (Core):** The core engine uses only standard Python libraries.
78
+
79
+ ---
80
+
81
+ ## 📦 Installation
82
+
83
+ ```bash
84
+ pip install tempid
85
+ ```
86
+
87
+ If you plan to use database backends for strict usage limits (`max_uses`), install the appropriate driver:
88
+ ```bash
89
+ pip install tempid[redis] # For RedisBackend
90
+ pip install tempid[mysql] # For MySQLBackend
91
+ pip install tempid[postgres] # For PostgreSQLBackend
92
+ pip install tempid[mongo] # For MongoBackend
93
+ pip install tempid[async-postgres] # For AsyncPostgreSQLBackend
94
+ pip install tempid[async-mysql] # For AsyncMySQLBackend
95
+ pip install tempid[async-mongo] # For AsyncMongoBackend
96
+ pip install tempid[all] # Install all drivers
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 🚀 Quick Start (Stateless Mode)
102
+
103
+ By default, `tempid` operates completely offline without needing a database. Expiration is cryptographically signed into the token itself.
104
+
105
+ ```python
106
+ from tempid import TempID
107
+
108
+ # 1. Create a token that expires in 15 minutes
109
+ tid = TempID.new("15m")
110
+ print(tid.value) # TEMP-V2.AIAGU-PSOHJ...
111
+
112
+ # 2. Check time remaining
113
+ print(tid.remaining()) # "14m 59s"
114
+
115
+ # 3. Verify securely (e.g., when a user submits it)
116
+ # verify() returns the TempID object if valid, or None if expired/tampered
117
+ verified = TempID.verify(tid.value)
118
+ if verified:
119
+ print("Token is valid!")
120
+ else:
121
+ print("Token is invalid or expired.")
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 🧳 Encrypted Payloads
127
+
128
+ You can embed JSON-serializable dictionaries directly into the token. The data is heavily compressed (zlib) and **fully encrypted** (AES-like CTR mode). Users cannot read or tamper with it.
129
+
130
+ ```python
131
+ # Create a token with a payload
132
+ tid = TempID.new("2h", payload={"user_id": 42, "role": "admin"})
133
+
134
+ # Later, verify and extract the data
135
+ verified_token = TempID.verify(tid.value)
136
+
137
+ if verified_token:
138
+ print(verified_token.payload["user_id"]) # 42
139
+ print(verified_token.payload["role"]) # "admin"
140
+ ```
141
+ *Note: Maximum payload size after compression is 512 bytes. If exceeded, `TempIDPayloadTooLargeError` is raised.*
142
+
143
+ ---
144
+
145
+ ## 🛡️ Strict Use Limits (`max_uses`)
146
+
147
+ Need a token that can only be used exactly 3 times, or a password reset link that burns after 1 use? You can set `max_uses`.
148
+
149
+ To use this feature, you **must** configure a database backend at application startup so `tempid` can track the usage atomically across your servers.
150
+
151
+ ### 1. Configure a Backend (Startup)
152
+ ```python
153
+ from tempid import configure
154
+ from tempid.backends import RedisBackend
155
+
156
+ # Run this ONCE when your app starts
157
+ configure(store=RedisBackend("redis://localhost:6379/0"))
158
+ ```
159
+
160
+ ### 2. Generate a Limited Token
161
+ ```python
162
+ # Expires in 1 hour, OR after 1 successful use
163
+ tid = TempID.new("1h", max_uses=1)
164
+ ```
165
+
166
+ ### 3. Consume the Token
167
+ ```python
168
+ # check_uses=True checks the database limit WITHOUT consuming a use
169
+ verified = TempID.verify(token_str, check_uses=True)
170
+
171
+ if verified:
172
+ # use() attempts to consume 1 use atomically in the database
173
+ if verified.use():
174
+ print("Success! Action performed.")
175
+ else:
176
+ print("Token limit reached (Already used!).")
177
+ ```
178
+
179
+ ### 4. Check Remaining Uses
180
+ ```python
181
+ info = verified.uses_info()
182
+ print(f"Used: {info['used']} / Total: {info['total']}")
183
+ ```
184
+
185
+ ---
186
+
187
+ ## ⚡ Async Support (FastAPI / Starlette)
188
+
189
+ `tempid` is fully async-native. If you are building high-concurrency apps, use the `Async` backends and methods to prevent blocking your event loop.
190
+
191
+ ```python
192
+ import asyncio
193
+ from tempid import TempID, configure
194
+ from tempid.async_backends import AsyncPostgreSQLBackend
195
+
196
+ # 1. Configure the Async Backend
197
+ configure(store=AsyncPostgreSQLBackend("postgresql://root:pass@localhost/mydb"))
198
+
199
+ async def api_endpoint(token_string: str):
200
+ # 2. Verify (Checks DB asynchronously without blocking)
201
+ tid = await TempID.verify_async(token_string, check_uses=True)
202
+
203
+ if not tid:
204
+ return {"error": "Invalid or exhausted token"}
205
+
206
+ # 3. Consume a use
207
+ success = await tid.use_async()
208
+ if success:
209
+ return {"data": tid.payload}
210
+ else:
211
+ return {"error": "Token already used!"}
212
+
213
+ # Check info
214
+ info = await tid.uses_info_async()
215
+ print(info)
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 🏭 Supported Backends Reference
221
+
222
+ All backends guarantee **perfect atomicity** (no race conditions or double-spending) even under extreme loads.
223
+
224
+ ### Synchronous Backends (`tempid.backends`)
225
+ - `MemoryBackend()`: Stores uses in a thread-safe dict. (Development only).
226
+ - `SQLiteBackend(db_path)`: Uses WAL mode and locks. Great for single-server production.
227
+ - `RedisBackend(uri)`: Uses atomic Lua scripts. Ideal for distributed caching.
228
+ - `MongoBackend(uri, db)`: Uses `find_one_and_update` on unique indexes.
229
+ - `MySQLBackend(host, port, user, password, db)`: Connection pooled, uses `SELECT FOR UPDATE`.
230
+ - `PostgreSQLBackend(dsn)`: Connection pooled, uses `ON CONFLICT DO UPDATE`.
231
+
232
+ ### Asynchronous Backends (`tempid.async_backends`)
233
+ - `AsyncMemoryBackend()`: Thread-safe, asyncio-safe. (Development only).
234
+ - `AsyncSQLiteBackend(db_path)`: Uses `aiosqlite`.
235
+ - `AsyncRedisBackend(uri)`: Uses `redis.asyncio` with Lua scripts.
236
+ - `AsyncMongoBackend(uri, db)`: Uses `motor`.
237
+ - `AsyncMySQLBackend(host, port, user, password, db)`: Uses `aiomysql` pools.
238
+ - `AsyncPostgreSQLBackend(dsn)`: Uses `asyncpg` pools (highest performance).
239
+
240
+ ---
241
+
242
+ ## 🔐 Security & Secrets
243
+
244
+ ### The Secret Key (Required)
245
+ `tempid` uses a 96-bit HMAC-SHA256 signature to prevent tampering and encryption. In production, you **must** set an environment variable to share the secret across your instances.
246
+
247
+ ```bash
248
+ # Set this in your OS, Docker, or .env
249
+ export TEMPID_SECRET="your-super-secret-32-byte-key-here"
250
+ ```
251
+
252
+ To generate a highly secure secret, run:
253
+ ```bash
254
+ python -c "import secrets; print(secrets.token_hex(32))"
255
+ ```
256
+
257
+ ### Encryption Engine
258
+ Payloads and timestamps are encrypted using a custom XOR-CTR stream cipher combined with HMAC-SHA256, ensuring no parts of the internal data can be deciphered without the `TEMPID_SECRET`.
259
+
260
+ ---
261
+
262
+ ## 📖 Complete API Reference
263
+
264
+ ### `TempID` Core Class
265
+
266
+ #### `TempID.new(expires_in: str, payload: dict = None, max_uses: int = 0) -> TempID`
267
+ Creates a new token.
268
+ - `expires_in`: Duration string (e.g. `"30s"`, `"15m"`, `"2h"`, `"7d"`).
269
+ - `payload`: Optional dict. Must be JSON serializable. Max 512 bytes.
270
+ - `max_uses`: Strict use limit. `0` means unlimited.
271
+
272
+ #### `TempID.from_string(value: str) -> TempID`
273
+ Parses a token string but **does not verify** uses limit or signature. Used internally or for manual exception handling.
274
+
275
+ #### `TempID.verify(value: str, check_uses: bool = False) -> TempID | None`
276
+ The primary, safe way to verify a token. Returns the `TempID` object if valid, unexpired, and untampered. If `check_uses=True`, it verifies the backend limit without consuming a use. Returns `None` on any failure.
277
+
278
+ #### `await TempID.verify_async(value: str, check_uses: bool = False) -> TempID | None`
279
+ The async counterpart for `verify()`.
280
+
281
+ #### `tid.use() -> bool`
282
+ Attempts to consume 1 use in the database. Returns `True` if successful, `False` if the limit is reached or the token is expired.
283
+
284
+ #### `await tid.use_async() -> bool`
285
+ The async counterpart for `use()`.
286
+
287
+ #### `tid.uses_info() -> dict`
288
+ Returns `{"total": max_uses, "used": count, "left": remaining}`.
289
+
290
+ #### `await tid.uses_info_async() -> dict`
291
+ The async counterpart for `uses_info()`.
292
+
293
+ #### `tid.valid() -> bool`
294
+ Returns `True` if the token's timestamp has not expired. (Does NOT check database limits).
295
+
296
+ #### `tid.expired() -> bool`
297
+ Opposite of `valid()`.
298
+
299
+ #### `tid.remaining() -> str`
300
+ Returns a human-readable string of time left (e.g., `"1h 4m 3s"`, `"expired"`).
301
+
302
+ #### `tid.on_expire(callback: Callable) -> TempID`
303
+ Registers a function to be called exactly once when the token is first determined to be expired by `valid()`.
304
+
305
+ ---
306
+
307
+ ## ⚠️ Exceptions Reference
308
+
309
+ Located in `tempid.exceptions`.
310
+
311
+ | Exception | Reason Thrown |
312
+ |-----------|---------------|
313
+ | `TempIDFormatError` | The token string is malformed or invalid base32. |
314
+ | `TempIDTamperedError` | The HMAC signature does not match (someone tried to forge or alter it). |
315
+ | `TempIDExpiredError` | The token's timestamp has passed. |
316
+ | `TempIDPayloadTooLargeError`| Passed payload exceeds 512 compressed bytes. |
317
+ | `TempIDRevokedError` | (Reserved for future manual revocation feature). |
318
+
319
+ **Example of manual handling:**
320
+ ```python
321
+ from tempid import TempID
322
+ from tempid.exceptions import TempIDFormatError, TempIDTamperedError
323
+
324
+ try:
325
+ tid = TempID.from_string(user_input)
326
+ if tid.valid():
327
+ print(tid.payload)
328
+ except TempIDTamperedError:
329
+ print("Someone tried to forge this token!")
330
+ except TempIDFormatError:
331
+ print("Malformed token.")
332
+ ```
333
+
334
+ ---
335
+
336
+ ## 📝 Real World Examples
337
+
338
+ ### Example 1: Password Reset (Stateless + Payload)
339
+ ```python
340
+ # User clicks "Forgot Password"
341
+ tid = TempID.new("15m", payload={"email": user.email})
342
+ send_email(user.email, f"https://myapp.com/reset?t={tid.value}")
343
+
344
+ # When user clicks the link
345
+ verified = TempID.verify(request.args["t"])
346
+ if verified:
347
+ reset_password(verified.payload["email"], new_password)
348
+ ```
349
+
350
+ ### Example 2: One-Time-Password (OTP / Burn-After-Reading)
351
+ ```python
352
+ configure(store=RedisBackend("redis://localhost"))
353
+
354
+ # Create 1-time use OTP
355
+ otp = TempID.new("5m", max_uses=1)
356
+ send_sms(user.phone, otp.value)
357
+
358
+ # Verify
359
+ verified = TempID.verify(user_input, check_uses=True)
360
+ if verified and verified.use():
361
+ login_user()
362
+ else:
363
+ print("Invalid or already used OTP!")
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 🤝 Backward Compatibility
369
+ `tempid` is fully backward compatible with legacy v1 tokens (`XXXX-XXXX-XXXX`). Passing a v1 token to `TempID.from_string()` will parse it seamlessly.
370
+
371
+ ## 📄 License
372
+ MIT © 2026 Rahul Vachhani
tempid-2.0.0/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # tempid
2
+
3
+ > Unique IDs that automatically expire, store encrypted payloads, and strictly limit usages — built for Enterprise Python.
4
+
5
+ `tempid` gives you Stripe-like, highly secure temporary tokens (`TEMP-V2.XXXX...`) without the boilerplate.
6
+
7
+ `tempid` operates as a full **Hybrid Token Engine**. It supports embedded JSON payloads, Strict Use-Count Limits (e.g. "burn after reading"), and high-concurrency Async/Sync database backends.
8
+
9
+ ---
10
+
11
+ ## ⚡ Features at a Glance
12
+ - **Stateless by Default:** Tokens hold their own expiration time. No database required for basic time-based expiry.
13
+ - **Encrypted Payloads:** Embed JSON data directly inside the token (up to 512 bytes). Fully encrypted, users cannot read or tamper with it.
14
+ - **Strict Use Limits:** Set a `max_uses` limit on tokens (e.g., a one-time-use OTP).
15
+ - **Enterprise DB Backends:** Built-in connection pooling for Redis, PostgreSQL, MySQL, SQLite, and MongoDB.
16
+ - **Hybrid Async Engine:** First-class `async/await` support for FastAPI, Sanic, and Starlette via dedicated `AsyncBackends`.
17
+ - **Zero Dependencies (Core):** The core engine uses only standard Python libraries.
18
+
19
+ ---
20
+
21
+ ## 📦 Installation
22
+
23
+ ```bash
24
+ pip install tempid
25
+ ```
26
+
27
+ If you plan to use database backends for strict usage limits (`max_uses`), install the appropriate driver:
28
+ ```bash
29
+ pip install tempid[redis] # For RedisBackend
30
+ pip install tempid[mysql] # For MySQLBackend
31
+ pip install tempid[postgres] # For PostgreSQLBackend
32
+ pip install tempid[mongo] # For MongoBackend
33
+ pip install tempid[async-postgres] # For AsyncPostgreSQLBackend
34
+ pip install tempid[async-mysql] # For AsyncMySQLBackend
35
+ pip install tempid[async-mongo] # For AsyncMongoBackend
36
+ pip install tempid[all] # Install all drivers
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 🚀 Quick Start (Stateless Mode)
42
+
43
+ By default, `tempid` operates completely offline without needing a database. Expiration is cryptographically signed into the token itself.
44
+
45
+ ```python
46
+ from tempid import TempID
47
+
48
+ # 1. Create a token that expires in 15 minutes
49
+ tid = TempID.new("15m")
50
+ print(tid.value) # TEMP-V2.AIAGU-PSOHJ...
51
+
52
+ # 2. Check time remaining
53
+ print(tid.remaining()) # "14m 59s"
54
+
55
+ # 3. Verify securely (e.g., when a user submits it)
56
+ # verify() returns the TempID object if valid, or None if expired/tampered
57
+ verified = TempID.verify(tid.value)
58
+ if verified:
59
+ print("Token is valid!")
60
+ else:
61
+ print("Token is invalid or expired.")
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 🧳 Encrypted Payloads
67
+
68
+ You can embed JSON-serializable dictionaries directly into the token. The data is heavily compressed (zlib) and **fully encrypted** (AES-like CTR mode). Users cannot read or tamper with it.
69
+
70
+ ```python
71
+ # Create a token with a payload
72
+ tid = TempID.new("2h", payload={"user_id": 42, "role": "admin"})
73
+
74
+ # Later, verify and extract the data
75
+ verified_token = TempID.verify(tid.value)
76
+
77
+ if verified_token:
78
+ print(verified_token.payload["user_id"]) # 42
79
+ print(verified_token.payload["role"]) # "admin"
80
+ ```
81
+ *Note: Maximum payload size after compression is 512 bytes. If exceeded, `TempIDPayloadTooLargeError` is raised.*
82
+
83
+ ---
84
+
85
+ ## 🛡️ Strict Use Limits (`max_uses`)
86
+
87
+ Need a token that can only be used exactly 3 times, or a password reset link that burns after 1 use? You can set `max_uses`.
88
+
89
+ To use this feature, you **must** configure a database backend at application startup so `tempid` can track the usage atomically across your servers.
90
+
91
+ ### 1. Configure a Backend (Startup)
92
+ ```python
93
+ from tempid import configure
94
+ from tempid.backends import RedisBackend
95
+
96
+ # Run this ONCE when your app starts
97
+ configure(store=RedisBackend("redis://localhost:6379/0"))
98
+ ```
99
+
100
+ ### 2. Generate a Limited Token
101
+ ```python
102
+ # Expires in 1 hour, OR after 1 successful use
103
+ tid = TempID.new("1h", max_uses=1)
104
+ ```
105
+
106
+ ### 3. Consume the Token
107
+ ```python
108
+ # check_uses=True checks the database limit WITHOUT consuming a use
109
+ verified = TempID.verify(token_str, check_uses=True)
110
+
111
+ if verified:
112
+ # use() attempts to consume 1 use atomically in the database
113
+ if verified.use():
114
+ print("Success! Action performed.")
115
+ else:
116
+ print("Token limit reached (Already used!).")
117
+ ```
118
+
119
+ ### 4. Check Remaining Uses
120
+ ```python
121
+ info = verified.uses_info()
122
+ print(f"Used: {info['used']} / Total: {info['total']}")
123
+ ```
124
+
125
+ ---
126
+
127
+ ## ⚡ Async Support (FastAPI / Starlette)
128
+
129
+ `tempid` is fully async-native. If you are building high-concurrency apps, use the `Async` backends and methods to prevent blocking your event loop.
130
+
131
+ ```python
132
+ import asyncio
133
+ from tempid import TempID, configure
134
+ from tempid.async_backends import AsyncPostgreSQLBackend
135
+
136
+ # 1. Configure the Async Backend
137
+ configure(store=AsyncPostgreSQLBackend("postgresql://root:pass@localhost/mydb"))
138
+
139
+ async def api_endpoint(token_string: str):
140
+ # 2. Verify (Checks DB asynchronously without blocking)
141
+ tid = await TempID.verify_async(token_string, check_uses=True)
142
+
143
+ if not tid:
144
+ return {"error": "Invalid or exhausted token"}
145
+
146
+ # 3. Consume a use
147
+ success = await tid.use_async()
148
+ if success:
149
+ return {"data": tid.payload}
150
+ else:
151
+ return {"error": "Token already used!"}
152
+
153
+ # Check info
154
+ info = await tid.uses_info_async()
155
+ print(info)
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 🏭 Supported Backends Reference
161
+
162
+ All backends guarantee **perfect atomicity** (no race conditions or double-spending) even under extreme loads.
163
+
164
+ ### Synchronous Backends (`tempid.backends`)
165
+ - `MemoryBackend()`: Stores uses in a thread-safe dict. (Development only).
166
+ - `SQLiteBackend(db_path)`: Uses WAL mode and locks. Great for single-server production.
167
+ - `RedisBackend(uri)`: Uses atomic Lua scripts. Ideal for distributed caching.
168
+ - `MongoBackend(uri, db)`: Uses `find_one_and_update` on unique indexes.
169
+ - `MySQLBackend(host, port, user, password, db)`: Connection pooled, uses `SELECT FOR UPDATE`.
170
+ - `PostgreSQLBackend(dsn)`: Connection pooled, uses `ON CONFLICT DO UPDATE`.
171
+
172
+ ### Asynchronous Backends (`tempid.async_backends`)
173
+ - `AsyncMemoryBackend()`: Thread-safe, asyncio-safe. (Development only).
174
+ - `AsyncSQLiteBackend(db_path)`: Uses `aiosqlite`.
175
+ - `AsyncRedisBackend(uri)`: Uses `redis.asyncio` with Lua scripts.
176
+ - `AsyncMongoBackend(uri, db)`: Uses `motor`.
177
+ - `AsyncMySQLBackend(host, port, user, password, db)`: Uses `aiomysql` pools.
178
+ - `AsyncPostgreSQLBackend(dsn)`: Uses `asyncpg` pools (highest performance).
179
+
180
+ ---
181
+
182
+ ## 🔐 Security & Secrets
183
+
184
+ ### The Secret Key (Required)
185
+ `tempid` uses a 96-bit HMAC-SHA256 signature to prevent tampering and encryption. In production, you **must** set an environment variable to share the secret across your instances.
186
+
187
+ ```bash
188
+ # Set this in your OS, Docker, or .env
189
+ export TEMPID_SECRET="your-super-secret-32-byte-key-here"
190
+ ```
191
+
192
+ To generate a highly secure secret, run:
193
+ ```bash
194
+ python -c "import secrets; print(secrets.token_hex(32))"
195
+ ```
196
+
197
+ ### Encryption Engine
198
+ Payloads and timestamps are encrypted using a custom XOR-CTR stream cipher combined with HMAC-SHA256, ensuring no parts of the internal data can be deciphered without the `TEMPID_SECRET`.
199
+
200
+ ---
201
+
202
+ ## 📖 Complete API Reference
203
+
204
+ ### `TempID` Core Class
205
+
206
+ #### `TempID.new(expires_in: str, payload: dict = None, max_uses: int = 0) -> TempID`
207
+ Creates a new token.
208
+ - `expires_in`: Duration string (e.g. `"30s"`, `"15m"`, `"2h"`, `"7d"`).
209
+ - `payload`: Optional dict. Must be JSON serializable. Max 512 bytes.
210
+ - `max_uses`: Strict use limit. `0` means unlimited.
211
+
212
+ #### `TempID.from_string(value: str) -> TempID`
213
+ Parses a token string but **does not verify** uses limit or signature. Used internally or for manual exception handling.
214
+
215
+ #### `TempID.verify(value: str, check_uses: bool = False) -> TempID | None`
216
+ The primary, safe way to verify a token. Returns the `TempID` object if valid, unexpired, and untampered. If `check_uses=True`, it verifies the backend limit without consuming a use. Returns `None` on any failure.
217
+
218
+ #### `await TempID.verify_async(value: str, check_uses: bool = False) -> TempID | None`
219
+ The async counterpart for `verify()`.
220
+
221
+ #### `tid.use() -> bool`
222
+ Attempts to consume 1 use in the database. Returns `True` if successful, `False` if the limit is reached or the token is expired.
223
+
224
+ #### `await tid.use_async() -> bool`
225
+ The async counterpart for `use()`.
226
+
227
+ #### `tid.uses_info() -> dict`
228
+ Returns `{"total": max_uses, "used": count, "left": remaining}`.
229
+
230
+ #### `await tid.uses_info_async() -> dict`
231
+ The async counterpart for `uses_info()`.
232
+
233
+ #### `tid.valid() -> bool`
234
+ Returns `True` if the token's timestamp has not expired. (Does NOT check database limits).
235
+
236
+ #### `tid.expired() -> bool`
237
+ Opposite of `valid()`.
238
+
239
+ #### `tid.remaining() -> str`
240
+ Returns a human-readable string of time left (e.g., `"1h 4m 3s"`, `"expired"`).
241
+
242
+ #### `tid.on_expire(callback: Callable) -> TempID`
243
+ Registers a function to be called exactly once when the token is first determined to be expired by `valid()`.
244
+
245
+ ---
246
+
247
+ ## ⚠️ Exceptions Reference
248
+
249
+ Located in `tempid.exceptions`.
250
+
251
+ | Exception | Reason Thrown |
252
+ |-----------|---------------|
253
+ | `TempIDFormatError` | The token string is malformed or invalid base32. |
254
+ | `TempIDTamperedError` | The HMAC signature does not match (someone tried to forge or alter it). |
255
+ | `TempIDExpiredError` | The token's timestamp has passed. |
256
+ | `TempIDPayloadTooLargeError`| Passed payload exceeds 512 compressed bytes. |
257
+ | `TempIDRevokedError` | (Reserved for future manual revocation feature). |
258
+
259
+ **Example of manual handling:**
260
+ ```python
261
+ from tempid import TempID
262
+ from tempid.exceptions import TempIDFormatError, TempIDTamperedError
263
+
264
+ try:
265
+ tid = TempID.from_string(user_input)
266
+ if tid.valid():
267
+ print(tid.payload)
268
+ except TempIDTamperedError:
269
+ print("Someone tried to forge this token!")
270
+ except TempIDFormatError:
271
+ print("Malformed token.")
272
+ ```
273
+
274
+ ---
275
+
276
+ ## 📝 Real World Examples
277
+
278
+ ### Example 1: Password Reset (Stateless + Payload)
279
+ ```python
280
+ # User clicks "Forgot Password"
281
+ tid = TempID.new("15m", payload={"email": user.email})
282
+ send_email(user.email, f"https://myapp.com/reset?t={tid.value}")
283
+
284
+ # When user clicks the link
285
+ verified = TempID.verify(request.args["t"])
286
+ if verified:
287
+ reset_password(verified.payload["email"], new_password)
288
+ ```
289
+
290
+ ### Example 2: One-Time-Password (OTP / Burn-After-Reading)
291
+ ```python
292
+ configure(store=RedisBackend("redis://localhost"))
293
+
294
+ # Create 1-time use OTP
295
+ otp = TempID.new("5m", max_uses=1)
296
+ send_sms(user.phone, otp.value)
297
+
298
+ # Verify
299
+ verified = TempID.verify(user_input, check_uses=True)
300
+ if verified and verified.use():
301
+ login_user()
302
+ else:
303
+ print("Invalid or already used OTP!")
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 🤝 Backward Compatibility
309
+ `tempid` is fully backward compatible with legacy v1 tokens (`XXXX-XXXX-XXXX`). Passing a v1 token to `TempID.from_string()` will parse it seamlessly.
310
+
311
+ ## 📄 License
312
+ MIT © 2026 Rahul Vachhani