tempid 2.0.0__py3-none-any.whl
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/__init__.py +54 -0
- tempid/async_backends.py +428 -0
- tempid/backends.py +449 -0
- tempid/core.py +725 -0
- tempid/exceptions.py +27 -0
- tempid/py.typed +0 -0
- tempid-2.0.0.dist-info/METADATA +372 -0
- tempid-2.0.0.dist-info/RECORD +11 -0
- tempid-2.0.0.dist-info/WHEEL +5 -0
- tempid-2.0.0.dist-info/licenses/LICENSE +21 -0
- tempid-2.0.0.dist-info/top_level.txt +1 -0
tempid/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from .core import TempID, configure, teardown, teardown_async
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
TempIDError,
|
|
4
|
+
TempIDExpiredError,
|
|
5
|
+
TempIDFormatError,
|
|
6
|
+
TempIDPayloadTooLargeError,
|
|
7
|
+
TempIDRevokedError,
|
|
8
|
+
TempIDTamperedError,
|
|
9
|
+
)
|
|
10
|
+
from .backends import (
|
|
11
|
+
MemoryBackend,
|
|
12
|
+
SQLiteBackend,
|
|
13
|
+
RedisBackend,
|
|
14
|
+
MongoBackend,
|
|
15
|
+
MySQLBackend,
|
|
16
|
+
PostgreSQLBackend,
|
|
17
|
+
)
|
|
18
|
+
from .async_backends import (
|
|
19
|
+
AsyncMemoryBackend,
|
|
20
|
+
AsyncSQLiteBackend,
|
|
21
|
+
AsyncRedisBackend,
|
|
22
|
+
AsyncMongoBackend,
|
|
23
|
+
AsyncPostgreSQLBackend,
|
|
24
|
+
AsyncMySQLBackend,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"TempID",
|
|
29
|
+
"configure",
|
|
30
|
+
"teardown",
|
|
31
|
+
"teardown_async",
|
|
32
|
+
# exceptions
|
|
33
|
+
"TempIDError",
|
|
34
|
+
"TempIDExpiredError",
|
|
35
|
+
"TempIDFormatError",
|
|
36
|
+
"TempIDPayloadTooLargeError",
|
|
37
|
+
"TempIDRevokedError",
|
|
38
|
+
"TempIDTamperedError",
|
|
39
|
+
# backends
|
|
40
|
+
"MemoryBackend",
|
|
41
|
+
"SQLiteBackend",
|
|
42
|
+
"RedisBackend",
|
|
43
|
+
"MongoBackend",
|
|
44
|
+
"MySQLBackend",
|
|
45
|
+
"PostgreSQLBackend",
|
|
46
|
+
"AsyncMemoryBackend",
|
|
47
|
+
"AsyncSQLiteBackend",
|
|
48
|
+
"AsyncRedisBackend",
|
|
49
|
+
"AsyncMongoBackend",
|
|
50
|
+
"AsyncPostgreSQLBackend",
|
|
51
|
+
"AsyncMySQLBackend",
|
|
52
|
+
]
|
|
53
|
+
__version__ = "2.0.0"
|
|
54
|
+
__author__ = "Rahul Patel"
|
tempid/async_backends.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tempid.async_backends — Pluggable async use-count backends for max_uses tokens.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from tempid import configure
|
|
7
|
+
from tempid.async_backends import AsyncRedisBackend
|
|
8
|
+
|
|
9
|
+
configure(store=AsyncRedisBackend("redis://localhost:6379"))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .backends import _LUA_INCR
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncMemoryBackend:
|
|
22
|
+
"""
|
|
23
|
+
In-process memory backend (Async). Zero dependencies.
|
|
24
|
+
Suitable for single-worker ASGI deployments (FastAPI dev server).
|
|
25
|
+
WARNING: Not for multi-worker production.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._lock: asyncio.Lock | None = None
|
|
30
|
+
self._store: dict[str, int] = {}
|
|
31
|
+
self._expiry: dict[str, int] = {}
|
|
32
|
+
|
|
33
|
+
def _get_lock(self) -> asyncio.Lock:
|
|
34
|
+
if self._lock is None:
|
|
35
|
+
self._lock = asyncio.Lock()
|
|
36
|
+
return self._lock
|
|
37
|
+
|
|
38
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
39
|
+
async with self._get_lock():
|
|
40
|
+
self._gc()
|
|
41
|
+
current = self._store.get(token_id, 0)
|
|
42
|
+
if current >= max_uses:
|
|
43
|
+
return False
|
|
44
|
+
self._store[token_id] = current + 1
|
|
45
|
+
if expires_at:
|
|
46
|
+
self._expiry[token_id] = expires_at
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
async def use_count(self, token_id: str) -> int:
|
|
50
|
+
async with self._get_lock():
|
|
51
|
+
return self._store.get(token_id, 0)
|
|
52
|
+
|
|
53
|
+
def _gc(self) -> None:
|
|
54
|
+
"""Lazy garbage collection — remove expired token entries."""
|
|
55
|
+
now = int(time.time())
|
|
56
|
+
expired = [tid for tid, exp in self._expiry.items() if exp < now]
|
|
57
|
+
for tid in expired:
|
|
58
|
+
self._store.pop(tid, None)
|
|
59
|
+
self._expiry.pop(tid, None)
|
|
60
|
+
|
|
61
|
+
async def aclose(self) -> None:
|
|
62
|
+
"""No-op for AsyncMemoryBackend."""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncSQLiteBackend:
|
|
67
|
+
"""
|
|
68
|
+
SQLite backend (Async). Persists use counts across server restarts.
|
|
69
|
+
Requires: ``pip install tempid[async-sqlite]`` (installs aiosqlite).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, path: str = "tempid_uses.db") -> None:
|
|
73
|
+
try:
|
|
74
|
+
import aiosqlite # type: ignore # noqa: F401
|
|
75
|
+
except ImportError:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"AsyncSQLiteBackend requires aiosqlite. "
|
|
78
|
+
"Install with: pip install tempid[async-sqlite]"
|
|
79
|
+
)
|
|
80
|
+
self._path = path
|
|
81
|
+
self._lock: asyncio.Lock | None = None
|
|
82
|
+
self._pool_created = False
|
|
83
|
+
self._conn: Any = None
|
|
84
|
+
|
|
85
|
+
def _get_lock(self) -> asyncio.Lock:
|
|
86
|
+
if self._lock is None:
|
|
87
|
+
self._lock = asyncio.Lock()
|
|
88
|
+
return self._lock
|
|
89
|
+
|
|
90
|
+
async def _setup(self) -> None:
|
|
91
|
+
if self._pool_created:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
import aiosqlite
|
|
95
|
+
try:
|
|
96
|
+
self._conn = await aiosqlite.connect(self._path, timeout=10)
|
|
97
|
+
await self._conn.execute("PRAGMA journal_mode=WAL")
|
|
98
|
+
await self._conn.execute("""
|
|
99
|
+
CREATE TABLE IF NOT EXISTS tempid_uses (
|
|
100
|
+
token_id TEXT PRIMARY KEY,
|
|
101
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
expires_at INTEGER NOT NULL DEFAULT 0
|
|
103
|
+
)
|
|
104
|
+
""")
|
|
105
|
+
await self._conn.commit()
|
|
106
|
+
self._pool_created = True
|
|
107
|
+
except Exception:
|
|
108
|
+
if self._conn:
|
|
109
|
+
await self._conn.close()
|
|
110
|
+
self._conn = None
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
114
|
+
async with self._get_lock():
|
|
115
|
+
await self._setup()
|
|
116
|
+
await self._cleanup()
|
|
117
|
+
|
|
118
|
+
async with self._conn.execute(
|
|
119
|
+
"SELECT count FROM tempid_uses WHERE token_id = ?", (token_id,)
|
|
120
|
+
) as cursor:
|
|
121
|
+
row = await cursor.fetchone()
|
|
122
|
+
|
|
123
|
+
current = row[0] if row else 0
|
|
124
|
+
if current >= max_uses:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
await self._conn.execute(
|
|
128
|
+
"INSERT INTO tempid_uses (token_id, count, expires_at) VALUES (?, 1, ?) "
|
|
129
|
+
"ON CONFLICT(token_id) DO UPDATE SET count = count + 1",
|
|
130
|
+
(token_id, expires_at),
|
|
131
|
+
)
|
|
132
|
+
await self._conn.commit()
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
async def use_count(self, token_id: str) -> int:
|
|
136
|
+
async with self._get_lock():
|
|
137
|
+
await self._setup()
|
|
138
|
+
async with self._conn.execute(
|
|
139
|
+
"SELECT count FROM tempid_uses WHERE token_id = ?", (token_id,)
|
|
140
|
+
) as cursor:
|
|
141
|
+
row = await cursor.fetchone()
|
|
142
|
+
return row[0] if row else 0
|
|
143
|
+
|
|
144
|
+
async def aclose(self) -> None:
|
|
145
|
+
if self._conn:
|
|
146
|
+
await self._conn.close()
|
|
147
|
+
self._conn = None
|
|
148
|
+
|
|
149
|
+
async def _cleanup(self) -> None:
|
|
150
|
+
await self._conn.execute(
|
|
151
|
+
"DELETE FROM tempid_uses WHERE expires_at > 0 AND expires_at < ?",
|
|
152
|
+
(int(time.time()),),
|
|
153
|
+
)
|
|
154
|
+
await self._conn.commit()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AsyncRedisBackend:
|
|
158
|
+
"""
|
|
159
|
+
Redis backend (Async). Safe for distributed, multi-worker deployments.
|
|
160
|
+
Requires: ``pip install tempid[async-redis]``
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, uri: str = "redis://localhost:6379/0", prefix: str = "tempid:") -> None:
|
|
164
|
+
try:
|
|
165
|
+
import redis.asyncio as redis # type: ignore
|
|
166
|
+
except ImportError:
|
|
167
|
+
raise ImportError(
|
|
168
|
+
"AsyncRedisBackend requires redis-py. "
|
|
169
|
+
"Install with: pip install tempid[async-redis]"
|
|
170
|
+
)
|
|
171
|
+
self._pool = redis.from_url(uri, decode_responses=True)
|
|
172
|
+
self.prefix = prefix
|
|
173
|
+
# Pre-register the script hash for performance
|
|
174
|
+
self._incr_script = self._pool.register_script(_LUA_INCR)
|
|
175
|
+
|
|
176
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
177
|
+
key = f"{self.prefix}{token_id}"
|
|
178
|
+
# Lua script handles both INCR and TTL atomically to avoid race conditions
|
|
179
|
+
count = await self._incr_script(keys=[key], args=[max_uses, expires_at])
|
|
180
|
+
|
|
181
|
+
# Redis Lua script returns 0 if limit exceeded
|
|
182
|
+
if count == 0:
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
async def use_count(self, token_id: str) -> int:
|
|
188
|
+
key = f"{self.prefix}{token_id}"
|
|
189
|
+
val = await self._pool.get(key)
|
|
190
|
+
return int(val) if val else 0
|
|
191
|
+
|
|
192
|
+
async def aclose(self) -> None:
|
|
193
|
+
if self._pool:
|
|
194
|
+
await self._pool.aclose()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AsyncMongoBackend:
|
|
198
|
+
"""
|
|
199
|
+
MongoDB backend (Async). Safe for distributed, multi-worker deployments.
|
|
200
|
+
Uses ``find_one_and_update`` for atomic document-level operations.
|
|
201
|
+
Requires: ``pip install tempid[async-mongo]``
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, uri: str = "mongodb://localhost:27017", db: str = "tempid") -> None:
|
|
205
|
+
try:
|
|
206
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
207
|
+
except ImportError:
|
|
208
|
+
raise ImportError(
|
|
209
|
+
"AsyncMongoBackend requires motor. "
|
|
210
|
+
"Install with: pip install tempid[async-mongo]"
|
|
211
|
+
)
|
|
212
|
+
self._client: Any = AsyncIOMotorClient(uri)
|
|
213
|
+
self._col: Any = self._client[db]["uses"]
|
|
214
|
+
self._setup_lock = asyncio.Lock()
|
|
215
|
+
self._setup_done = False
|
|
216
|
+
|
|
217
|
+
async def _setup(self) -> None:
|
|
218
|
+
if self._setup_done:
|
|
219
|
+
return
|
|
220
|
+
async with self._setup_lock:
|
|
221
|
+
if self._setup_done:
|
|
222
|
+
return
|
|
223
|
+
await self._col.create_index("token_id", unique=True)
|
|
224
|
+
await self._col.create_index("expires_at", expireAfterSeconds=0, sparse=True)
|
|
225
|
+
self._setup_done = True
|
|
226
|
+
|
|
227
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
228
|
+
from pymongo.errors import DuplicateKeyError
|
|
229
|
+
await self._setup()
|
|
230
|
+
try:
|
|
231
|
+
await self._col.insert_one({"token_id": token_id, "count": 1, "expires_at": expires_at})
|
|
232
|
+
return True
|
|
233
|
+
except DuplicateKeyError:
|
|
234
|
+
result = await self._col.find_one_and_update(
|
|
235
|
+
{"token_id": token_id, "count": {"$lt": max_uses}},
|
|
236
|
+
{"$inc": {"count": 1}},
|
|
237
|
+
return_document=True,
|
|
238
|
+
)
|
|
239
|
+
return result is not None
|
|
240
|
+
|
|
241
|
+
async def use_count(self, token_id: str) -> int:
|
|
242
|
+
await self._setup()
|
|
243
|
+
doc = await self._col.find_one({"token_id": token_id})
|
|
244
|
+
return doc["count"] if doc else 0
|
|
245
|
+
|
|
246
|
+
async def aclose(self) -> None:
|
|
247
|
+
"""Motor closes the client automatically on event loop teardown, but can be closed explicitly."""
|
|
248
|
+
if self._client:
|
|
249
|
+
self._client.close()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class AsyncPostgreSQLBackend:
|
|
253
|
+
"""
|
|
254
|
+
PostgreSQL backend (Async). Uses asyncpg.
|
|
255
|
+
Requires: ``pip install tempid[async-postgres]``
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
dsn: str = "postgresql://localhost/tempid",
|
|
261
|
+
min_conn: int = 2,
|
|
262
|
+
max_conn: int = 10,
|
|
263
|
+
) -> None:
|
|
264
|
+
try:
|
|
265
|
+
import asyncpg # type: ignore # noqa: F401
|
|
266
|
+
except ImportError:
|
|
267
|
+
raise ImportError(
|
|
268
|
+
"AsyncPostgreSQLBackend requires asyncpg. "
|
|
269
|
+
"Install with: pip install tempid[async-postgres]"
|
|
270
|
+
)
|
|
271
|
+
self.dsn = dsn
|
|
272
|
+
self.min_conn = min_conn
|
|
273
|
+
self.max_conn = max_conn
|
|
274
|
+
self._pool = None
|
|
275
|
+
self._setup_lock = asyncio.Lock()
|
|
276
|
+
|
|
277
|
+
async def _get_pool(self):
|
|
278
|
+
import asyncpg
|
|
279
|
+
if self._pool is None:
|
|
280
|
+
async with self._setup_lock:
|
|
281
|
+
if self._pool is None:
|
|
282
|
+
# 1. Create pool in local variable
|
|
283
|
+
pool = await asyncpg.create_pool(
|
|
284
|
+
self.dsn, min_size=self.min_conn, max_size=self.max_conn
|
|
285
|
+
)
|
|
286
|
+
# 2. Setup tables using the local pool
|
|
287
|
+
async with pool.acquire() as conn:
|
|
288
|
+
await conn.execute("""
|
|
289
|
+
CREATE TABLE IF NOT EXISTS tempid_uses (
|
|
290
|
+
token_id TEXT PRIMARY KEY,
|
|
291
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
292
|
+
expires_at INTEGER NOT NULL DEFAULT 0
|
|
293
|
+
)
|
|
294
|
+
""")
|
|
295
|
+
# 3. Assign self._pool LAST to prevent race condition leaks
|
|
296
|
+
self._pool = pool
|
|
297
|
+
return self._pool
|
|
298
|
+
|
|
299
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
300
|
+
pool = await self._get_pool()
|
|
301
|
+
async with pool.acquire() as conn:
|
|
302
|
+
async with conn.transaction():
|
|
303
|
+
row = await conn.fetchrow(
|
|
304
|
+
"""
|
|
305
|
+
INSERT INTO tempid_uses (token_id, count, expires_at)
|
|
306
|
+
VALUES ($1, 1, $2)
|
|
307
|
+
ON CONFLICT (token_id) DO UPDATE
|
|
308
|
+
SET count = tempid_uses.count + 1
|
|
309
|
+
WHERE tempid_uses.count < $3
|
|
310
|
+
RETURNING count
|
|
311
|
+
""",
|
|
312
|
+
token_id, expires_at, max_uses
|
|
313
|
+
)
|
|
314
|
+
return row is not None
|
|
315
|
+
|
|
316
|
+
async def use_count(self, token_id: str) -> int:
|
|
317
|
+
pool = await self._get_pool()
|
|
318
|
+
async with pool.acquire() as conn:
|
|
319
|
+
row = await conn.fetchrow("SELECT count FROM tempid_uses WHERE token_id = $1", token_id)
|
|
320
|
+
return row["count"] if row else 0
|
|
321
|
+
|
|
322
|
+
async def aclose(self) -> None:
|
|
323
|
+
if self._pool:
|
|
324
|
+
await self._pool.close()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class AsyncMySQLBackend:
|
|
328
|
+
"""
|
|
329
|
+
MySQL backend (Async). Uses aiomysql.
|
|
330
|
+
Requires: ``pip install tempid[async-mysql]``
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(
|
|
334
|
+
self,
|
|
335
|
+
host: str = "localhost",
|
|
336
|
+
port: int = 3306,
|
|
337
|
+
user: str = "root",
|
|
338
|
+
password: str = "",
|
|
339
|
+
db: str = "tempid",
|
|
340
|
+
max_connections: int = 10,
|
|
341
|
+
) -> None:
|
|
342
|
+
try:
|
|
343
|
+
import aiomysql # type: ignore # noqa: F401
|
|
344
|
+
except ImportError:
|
|
345
|
+
raise ImportError(
|
|
346
|
+
"AsyncMySQLBackend requires aiomysql. "
|
|
347
|
+
"Install with: pip install tempid[async-mysql]"
|
|
348
|
+
)
|
|
349
|
+
self.host = host
|
|
350
|
+
self.port = port
|
|
351
|
+
self.user = user
|
|
352
|
+
self.password = password
|
|
353
|
+
self.db = db
|
|
354
|
+
self.max_connections = max_connections
|
|
355
|
+
self._pool = None
|
|
356
|
+
self._setup_lock = asyncio.Lock()
|
|
357
|
+
|
|
358
|
+
async def _get_pool(self):
|
|
359
|
+
import aiomysql
|
|
360
|
+
if self._pool is None:
|
|
361
|
+
async with self._setup_lock:
|
|
362
|
+
if self._pool is None:
|
|
363
|
+
# 1. Create pool in local variable with explicit autocommit=False
|
|
364
|
+
pool = await aiomysql.create_pool(
|
|
365
|
+
host=self.host,
|
|
366
|
+
port=self.port,
|
|
367
|
+
user=self.user,
|
|
368
|
+
password=self.password,
|
|
369
|
+
db=self.db,
|
|
370
|
+
minsize=1,
|
|
371
|
+
maxsize=self.max_connections,
|
|
372
|
+
autocommit=False,
|
|
373
|
+
)
|
|
374
|
+
# 2. Setup tables using the local pool
|
|
375
|
+
async with pool.acquire() as conn:
|
|
376
|
+
async with conn.cursor() as cur:
|
|
377
|
+
await cur.execute("""
|
|
378
|
+
CREATE TABLE IF NOT EXISTS tempid_uses (
|
|
379
|
+
token_id VARCHAR(64) PRIMARY KEY,
|
|
380
|
+
count INT NOT NULL DEFAULT 0,
|
|
381
|
+
expires_at INT NOT NULL DEFAULT 0
|
|
382
|
+
)
|
|
383
|
+
""")
|
|
384
|
+
await conn.commit()
|
|
385
|
+
# 3. Assign self._pool LAST to prevent race condition leaks
|
|
386
|
+
self._pool = pool
|
|
387
|
+
return self._pool
|
|
388
|
+
|
|
389
|
+
async def increment_use(self, token_id: str, max_uses: int, expires_at: int = 0) -> bool:
|
|
390
|
+
pool = await self._get_pool()
|
|
391
|
+
async with pool.acquire() as conn:
|
|
392
|
+
try:
|
|
393
|
+
async with conn.cursor() as cur:
|
|
394
|
+
await cur.execute(
|
|
395
|
+
"SELECT count FROM tempid_uses WHERE token_id = %s FOR UPDATE",
|
|
396
|
+
(token_id,)
|
|
397
|
+
)
|
|
398
|
+
row = await cur.fetchone()
|
|
399
|
+
|
|
400
|
+
if row and row[0] >= max_uses:
|
|
401
|
+
await conn.rollback()
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
await cur.execute(
|
|
405
|
+
"INSERT INTO tempid_uses (token_id, count, expires_at) VALUES (%s, 1, %s) "
|
|
406
|
+
"ON DUPLICATE KEY UPDATE count = count + 1",
|
|
407
|
+
(token_id, expires_at),
|
|
408
|
+
)
|
|
409
|
+
await conn.commit()
|
|
410
|
+
return True
|
|
411
|
+
except Exception:
|
|
412
|
+
await conn.rollback()
|
|
413
|
+
raise
|
|
414
|
+
|
|
415
|
+
async def use_count(self, token_id: str) -> int:
|
|
416
|
+
pool = await self._get_pool()
|
|
417
|
+
async with pool.acquire() as conn:
|
|
418
|
+
async with conn.cursor() as cur:
|
|
419
|
+
await cur.execute(
|
|
420
|
+
"SELECT count FROM tempid_uses WHERE token_id = %s", (token_id,)
|
|
421
|
+
)
|
|
422
|
+
row = await cur.fetchone()
|
|
423
|
+
return row[0] if row else 0
|
|
424
|
+
|
|
425
|
+
async def aclose(self) -> None:
|
|
426
|
+
if self._pool:
|
|
427
|
+
self._pool.close()
|
|
428
|
+
await self._pool.wait_closed()
|