audex 1.0.7a3__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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import collections
|
|
5
|
+
import time
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
import cachetools
|
|
9
|
+
|
|
10
|
+
from audex.lib.cache import EMPTY
|
|
11
|
+
from audex.lib.cache import NEGATIVE
|
|
12
|
+
from audex.lib.cache import VT
|
|
13
|
+
from audex.lib.cache import CacheMiss
|
|
14
|
+
from audex.lib.cache import Empty
|
|
15
|
+
from audex.lib.cache import KeyBuilder
|
|
16
|
+
from audex.lib.cache import KVCache
|
|
17
|
+
from audex.lib.cache import Negative
|
|
18
|
+
from audex.lib.cache import T
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TTLEntry:
|
|
22
|
+
"""Wrapper for cache entries with TTL information."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("expire_at", "value")
|
|
25
|
+
|
|
26
|
+
def __init__(self, value: t.Any, ttl: int | None = None):
|
|
27
|
+
self.value = value
|
|
28
|
+
self.expire_at = time.time() + ttl if ttl is not None else None
|
|
29
|
+
|
|
30
|
+
def is_expired(self) -> bool:
|
|
31
|
+
"""Check if this entry has expired."""
|
|
32
|
+
if self.expire_at is None:
|
|
33
|
+
return False
|
|
34
|
+
return time.time() > self.expire_at
|
|
35
|
+
|
|
36
|
+
def get_ttl(self) -> int | None:
|
|
37
|
+
"""Get remaining TTL in seconds."""
|
|
38
|
+
if self.expire_at is None:
|
|
39
|
+
return None
|
|
40
|
+
remaining = int(self.expire_at - time.time())
|
|
41
|
+
return max(0, remaining)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InmemoryCache(KVCache):
|
|
45
|
+
__logtag__ = "audex.lib.cache.inmemory"
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
key_builder: KeyBuilder,
|
|
51
|
+
cache_type: t.Literal["lru", "lfu", "ttl", "fifo"] = "lru",
|
|
52
|
+
maxsize: int = 1000,
|
|
53
|
+
default_ttl: int = 300,
|
|
54
|
+
negative_ttl: int = 60,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize InmemoryCache.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
key_builder: KeyBuilder instance for building cache keys
|
|
60
|
+
cache_type: Type of cache strategy ('lru', 'lfu', 'ttl', 'fifo')
|
|
61
|
+
maxsize: Maximum number of items in cache
|
|
62
|
+
default_ttl: Default TTL in seconds for cache entries
|
|
63
|
+
negative_ttl: TTL in seconds for negative cache entries
|
|
64
|
+
"""
|
|
65
|
+
super().__init__()
|
|
66
|
+
self._key_builder = key_builder
|
|
67
|
+
self.cache_type = cache_type
|
|
68
|
+
self.maxsize = maxsize
|
|
69
|
+
self.default_ttl = default_ttl
|
|
70
|
+
self.negative_ttl = negative_ttl
|
|
71
|
+
self.logger.info("Initializing Cachetools cache")
|
|
72
|
+
|
|
73
|
+
# Thread lock for thread-safe operations (async lock)
|
|
74
|
+
self._lock = asyncio.Lock()
|
|
75
|
+
|
|
76
|
+
# Initialize the appropriate cache type
|
|
77
|
+
self._cache: t.MutableMapping[str, TTLEntry]
|
|
78
|
+
if cache_type == "lru":
|
|
79
|
+
self._cache = cachetools.LRUCache(maxsize=maxsize)
|
|
80
|
+
self.logger.info(f"Initialized LRU cache with maxsize={maxsize}")
|
|
81
|
+
elif cache_type == "lfu":
|
|
82
|
+
self._cache = cachetools.LFUCache(maxsize=maxsize)
|
|
83
|
+
self.logger.info(f"Initialized LFU cache with maxsize={maxsize}")
|
|
84
|
+
elif cache_type == "ttl":
|
|
85
|
+
cache_ttl = default_ttl
|
|
86
|
+
self._cache = cachetools.TTLCache(maxsize=maxsize, ttl=cache_ttl)
|
|
87
|
+
self.logger.info(f"Initialized TTL cache with maxsize={maxsize}, ttl={cache_ttl}")
|
|
88
|
+
elif cache_type == "fifo":
|
|
89
|
+
# Cachetools doesn't have built-in FIFO, use OrderedDict wrapper
|
|
90
|
+
self._cache = collections.OrderedDict()
|
|
91
|
+
self._maxsize = maxsize
|
|
92
|
+
self.logger.info(f"Initialized FIFO cache with maxsize={maxsize}")
|
|
93
|
+
|
|
94
|
+
self.logger.info(
|
|
95
|
+
f"Cachetools cache initialized with type={self.cache_type}, maxsize={maxsize}",
|
|
96
|
+
cache_type=self.cache_type,
|
|
97
|
+
maxsize=maxsize,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def key_builder(self) -> KeyBuilder:
|
|
102
|
+
"""Get the key builder instance."""
|
|
103
|
+
return self._key_builder
|
|
104
|
+
|
|
105
|
+
async def _evict_if_needed(self) -> None:
|
|
106
|
+
"""Evict the oldest entry if FIFO cache is at capacity."""
|
|
107
|
+
if (
|
|
108
|
+
self.cache_type == "fifo"
|
|
109
|
+
and len(self._cache) >= self._maxsize
|
|
110
|
+
and isinstance(self._cache, collections.OrderedDict)
|
|
111
|
+
):
|
|
112
|
+
oldest_key = next(iter(self._cache))
|
|
113
|
+
del self._cache[oldest_key]
|
|
114
|
+
self.logger.debug(f"Evicted oldest entry: {oldest_key}")
|
|
115
|
+
|
|
116
|
+
async def _cleanup_expired(self) -> None:
|
|
117
|
+
"""Remove expired entries from cache."""
|
|
118
|
+
if self.cache_type == "ttl":
|
|
119
|
+
# TTLCache handles expiration automatically
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
expired_keys = []
|
|
123
|
+
for key, entry in self._cache.items():
|
|
124
|
+
if entry.is_expired():
|
|
125
|
+
expired_keys.append(key)
|
|
126
|
+
|
|
127
|
+
for key in expired_keys:
|
|
128
|
+
try:
|
|
129
|
+
del self._cache[key]
|
|
130
|
+
self.logger.debug(f"Removed expired entry: {key}")
|
|
131
|
+
except KeyError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
async def set_negative(self, key: str, /) -> None:
|
|
135
|
+
"""Store cache miss marker to prevent cache penetration."""
|
|
136
|
+
async with self._lock:
|
|
137
|
+
try:
|
|
138
|
+
await self._evict_if_needed()
|
|
139
|
+
entry = TTLEntry(NEGATIVE, self.negative_ttl)
|
|
140
|
+
self._cache[key] = entry
|
|
141
|
+
self.logger.debug(f"Set negative cache for key: {key}")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self.logger.warning(f"Failed to set negative cache for key {key}: {e}")
|
|
144
|
+
|
|
145
|
+
async def get_item(self, key: str) -> VT | Empty | Negative:
|
|
146
|
+
async with self._lock:
|
|
147
|
+
try:
|
|
148
|
+
await self._cleanup_expired()
|
|
149
|
+
entry = self._cache.get(key)
|
|
150
|
+
|
|
151
|
+
if entry is None:
|
|
152
|
+
self.logger.debug(f"Cache miss for key: {key}")
|
|
153
|
+
return EMPTY
|
|
154
|
+
|
|
155
|
+
if entry.is_expired():
|
|
156
|
+
del self._cache[key]
|
|
157
|
+
self.logger.debug(f"Entry expired for key: {key}")
|
|
158
|
+
return EMPTY
|
|
159
|
+
|
|
160
|
+
if isinstance(entry.value, CacheMiss):
|
|
161
|
+
self.logger.debug(f"Found negative cache for key: {key}")
|
|
162
|
+
return NEGATIVE
|
|
163
|
+
|
|
164
|
+
if isinstance(entry.value, Negative):
|
|
165
|
+
self.logger.debug(f"Found negative cache for key: {key}")
|
|
166
|
+
return NEGATIVE
|
|
167
|
+
|
|
168
|
+
self.logger.debug(f"Cache hit for key: {key}")
|
|
169
|
+
return entry.value # type: ignore
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self.logger.error(f"Error when getting key {key}: {e}")
|
|
172
|
+
return EMPTY
|
|
173
|
+
|
|
174
|
+
async def set_item(self, key: str, value: VT) -> None:
|
|
175
|
+
async with self._lock:
|
|
176
|
+
try:
|
|
177
|
+
await self._evict_if_needed()
|
|
178
|
+
entry = TTLEntry(value, self.default_ttl)
|
|
179
|
+
self._cache[key] = entry
|
|
180
|
+
self.logger.debug(f"Successfully cached key: {key}")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.logger.error(f"Failed to set cache for key {key}: {e}")
|
|
183
|
+
|
|
184
|
+
async def del_item(self, key: str) -> None:
|
|
185
|
+
async with self._lock:
|
|
186
|
+
try:
|
|
187
|
+
if key not in self._cache:
|
|
188
|
+
self.logger.debug(f"Key not found for deletion: {key}")
|
|
189
|
+
raise KeyError(key)
|
|
190
|
+
del self._cache[key]
|
|
191
|
+
self.logger.debug(f"Successfully deleted key: {key}")
|
|
192
|
+
except KeyError:
|
|
193
|
+
raise
|
|
194
|
+
except Exception as e:
|
|
195
|
+
self.logger.error(f"Error when deleting key {key}: {e}")
|
|
196
|
+
raise KeyError(key) from e
|
|
197
|
+
|
|
198
|
+
async def iter_keys(self) -> t.AsyncIterator[str]:
|
|
199
|
+
async with self._lock:
|
|
200
|
+
try:
|
|
201
|
+
await self._cleanup_expired()
|
|
202
|
+
count = 0
|
|
203
|
+
keys_snapshot = list(self._cache.keys())
|
|
204
|
+
|
|
205
|
+
for key in keys_snapshot:
|
|
206
|
+
if self.key_builder.validate(key):
|
|
207
|
+
yield key
|
|
208
|
+
count += 1
|
|
209
|
+
|
|
210
|
+
self.logger.debug(f"Iterated over {count} cache keys")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.logger.error(f"Error during iteration: {e}")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
async def len(self) -> int:
|
|
216
|
+
async with self._lock:
|
|
217
|
+
try:
|
|
218
|
+
await self._cleanup_expired()
|
|
219
|
+
count = sum(1 for key in self._cache if self.key_builder.validate(key))
|
|
220
|
+
self.logger.debug(f"Cache contains {count} keys")
|
|
221
|
+
return count
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self.logger.error(f"Error when counting keys: {e}")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
async def contains(self, key: str) -> bool:
|
|
227
|
+
if not self.key_builder.validate(key):
|
|
228
|
+
return False
|
|
229
|
+
async with self._lock:
|
|
230
|
+
try:
|
|
231
|
+
await self._cleanup_expired()
|
|
232
|
+
if key not in self._cache:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
entry = self._cache[key]
|
|
236
|
+
if entry.is_expired():
|
|
237
|
+
del self._cache[key]
|
|
238
|
+
return False
|
|
239
|
+
if isinstance(entry.value, CacheMiss):
|
|
240
|
+
return False
|
|
241
|
+
if isinstance(entry.value, Negative):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
self.logger.debug(f"Key existence check for {key}: True")
|
|
245
|
+
return True
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.error(f"Error when checking key existence {key}: {e}")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
async def keys(self) -> list[str]:
|
|
251
|
+
"""Return a list of cache keys."""
|
|
252
|
+
result = []
|
|
253
|
+
async for key in self.iter_keys():
|
|
254
|
+
result.append(key)
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
async def get(self, key: str, default: VT | T | None = None, /) -> VT | T | None:
|
|
258
|
+
"""Get value from cache, return default if not found."""
|
|
259
|
+
result = await self.get_item(key)
|
|
260
|
+
if isinstance(result, Empty):
|
|
261
|
+
return default
|
|
262
|
+
return result # type: ignore
|
|
263
|
+
|
|
264
|
+
async def setdefault(self, key: str, default: VT | None = None, /) -> VT | None:
|
|
265
|
+
"""Get value or set and return default if key doesn't exist."""
|
|
266
|
+
async with self._lock:
|
|
267
|
+
try:
|
|
268
|
+
await self._cleanup_expired()
|
|
269
|
+
entry = self._cache.get(key)
|
|
270
|
+
|
|
271
|
+
if entry is not None and not entry.is_expired():
|
|
272
|
+
if isinstance(entry.value, CacheMiss):
|
|
273
|
+
self.logger.debug(f"Key {key} in negative cache")
|
|
274
|
+
return default
|
|
275
|
+
self.logger.debug(f"Key {key} exists, returning cached value")
|
|
276
|
+
return entry.value # type: ignore
|
|
277
|
+
|
|
278
|
+
# Key doesn't exist or is expired
|
|
279
|
+
if entry is not None and entry.is_expired():
|
|
280
|
+
del self._cache[key]
|
|
281
|
+
|
|
282
|
+
if default is not None:
|
|
283
|
+
await self._evict_if_needed()
|
|
284
|
+
new_entry = TTLEntry(default, self.default_ttl)
|
|
285
|
+
self._cache[key] = new_entry
|
|
286
|
+
self.logger.debug(f"Set default value for key: {key}")
|
|
287
|
+
return default
|
|
288
|
+
|
|
289
|
+
await self._evict_if_needed()
|
|
290
|
+
neg_entry = TTLEntry(NEGATIVE, self.negative_ttl)
|
|
291
|
+
self._cache[key] = neg_entry
|
|
292
|
+
self.logger.debug(f"Set negative cache for key: {key}")
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.logger.error(f"Error in setdefault for key {key}: {e}")
|
|
297
|
+
return default
|
|
298
|
+
|
|
299
|
+
async def clear(self) -> None:
|
|
300
|
+
"""Clear all cache entries with the configured prefix."""
|
|
301
|
+
async with self._lock:
|
|
302
|
+
try:
|
|
303
|
+
keys_to_delete = [key for key in self._cache if self.key_builder.validate(key)]
|
|
304
|
+
|
|
305
|
+
for key in keys_to_delete:
|
|
306
|
+
del self._cache[key]
|
|
307
|
+
|
|
308
|
+
self.logger.info(f"Cleared {len(keys_to_delete)} cache entries")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
self.logger.error(f"Failed to clear cache: {e}")
|
|
311
|
+
|
|
312
|
+
async def pop(self, key: str, default: VT | T | None = None, /) -> VT | T | None:
|
|
313
|
+
"""Remove and return value, or return default if not found."""
|
|
314
|
+
async with self._lock:
|
|
315
|
+
try:
|
|
316
|
+
await self._cleanup_expired()
|
|
317
|
+
entry = self._cache.get(key)
|
|
318
|
+
|
|
319
|
+
if entry is not None and not entry.is_expired():
|
|
320
|
+
del self._cache[key]
|
|
321
|
+
if isinstance(entry.value, CacheMiss):
|
|
322
|
+
return default
|
|
323
|
+
self.logger.debug(f"Successfully popped key: {key}")
|
|
324
|
+
return entry.value # type: ignore
|
|
325
|
+
|
|
326
|
+
self.logger.debug(f"Key not found for pop operation: {key}")
|
|
327
|
+
return default
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self.logger.error(f"Error when popping key {key}: {e}")
|
|
330
|
+
return default
|
|
331
|
+
|
|
332
|
+
async def popitem(self) -> tuple[str, VT]:
|
|
333
|
+
"""Remove and return an arbitrary (key, value) pair."""
|
|
334
|
+
async with self._lock:
|
|
335
|
+
try:
|
|
336
|
+
await self._cleanup_expired()
|
|
337
|
+
|
|
338
|
+
for key in list(self._cache.keys()):
|
|
339
|
+
if not self.key_builder.validate(key):
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
entry = self._cache.get(key)
|
|
343
|
+
if entry is not None and not entry.is_expired():
|
|
344
|
+
del self._cache[key]
|
|
345
|
+
if not isinstance(entry.value, CacheMiss):
|
|
346
|
+
self.logger.debug(f"Successfully popped item: {key}")
|
|
347
|
+
return key, entry.value
|
|
348
|
+
|
|
349
|
+
self.logger.debug("No items to pop")
|
|
350
|
+
raise KeyError("popitem(): cache is empty")
|
|
351
|
+
except KeyError:
|
|
352
|
+
raise
|
|
353
|
+
except Exception as e:
|
|
354
|
+
self.logger.error(f"Error during popitem: {e}")
|
|
355
|
+
raise KeyError(f"popitem() failed: {e}") from e
|
|
356
|
+
|
|
357
|
+
async def set(self, key: str, value: VT, /) -> None:
|
|
358
|
+
"""Set a key-value pair (alias for set_item)."""
|
|
359
|
+
await self.set_item(key, value)
|
|
360
|
+
|
|
361
|
+
async def setx(self, key: str, value: VT, /, ttl: int | None = None) -> None:
|
|
362
|
+
"""Set a key-value pair with optional TTL."""
|
|
363
|
+
async with self._lock:
|
|
364
|
+
try:
|
|
365
|
+
await self._evict_if_needed()
|
|
366
|
+
entry = TTLEntry(value, ttl)
|
|
367
|
+
self._cache[key] = entry
|
|
368
|
+
|
|
369
|
+
if ttl is not None:
|
|
370
|
+
self.logger.debug(f"Set key {key} with TTL {ttl}")
|
|
371
|
+
else:
|
|
372
|
+
self.logger.debug(f"Set key {key} without TTL")
|
|
373
|
+
except Exception as e:
|
|
374
|
+
self.logger.error(f"Failed to setx for key {key}: {e}")
|
|
375
|
+
|
|
376
|
+
async def ttl(self, key: str, /) -> int | None:
|
|
377
|
+
"""Get TTL for a key."""
|
|
378
|
+
async with self._lock:
|
|
379
|
+
try:
|
|
380
|
+
entry = self._cache.get(key)
|
|
381
|
+
|
|
382
|
+
if entry is None:
|
|
383
|
+
self.logger.debug(f"Key {key} does not exist")
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
if entry.is_expired():
|
|
387
|
+
del self._cache[key]
|
|
388
|
+
self.logger.debug(f"Key {key} has expired")
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
ttl_value = entry.get_ttl()
|
|
392
|
+
if ttl_value is None:
|
|
393
|
+
self.logger.debug(f"Key {key} exists without expiration")
|
|
394
|
+
else:
|
|
395
|
+
self.logger.debug(f"Key {key} TTL: {ttl_value}")
|
|
396
|
+
|
|
397
|
+
return ttl_value
|
|
398
|
+
except Exception as e:
|
|
399
|
+
self.logger.error(f"Error when getting TTL for key {key}: {e}")
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
async def expire(self, key: str, /, ttl: int | None = None) -> None:
|
|
403
|
+
"""Set or remove expiration for a key."""
|
|
404
|
+
async with self._lock:
|
|
405
|
+
try:
|
|
406
|
+
entry = self._cache.get(key)
|
|
407
|
+
|
|
408
|
+
if entry is None:
|
|
409
|
+
self.logger.warning(f"Cannot set expiration for non-existent key: {key}")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
if entry.is_expired():
|
|
413
|
+
del self._cache[key]
|
|
414
|
+
self.logger.warning(f"Key {key} has already expired")
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Create new entry with updated TTL
|
|
418
|
+
new_entry = TTLEntry(entry.value, ttl)
|
|
419
|
+
self._cache[key] = new_entry
|
|
420
|
+
|
|
421
|
+
if ttl is not None:
|
|
422
|
+
self.logger.debug(f"Set expiration for key {key}: {ttl} seconds")
|
|
423
|
+
else:
|
|
424
|
+
self.logger.debug(f"Removed expiration for key: {key}")
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.logger.error(f"Error when setting expiration for key {key}: {e}")
|
|
427
|
+
|
|
428
|
+
async def incr(self, key: str, /, amount: int = 1) -> int:
|
|
429
|
+
"""Increment a key's value."""
|
|
430
|
+
async with self._lock:
|
|
431
|
+
try:
|
|
432
|
+
await self._cleanup_expired()
|
|
433
|
+
entry = self._cache.get(key)
|
|
434
|
+
|
|
435
|
+
if entry is None or entry.is_expired():
|
|
436
|
+
# Initialize to amount if key doesn't exist
|
|
437
|
+
new_value = amount
|
|
438
|
+
await self._evict_if_needed()
|
|
439
|
+
new_entry = TTLEntry(new_value, self.default_ttl)
|
|
440
|
+
self._cache[key] = new_entry
|
|
441
|
+
self.logger.debug(f"Initialized and incremented key {key} to {new_value}")
|
|
442
|
+
return new_value
|
|
443
|
+
|
|
444
|
+
if not isinstance(entry.value, int):
|
|
445
|
+
self.logger.error(f"Cannot increment non-integer value for key {key}")
|
|
446
|
+
raise ValueError(f"Value at key {key} is not an integer")
|
|
447
|
+
|
|
448
|
+
new_value = entry.value + amount
|
|
449
|
+
# Preserve existing TTL
|
|
450
|
+
existing_ttl = entry.get_ttl()
|
|
451
|
+
new_entry = TTLEntry(new_value, existing_ttl)
|
|
452
|
+
self._cache[key] = new_entry
|
|
453
|
+
|
|
454
|
+
self.logger.debug(f"Incremented key {key} by {amount}, result: {new_value}")
|
|
455
|
+
return new_value
|
|
456
|
+
except Exception as e:
|
|
457
|
+
self.logger.error(f"Error when incrementing key {key}: {e}")
|
|
458
|
+
return 0
|
|
459
|
+
|
|
460
|
+
async def decr(self, key: str, /, amount: int = 1) -> int:
|
|
461
|
+
"""Decrement a key's value."""
|
|
462
|
+
return await self.incr(key, -amount)
|
|
463
|
+
|
|
464
|
+
async def values(self) -> list[VT]:
|
|
465
|
+
"""Return all cache values."""
|
|
466
|
+
async with self._lock:
|
|
467
|
+
try:
|
|
468
|
+
await self._cleanup_expired()
|
|
469
|
+
values = []
|
|
470
|
+
for key, entry in self._cache.items():
|
|
471
|
+
if not self.key_builder.validate(key):
|
|
472
|
+
continue
|
|
473
|
+
if entry.is_expired():
|
|
474
|
+
continue
|
|
475
|
+
if isinstance(entry.value, CacheMiss):
|
|
476
|
+
continue
|
|
477
|
+
values.append(entry.value)
|
|
478
|
+
return values
|
|
479
|
+
except Exception as e:
|
|
480
|
+
self.logger.error(f"Error when getting values: {e}")
|
|
481
|
+
return []
|
|
482
|
+
|
|
483
|
+
async def items(self) -> list[tuple[str, VT]]:
|
|
484
|
+
"""Return all cache items as (key, value) pairs."""
|
|
485
|
+
async with self._lock:
|
|
486
|
+
try:
|
|
487
|
+
await self._cleanup_expired()
|
|
488
|
+
items = []
|
|
489
|
+
for key, entry in self._cache.items():
|
|
490
|
+
if not self.key_builder.validate(key):
|
|
491
|
+
continue
|
|
492
|
+
if entry.is_expired():
|
|
493
|
+
continue
|
|
494
|
+
if isinstance(entry.value, CacheMiss):
|
|
495
|
+
continue
|
|
496
|
+
items.append((key, entry.value))
|
|
497
|
+
return items
|
|
498
|
+
except Exception as e:
|
|
499
|
+
self.logger.error(f"Error when getting items: {e}")
|
|
500
|
+
return []
|
|
501
|
+
|
|
502
|
+
async def init(self) -> None:
|
|
503
|
+
"""Initialize cache resources if needed."""
|
|
504
|
+
self.logger.info("InmemoryCache initialized")
|
|
505
|
+
|
|
506
|
+
async def close(self) -> None:
|
|
507
|
+
"""Close cache and cleanup resources."""
|
|
508
|
+
async with self._lock:
|
|
509
|
+
try:
|
|
510
|
+
self._cache.clear()
|
|
511
|
+
self.logger.info("Cachetools cache closed and cleared")
|
|
512
|
+
except Exception as e:
|
|
513
|
+
self.logger.warning(f"Error while closing cache: {e}")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from audex.helper.mixin import AsyncContextMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Database(AsyncContextMixin, abc.ABC):
|
|
10
|
+
"""Abstract base class for database containers.
|
|
11
|
+
|
|
12
|
+
All database implementations should inherit from this class and implement
|
|
13
|
+
the required abstract methods. This ensures a consistent interface across
|
|
14
|
+
different database backends.
|
|
15
|
+
|
|
16
|
+
The class provides:
|
|
17
|
+
1. Lifecycle management through AsyncContextMixin (init/close)
|
|
18
|
+
2. Health check interface (ping)
|
|
19
|
+
3. Raw query execution interface (exec)
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
```python
|
|
23
|
+
class MyDatabase(Database):
|
|
24
|
+
async def init(self) -> None:
|
|
25
|
+
# Initialize connection
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def close(self) -> None:
|
|
29
|
+
# Close connection
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
async def ping(self) -> bool:
|
|
33
|
+
# Check connectivity
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
async def exec(
|
|
37
|
+
self, readonly: bool, **kwargs: t.Any
|
|
38
|
+
) -> t.Any:
|
|
39
|
+
# Execute raw query
|
|
40
|
+
pass
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@abc.abstractmethod
|
|
45
|
+
async def ping(self) -> bool:
|
|
46
|
+
"""Check database connectivity.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if database is reachable and healthy, False otherwise.
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
Implementations should not raise exceptions. Instead, catch
|
|
53
|
+
any errors and return False.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
async def exec(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
59
|
+
"""Execute a raw database command.
|
|
60
|
+
|
|
61
|
+
This method provides a way to execute native database commands
|
|
62
|
+
when ORM abstractions are insufficient or when optimization
|
|
63
|
+
requires direct database access.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
*args: Positional arguments for the database command.
|
|
67
|
+
**kwargs: Database-specific command parameters.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Database-specific result object.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
RuntimeError: If the execution fails.
|
|
74
|
+
|
|
75
|
+
Note:
|
|
76
|
+
The exact signature and return type will vary by database
|
|
77
|
+
implementation. Refer to specific database class documentation
|
|
78
|
+
for details.
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return f"DATABASE <{self.__class__.__name__}>"
|