nanasqlite 1.3.3.dev4__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.
- nanasqlite/__init__.py +52 -0
- nanasqlite/async_core.py +1456 -0
- nanasqlite/cache.py +335 -0
- nanasqlite/core.py +2336 -0
- nanasqlite/exceptions.py +117 -0
- nanasqlite/py.typed +0 -0
- nanasqlite/sql_utils.py +174 -0
- nanasqlite/utils.py +202 -0
- nanasqlite-1.3.3.dev4.dist-info/METADATA +413 -0
- nanasqlite-1.3.3.dev4.dist-info/RECORD +13 -0
- nanasqlite-1.3.3.dev4.dist-info/WHEEL +5 -0
- nanasqlite-1.3.3.dev4.dist-info/licenses/LICENSE +21 -0
- nanasqlite-1.3.3.dev4.dist-info/top_level.txt +1 -0
nanasqlite/exceptions.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NanaSQLite Exception Classes
|
|
3
|
+
|
|
4
|
+
カスタム例外クラスを定義し、エラーハンドリングを統一する。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NanaSQLiteError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
NanaSQLiteの基底例外クラス
|
|
11
|
+
|
|
12
|
+
すべてのNanaSQLite固有の例外はこのクラスを継承する。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NanaSQLiteValidationError(NanaSQLiteError):
|
|
19
|
+
"""
|
|
20
|
+
バリデーションエラー
|
|
21
|
+
|
|
22
|
+
不正な入力値やパラメータに対して発生する。
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- 不正なテーブル名やカラム名
|
|
26
|
+
- 不正なSQL識別子
|
|
27
|
+
- パラメータの型エラー
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NanaSQLiteDatabaseError(NanaSQLiteError):
|
|
34
|
+
"""
|
|
35
|
+
データベース操作エラー
|
|
36
|
+
|
|
37
|
+
SQLite/APSWのデータベース操作で発生するエラーをラップ。
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
- データベースロック
|
|
41
|
+
- ディスク容量不足
|
|
42
|
+
- ファイル権限エラー
|
|
43
|
+
- SQL構文エラー
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str, original_error: Exception = None):
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
self.original_error = original_error
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NanaSQLiteTransactionError(NanaSQLiteError):
|
|
52
|
+
"""
|
|
53
|
+
トランザクション関連エラー
|
|
54
|
+
|
|
55
|
+
トランザクションの開始、コミット、ロールバックで発生するエラー。
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
- ネストしたトランザクションの試み
|
|
59
|
+
- トランザクション外でのコミット/ロールバック
|
|
60
|
+
- トランザクション中の接続クローズ
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NanaSQLiteConnectionError(NanaSQLiteError):
|
|
67
|
+
"""
|
|
68
|
+
接続エラー
|
|
69
|
+
|
|
70
|
+
データベース接続の作成や管理で発生するエラー。
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
- 閉じられた接続の使用
|
|
74
|
+
- 接続の初期化失敗
|
|
75
|
+
- 孤立した子インスタンスの使用
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NanaSQLiteClosedError(NanaSQLiteConnectionError):
|
|
82
|
+
"""
|
|
83
|
+
Closed instance error / クローズ済みエラー
|
|
84
|
+
|
|
85
|
+
Occurs when operating on a closed instance, or on a child instance whose parent has been closed.
|
|
86
|
+
閉じられたインスタンスや、親が閉じられた子インスタンスを操作しようとした時に発生。
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NanaSQLiteLockError(NanaSQLiteError):
|
|
93
|
+
"""
|
|
94
|
+
ロック取得エラー
|
|
95
|
+
|
|
96
|
+
データベースロックの取得に失敗した場合に発生。
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
- ロック取得タイムアウト
|
|
100
|
+
- デッドロック検出
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NanaSQLiteCacheError(NanaSQLiteError):
|
|
107
|
+
"""
|
|
108
|
+
キャッシュ関連エラー
|
|
109
|
+
|
|
110
|
+
キャッシュの操作で発生するエラー。
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
- キャッシュサイズ超過
|
|
114
|
+
- キャッシュの不整合
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
pass
|
nanasqlite/py.typed
ADDED
|
File without changes
|
nanasqlite/sql_utils.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQL utility functions for NanaSQLite.
|
|
3
|
+
|
|
4
|
+
This module provides utility functions for SQL string processing,
|
|
5
|
+
particularly for sanitizing SQL expressions to prevent injection attacks
|
|
6
|
+
and handle edge cases in SQL parsing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sanitize_sql_for_function_scan(sql: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Return a version of the SQL string where string literals and comments
|
|
13
|
+
are replaced with spaces so that function-like patterns inside them
|
|
14
|
+
are ignored by the validation regex.
|
|
15
|
+
|
|
16
|
+
This function implements a state machine that processes SQL character by character,
|
|
17
|
+
tracking whether we're inside:
|
|
18
|
+
- Single-quoted string literals (with '' escaping)
|
|
19
|
+
- Double-quoted identifiers/strings (with "" escaping)
|
|
20
|
+
- Line comments (-- to newline)
|
|
21
|
+
- Block comments (/* to */)
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
sql: The SQL string to sanitize
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A sanitized version of the SQL string where all content inside
|
|
28
|
+
string literals and comments is replaced with spaces, while
|
|
29
|
+
preserving the original length and newline positions.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> sanitize_sql_for_function_scan("SELECT 'COUNT(*)' FROM table")
|
|
33
|
+
'SELECT FROM table'
|
|
34
|
+
>>> sanitize_sql_for_function_scan("SELECT COUNT(*) -- comment")
|
|
35
|
+
'SELECT COUNT(*) '
|
|
36
|
+
|
|
37
|
+
Note:
|
|
38
|
+
This function handles SQL-specific escaping rules:
|
|
39
|
+
- Single quotes are escaped as ''
|
|
40
|
+
- Double quotes are escaped as ""
|
|
41
|
+
- Line comments start with -- and end at newline
|
|
42
|
+
- Block comments are /* ... */
|
|
43
|
+
"""
|
|
44
|
+
if not sql:
|
|
45
|
+
return sql
|
|
46
|
+
|
|
47
|
+
result = []
|
|
48
|
+
i = 0
|
|
49
|
+
length = len(sql)
|
|
50
|
+
in_single = False
|
|
51
|
+
in_double = False
|
|
52
|
+
in_line_comment = False
|
|
53
|
+
in_block_comment = False
|
|
54
|
+
|
|
55
|
+
while i < length:
|
|
56
|
+
ch = sql[i]
|
|
57
|
+
|
|
58
|
+
# Inside line comment
|
|
59
|
+
if in_line_comment:
|
|
60
|
+
if ch == "\n":
|
|
61
|
+
in_line_comment = False
|
|
62
|
+
result.append(ch)
|
|
63
|
+
else:
|
|
64
|
+
result.append(" ")
|
|
65
|
+
i += 1
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# Inside block comment
|
|
69
|
+
if in_block_comment:
|
|
70
|
+
if ch == "*" and i + 1 < length and sql[i + 1] == "/":
|
|
71
|
+
in_block_comment = False
|
|
72
|
+
result.append(" ") # Replace */
|
|
73
|
+
i += 2
|
|
74
|
+
else:
|
|
75
|
+
result.append(" ")
|
|
76
|
+
i += 1
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Inside single-quoted string literal
|
|
80
|
+
if in_single:
|
|
81
|
+
if ch == "'" and i + 1 < length and sql[i + 1] == "'":
|
|
82
|
+
# Escaped single quote (SQL standard: '')
|
|
83
|
+
result.append(" ")
|
|
84
|
+
i += 2
|
|
85
|
+
elif ch == "'":
|
|
86
|
+
in_single = False
|
|
87
|
+
result.append(" ")
|
|
88
|
+
i += 1
|
|
89
|
+
else:
|
|
90
|
+
result.append(" ")
|
|
91
|
+
i += 1
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Inside double-quoted identifier/string
|
|
95
|
+
if in_double:
|
|
96
|
+
if ch == '"' and i + 1 < length and sql[i + 1] == '"':
|
|
97
|
+
# Escaped double quote (SQL standard: "")
|
|
98
|
+
result.append(" ")
|
|
99
|
+
i += 2
|
|
100
|
+
elif ch == '"':
|
|
101
|
+
in_double = False
|
|
102
|
+
result.append(" ")
|
|
103
|
+
i += 1
|
|
104
|
+
else:
|
|
105
|
+
result.append(" ")
|
|
106
|
+
i += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Outside literals/comments - check for delimiters
|
|
110
|
+
|
|
111
|
+
# Line comment start
|
|
112
|
+
if ch == "-" and i + 1 < length and sql[i + 1] == "-":
|
|
113
|
+
in_line_comment = True
|
|
114
|
+
result.append(" ")
|
|
115
|
+
i += 2
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Block comment start
|
|
119
|
+
if ch == "/" and i + 1 < length and sql[i + 1] == "*":
|
|
120
|
+
in_block_comment = True
|
|
121
|
+
result.append(" ")
|
|
122
|
+
i += 2
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Single-quoted string start
|
|
126
|
+
if ch == "'":
|
|
127
|
+
in_single = True
|
|
128
|
+
result.append(" ")
|
|
129
|
+
i += 1
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Double-quoted identifier start
|
|
133
|
+
if ch == '"':
|
|
134
|
+
in_double = True
|
|
135
|
+
result.append(" ")
|
|
136
|
+
i += 1
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Normal code - preserve as-is
|
|
140
|
+
result.append(ch)
|
|
141
|
+
i += 1
|
|
142
|
+
|
|
143
|
+
return "".join(result)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fast_validate_sql_chars(expr: str) -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Validate that a SQL expression contains only safe characters.
|
|
149
|
+
This is a ReDoS-resistant alternative to complex regex for basic validation.
|
|
150
|
+
|
|
151
|
+
Safe characters include:
|
|
152
|
+
- Alphanumeric characters
|
|
153
|
+
- Underscore (_)
|
|
154
|
+
- Space ( )
|
|
155
|
+
- Comma (,) -- for ORDER BY/GROUP BY
|
|
156
|
+
- Dot (.) -- for table.column
|
|
157
|
+
- Parentheses (()) -- for function calls
|
|
158
|
+
- Operators: =, <, >, !, +, -, *, /
|
|
159
|
+
- Quotes: ', " (handled carefully by other layers)
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
expr: The SQL expression to validate
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if all characters are within the safe set, False otherwise.
|
|
166
|
+
"""
|
|
167
|
+
if not expr:
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
# Safe character set: Alphanumeric, underscores, spaces, and common SQL punctuation/operators
|
|
171
|
+
# Including ?, :, @, $ for parameter placeholders
|
|
172
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ,.()'=<>!+-*/\"|?:@$")
|
|
173
|
+
|
|
174
|
+
return all(c in safe_chars for c in expr)
|
nanasqlite/utils.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility module for NanaSQLite: Provides ExpiringDict and other helper classes.
|
|
3
|
+
(NanaSQLite用ユーティリティモジュール: 有効期限付き辞書やその他の補助クラスを提供)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import collections.abc
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExpirationMode(str, Enum):
|
|
21
|
+
"""
|
|
22
|
+
Modes for detecting and handling expired items.
|
|
23
|
+
(有効期限切れの検知と処理モード)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
LAZY = "lazy" # Check on access only (アクセ時にのみチェック)
|
|
27
|
+
SCHEDULER = "scheduler" # Single background worker (単一のバックグラウンドワーカー)
|
|
28
|
+
TIMER = "timer" # Individual timers for each key (キーごとの個別タイマー)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExpiringDict(collections.abc.MutableMapping):
|
|
32
|
+
"""
|
|
33
|
+
A dictionary-like object where keys expire after a set time.
|
|
34
|
+
Supports multiple expiration modes for different scales and precision requirements.
|
|
35
|
+
(キーが一定時間後に失効する辞書型オブジェクト。規模や精度に応じて複数の失効モードをサポート)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
expiration_time: float,
|
|
41
|
+
mode: ExpirationMode = ExpirationMode.SCHEDULER,
|
|
42
|
+
on_expire: Callable[[str, Any], None] | None = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Args:
|
|
46
|
+
expiration_time: Time in seconds before a key expires. (失効までの時間(秒))
|
|
47
|
+
mode: Expiration detection mode. (失効検知モード)
|
|
48
|
+
on_expire: Callback function called when an item expires. (失効時に呼ばれるコールバック)
|
|
49
|
+
"""
|
|
50
|
+
self._data: dict[str, Any] = {}
|
|
51
|
+
self._exptimes: dict[str, float] = {} # key -> expiry (Unix timestamp)
|
|
52
|
+
self._expiration_time = expiration_time
|
|
53
|
+
self._mode = mode
|
|
54
|
+
self._on_expire = on_expire
|
|
55
|
+
|
|
56
|
+
# Mode specific structures
|
|
57
|
+
self._timers: dict[str, threading.Timer] = {}
|
|
58
|
+
self._async_tasks: dict[str, asyncio.Task] = {}
|
|
59
|
+
self._scheduler_thread: threading.Thread | None = None
|
|
60
|
+
self._scheduler_running = False
|
|
61
|
+
self._lock = threading.RLock()
|
|
62
|
+
|
|
63
|
+
if self._mode == ExpirationMode.SCHEDULER:
|
|
64
|
+
self._start_scheduler()
|
|
65
|
+
|
|
66
|
+
def _start_scheduler(self) -> None:
|
|
67
|
+
"""Start the background scheduler thread if not already running."""
|
|
68
|
+
with self._lock:
|
|
69
|
+
if self._scheduler_running:
|
|
70
|
+
return
|
|
71
|
+
self._scheduler_running = True
|
|
72
|
+
self._scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True)
|
|
73
|
+
self._scheduler_thread.start()
|
|
74
|
+
|
|
75
|
+
def _scheduler_loop(self) -> None:
|
|
76
|
+
"""Single background worker loop to evict expired items."""
|
|
77
|
+
while self._scheduler_running:
|
|
78
|
+
now = time.time()
|
|
79
|
+
expired_keys = []
|
|
80
|
+
|
|
81
|
+
with self._lock:
|
|
82
|
+
# Since items are added in order, the first item is the oldest (likely to expire first)
|
|
83
|
+
if not self._exptimes:
|
|
84
|
+
sleep_time = 1.0
|
|
85
|
+
else:
|
|
86
|
+
first_key = next(iter(self._exptimes))
|
|
87
|
+
expiry = self._exptimes[first_key]
|
|
88
|
+
if expiry <= now:
|
|
89
|
+
expired_keys.append(first_key)
|
|
90
|
+
sleep_time = 0 # Process next immediately
|
|
91
|
+
else:
|
|
92
|
+
sleep_time = min(expiry - now, 1.0)
|
|
93
|
+
|
|
94
|
+
# Do deletion outside of the common lock if possible, but for simplicity here we keep it safe
|
|
95
|
+
for key in expired_keys:
|
|
96
|
+
self._evict(key)
|
|
97
|
+
|
|
98
|
+
if sleep_time > 0:
|
|
99
|
+
time.sleep(sleep_time)
|
|
100
|
+
|
|
101
|
+
def _evict(self, key: str) -> None:
|
|
102
|
+
"""Evict an item and trigger callback."""
|
|
103
|
+
with self._lock:
|
|
104
|
+
if key in self._data:
|
|
105
|
+
value = self._data.pop(key)
|
|
106
|
+
self._exptimes.pop(key, None)
|
|
107
|
+
if self._on_expire:
|
|
108
|
+
try:
|
|
109
|
+
self._on_expire(key, value)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"Error in ExpiringDict on_expire callback for key '{key}': {e}")
|
|
112
|
+
logger.debug(f"Key '{key}' expired and removed.")
|
|
113
|
+
|
|
114
|
+
def _check_expiry(self, key: str) -> bool:
|
|
115
|
+
"""Check if a key is expired and remove it if it is (Lazy eviction)."""
|
|
116
|
+
now = time.time()
|
|
117
|
+
with self._lock:
|
|
118
|
+
if key in self._exptimes and self._exptimes[key] <= now:
|
|
119
|
+
self._evict(key)
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
124
|
+
expiry = time.time() + self._expiration_time
|
|
125
|
+
with self._lock:
|
|
126
|
+
# If item already exists, remove it first to maintain insertion order (for FIFO/Scheduler)
|
|
127
|
+
if key in self._data:
|
|
128
|
+
self._cancel_timer(key)
|
|
129
|
+
del self._data[key]
|
|
130
|
+
del self._exptimes[key]
|
|
131
|
+
|
|
132
|
+
self._data[key] = value
|
|
133
|
+
self._exptimes[key] = expiry
|
|
134
|
+
|
|
135
|
+
if self._mode == ExpirationMode.TIMER:
|
|
136
|
+
self._set_timer(key)
|
|
137
|
+
|
|
138
|
+
def _set_timer(self, key: str) -> None:
|
|
139
|
+
"""Set individual timer (TIMER mode)."""
|
|
140
|
+
# Try to use current event loop if available, else use threading.Timer
|
|
141
|
+
try:
|
|
142
|
+
loop = asyncio.get_running_loop()
|
|
143
|
+
task = loop.call_later(self._expiration_time, self._evict, key)
|
|
144
|
+
self._async_tasks[key] = task # type: ignore
|
|
145
|
+
except RuntimeError:
|
|
146
|
+
timer = threading.Timer(self._expiration_time, self._evict, args=(key,))
|
|
147
|
+
timer.daemon = True
|
|
148
|
+
timer.start()
|
|
149
|
+
self._timers[key] = timer
|
|
150
|
+
|
|
151
|
+
def _cancel_timer(self, key: str) -> None:
|
|
152
|
+
"""Cancel individual timer (TIMER mode)."""
|
|
153
|
+
if key in self._timers:
|
|
154
|
+
self._timers[key].cancel()
|
|
155
|
+
del self._timers[key]
|
|
156
|
+
if key in self._async_tasks:
|
|
157
|
+
self._async_tasks[key].cancel()
|
|
158
|
+
del self._async_tasks[key]
|
|
159
|
+
|
|
160
|
+
def __getitem__(self, key: str) -> Any:
|
|
161
|
+
self._check_expiry(key)
|
|
162
|
+
with self._lock:
|
|
163
|
+
return self._data[key]
|
|
164
|
+
|
|
165
|
+
def __delitem__(self, key: str) -> None:
|
|
166
|
+
with self._lock:
|
|
167
|
+
self._cancel_timer(key)
|
|
168
|
+
if key in self._data:
|
|
169
|
+
del self._data[key]
|
|
170
|
+
del self._exptimes[key]
|
|
171
|
+
|
|
172
|
+
def __iter__(self) -> Iterator[str]:
|
|
173
|
+
# Clean up expired items during iteration to stay accurate
|
|
174
|
+
keys = list(self._data.keys())
|
|
175
|
+
for key in keys:
|
|
176
|
+
if not self._check_expiry(key):
|
|
177
|
+
yield key
|
|
178
|
+
|
|
179
|
+
def __len__(self) -> int:
|
|
180
|
+
# Note: could be inaccurate if items expired but not yet evicted
|
|
181
|
+
# But for performance we return current size.
|
|
182
|
+
return len(self._data)
|
|
183
|
+
|
|
184
|
+
def __contains__(self, key: object) -> bool:
|
|
185
|
+
if not isinstance(key, str):
|
|
186
|
+
return False
|
|
187
|
+
return not self._check_expiry(key) and key in self._data
|
|
188
|
+
|
|
189
|
+
def set_on_expire_callback(self, callback: Callable[[str, Any], None]) -> None:
|
|
190
|
+
"""Update the expiration callback."""
|
|
191
|
+
self._on_expire = callback
|
|
192
|
+
|
|
193
|
+
def clear(self) -> None:
|
|
194
|
+
with self._lock:
|
|
195
|
+
for key in list(self._timers.keys()):
|
|
196
|
+
self._cancel_timer(key)
|
|
197
|
+
self._data.clear()
|
|
198
|
+
self._exptimes.clear()
|
|
199
|
+
self._scheduler_running = False
|
|
200
|
+
|
|
201
|
+
def __del__(self):
|
|
202
|
+
self._scheduler_running = False
|