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.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. 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__}>"