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 +21 -0
- tempid-2.0.0/PKG-INFO +372 -0
- tempid-2.0.0/README.md +312 -0
- tempid-2.0.0/pyproject.toml +70 -0
- tempid-2.0.0/setup.cfg +4 -0
- tempid-2.0.0/tempid/__init__.py +54 -0
- tempid-2.0.0/tempid/async_backends.py +428 -0
- tempid-2.0.0/tempid/backends.py +449 -0
- tempid-2.0.0/tempid/core.py +725 -0
- tempid-2.0.0/tempid/exceptions.py +27 -0
- tempid-2.0.0/tempid/py.typed +0 -0
- tempid-2.0.0/tempid.egg-info/PKG-INFO +372 -0
- tempid-2.0.0/tempid.egg-info/SOURCES.txt +19 -0
- tempid-2.0.0/tempid.egg-info/dependency_links.txt +1 -0
- tempid-2.0.0/tempid.egg-info/requires.txt +46 -0
- tempid-2.0.0/tempid.egg-info/top_level.txt +1 -0
- tempid-2.0.0/tests/test_async_backends.py +142 -0
- tempid-2.0.0/tests/test_backends.py +131 -0
- tempid-2.0.0/tests/test_core_engine.py +244 -0
- tempid-2.0.0/tests/test_payloads.py +127 -0
- tempid-2.0.0/tests/test_security.py +144 -0
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
|