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
audex/lib/usb.py ADDED
@@ -0,0 +1,554 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import asyncio
5
+ import contextlib
6
+ import os
7
+ import pathlib
8
+ import platform
9
+ import shutil
10
+ import typing as t
11
+
12
+ from audex.helper.mixin import AsyncLifecycleMixin
13
+ from audex.helper.mixin import LoggingMixin
14
+
15
+
16
+ class USBDevice(t.NamedTuple):
17
+ """Represents a USB storage device.
18
+
19
+ Attributes:
20
+ device_node: Device node path (e.g., /dev/sdb1 on Linux, E: on Windows).
21
+ mount_point: Where the device is mounted (e.g., /media/usb or E:\\).
22
+ label: Volume label of the device.
23
+ fs_type: Filesystem type (e.g., vfat, exfat, ntfs).
24
+ size_bytes: Total size in bytes.
25
+ vendor: Device vendor name.
26
+ model: Device model name.
27
+ """
28
+
29
+ device_node: str
30
+ mount_point: str
31
+ label: str | None
32
+ fs_type: str | None
33
+ size_bytes: int | None
34
+ vendor: str | None
35
+ model: str | None
36
+
37
+
38
+ class USBExportTask(t.NamedTuple):
39
+ """Represents a file/directory export task.
40
+
41
+ Attributes:
42
+ source: Source file or directory path.
43
+ dest_name: Destination name (relative to USB root).
44
+ is_directory: Whether the source is a directory.
45
+ """
46
+
47
+ source: pathlib.Path
48
+ dest_name: str
49
+ is_directory: bool
50
+
51
+
52
+ class USBBackend(abc.ABC):
53
+ """Abstract base class for USB device backends."""
54
+
55
+ def __init__(self, logger: t.Any) -> None:
56
+ self.logger = logger
57
+
58
+ @abc.abstractmethod
59
+ def list_devices(self) -> list[USBDevice]:
60
+ """List all currently connected USB storage devices."""
61
+
62
+ @abc.abstractmethod
63
+ async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
64
+ """Start monitoring for USB device events."""
65
+
66
+ @abc.abstractmethod
67
+ async def stop_monitoring(self) -> None:
68
+ """Stop monitoring for USB device events."""
69
+
70
+
71
+ class LinuxUSBBackend(USBBackend):
72
+ """Linux USB backend using pyudev."""
73
+
74
+ def __init__(self, logger: t.Any) -> None:
75
+ super().__init__(logger)
76
+ try:
77
+ import pyudev
78
+
79
+ self._pyudev = pyudev
80
+ self._context = pyudev.Context()
81
+ self._monitor: pyudev.Monitor | None = None
82
+ self._observer: pyudev.MonitorObserver | None = None
83
+ except ImportError as e:
84
+ raise RuntimeError(
85
+ "pyudev is required for Linux USB support. Install it with: pip install pyudev"
86
+ ) from e
87
+
88
+ def list_devices(self) -> list[USBDevice]:
89
+ """List all currently connected USB storage devices."""
90
+ devices: list[USBDevice] = []
91
+
92
+ for device in self._context.list_devices(subsystem="block", DEVTYPE="partition"):
93
+ if device.find_parent("usb") is None:
94
+ continue
95
+
96
+ mount_point = self._get_mount_point(device.device_node)
97
+ if not mount_point:
98
+ continue
99
+
100
+ id_vendor = device.get("ID_VENDOR", "Unknown")
101
+ id_model = device.get("ID_MODEL", "Unknown")
102
+ id_fs_type = device.get("ID_FS_TYPE")
103
+ id_fs_label = device.get("ID_FS_LABEL")
104
+
105
+ size_bytes = None
106
+ size_file = pathlib.Path(f"/sys/class/block/{device.sys_name}/size")
107
+ if size_file.exists():
108
+ try:
109
+ sectors = int(size_file.read_text().strip())
110
+ size_bytes = sectors * 512
111
+ except (ValueError, OSError):
112
+ pass
113
+
114
+ usb_device = USBDevice(
115
+ device_node=device.device_node,
116
+ mount_point=mount_point,
117
+ label=id_fs_label,
118
+ fs_type=id_fs_type,
119
+ size_bytes=size_bytes,
120
+ vendor=id_vendor,
121
+ model=id_model,
122
+ )
123
+ devices.append(usb_device)
124
+ self.logger.debug(f"Found USB device: {usb_device}")
125
+
126
+ return devices
127
+
128
+ def _get_mount_point(self, device_node: str) -> str | None:
129
+ """Get the mount point for a device node."""
130
+ try:
131
+ with pathlib.Path("/proc/mounts").open("r") as f:
132
+ for line in f:
133
+ parts = line.split()
134
+ if len(parts) >= 2 and parts[0] == device_node:
135
+ return parts[1]
136
+ except OSError as e:
137
+ self.logger.warning(f"Failed to read /proc/mounts: {e}")
138
+
139
+ return None
140
+
141
+ async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
142
+ """Start monitoring for USB device events."""
143
+ self._monitor = self._pyudev.Monitor.from_netlink(self._context)
144
+ self._monitor.filter_by(subsystem="block")
145
+
146
+ self._observer = self._pyudev.MonitorObserver(self._monitor, callback)
147
+ self._observer.start()
148
+ self.logger.debug("Started Linux USB monitoring")
149
+
150
+ async def stop_monitoring(self) -> None:
151
+ """Stop monitoring for USB device events."""
152
+ if self._observer:
153
+ self._observer.stop()
154
+ self._observer = None
155
+
156
+ self._monitor = None
157
+ self.logger.debug("Stopped Linux USB monitoring")
158
+
159
+
160
+ class WindowsUSBBackend(USBBackend):
161
+ """Windows USB backend using win32api and WMI."""
162
+
163
+ def __init__(self, logger: t.Any) -> None:
164
+ super().__init__(logger)
165
+ try:
166
+ import win32api
167
+ import win32file
168
+ import wmi
169
+
170
+ self._win32api = win32api
171
+ self._win32file = win32file
172
+ self._wmi = wmi
173
+ self._wmi_connection = wmi.WMI()
174
+ self._monitoring = False
175
+ self._monitor_task: asyncio.Task[None] | None = None
176
+ except ImportError as e:
177
+ raise RuntimeError(
178
+ "pywin32 and WMI are required for Windows USB support. "
179
+ "Install them with: pip install pywin32 wmi"
180
+ ) from e
181
+
182
+ def list_devices(self) -> list[USBDevice]:
183
+ """List all currently connected USB storage devices."""
184
+ devices: list[USBDevice] = []
185
+
186
+ # Get all removable drives
187
+ drive_types = self._win32api.GetLogicalDriveStrings()
188
+ drives = [d for d in drive_types.split("\x00") if d]
189
+
190
+ for drive in drives:
191
+ try:
192
+ drive_type = self._win32file.GetDriveType(drive)
193
+ # DRIVE_REMOVABLE = 2
194
+ if drive_type != 2:
195
+ continue
196
+
197
+ # Get drive information
198
+ try:
199
+ volume_info = self._win32api.GetVolumeInformation(drive)
200
+ label = volume_info[0]
201
+ fs_type = volume_info[4]
202
+ except Exception:
203
+ label = None
204
+ fs_type = None
205
+
206
+ # Get drive size
207
+ size_bytes = None
208
+ try:
209
+ _, total_bytes, _ = self._win32api.GetDiskFreeSpaceEx(drive)
210
+ size_bytes = total_bytes
211
+ except Exception:
212
+ pass
213
+
214
+ # Try to get vendor and model from WMI
215
+ vendor = None
216
+ model = None
217
+ try:
218
+ for disk in self._wmi_connection.Win32_DiskDrive():
219
+ if disk.MediaType == "Removable Media":
220
+ for partition in disk.associators("Win32_DiskDriveToDiskPartition"):
221
+ for logical_disk in partition.associators(
222
+ "Win32_LogicalDiskToPartition"
223
+ ):
224
+ if logical_disk.DeviceID == drive.rstrip("\\"):
225
+ vendor = disk.Manufacturer
226
+ model = disk.Model
227
+ break
228
+ except Exception:
229
+ pass
230
+
231
+ device_node = drive.rstrip("\\")
232
+ mount_point = drive
233
+
234
+ usb_device = USBDevice(
235
+ device_node=device_node,
236
+ mount_point=mount_point,
237
+ label=label,
238
+ fs_type=fs_type,
239
+ size_bytes=size_bytes,
240
+ vendor=vendor,
241
+ model=model,
242
+ )
243
+ devices.append(usb_device)
244
+ self.logger.debug(f"Found USB device: {usb_device}")
245
+
246
+ except Exception as e:
247
+ self.logger.warning(f"Error checking drive {drive}: {e}")
248
+ continue
249
+
250
+ return devices
251
+
252
+ async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
253
+ """Start monitoring for USB device events using WMI."""
254
+ self._monitoring = True
255
+ self._monitor_task = asyncio.create_task(self._monitor_loop(callback))
256
+ self.logger.debug("Started Windows USB monitoring")
257
+
258
+ async def stop_monitoring(self) -> None:
259
+ """Stop monitoring for USB device events."""
260
+ self._monitoring = False
261
+ if self._monitor_task:
262
+ self._monitor_task.cancel()
263
+ with contextlib.suppress(asyncio.CancelledError):
264
+ await self._monitor_task
265
+ self._monitor_task = None
266
+ self.logger.debug("Stopped Windows USB monitoring")
267
+
268
+ async def _monitor_loop(self, callback: t.Callable[[str, t.Any], None]) -> None:
269
+ """Monitor loop for Windows USB events."""
270
+ # Watch for Win32_VolumeChangeEvent
271
+ watcher = self._wmi_connection.Win32_VolumeChangeEvent.watch_for(
272
+ notification_type="Creation",
273
+ delay_secs=1,
274
+ )
275
+
276
+ while self._monitoring:
277
+ try:
278
+ # Check for events (non-blocking with timeout)
279
+ await asyncio.sleep(1)
280
+
281
+ # Poll for new devices
282
+ try:
283
+ event = watcher(timeout_ms=100)
284
+ if event and event.EventType == 2: # Event types: 2=inserted, 3=removed
285
+ self.logger.info("USB device connected (Windows)")
286
+ # Create a simple device object for callback
287
+ callback("add", {"device_type": "volume"})
288
+ except Exception:
289
+ # Timeout or no event
290
+ pass
291
+
292
+ except Exception as e:
293
+ self.logger.error(f"Error in Windows USB monitor loop: {e}")
294
+ await asyncio.sleep(5)
295
+
296
+
297
+ class USBManager(LoggingMixin, AsyncLifecycleMixin):
298
+ """Cross-platform USB storage device manager for file export.
299
+
300
+ This manager monitors USB device connections and provides functionality
301
+ to export files and directories to connected USB drives. It automatically
302
+ selects the appropriate backend (Linux/Windows) based on the platform.
303
+
304
+ Example:
305
+ ```python
306
+ # Create manager
307
+ manager = USBManager()
308
+
309
+ # Add export tasks
310
+ manager.add_export_task(
311
+ source="/var/log/app.log",
312
+ dest_name="logs/app.log",
313
+ )
314
+ manager.add_export_task(
315
+ source="/data/recordings",
316
+ dest_name="recordings",
317
+ is_directory=True,
318
+ )
319
+
320
+ # Start monitoring
321
+ await manager.start()
322
+
323
+ # Export to specific device manually
324
+ devices = manager.list_devices()
325
+ if devices:
326
+ await manager.export(devices[0])
327
+
328
+ # Stop monitoring
329
+ await manager.stop()
330
+ ```
331
+ """
332
+
333
+ __logtag__ = "audex.lib.usb.manager"
334
+
335
+ def __init__(self) -> None:
336
+ super().__init__()
337
+ self.export_tasks: list[USBExportTask] = []
338
+
339
+ # Initialize platform-specific backend
340
+ system = platform.system()
341
+ if system == "Linux":
342
+ self._backend: USBBackend = LinuxUSBBackend(self.logger)
343
+ self.logger.info("Initialized Linux USB backend")
344
+ elif system == "Windows":
345
+ self._backend = WindowsUSBBackend(self.logger)
346
+ self.logger.info("Initialized Windows USB backend")
347
+ else:
348
+ raise RuntimeError(f"Unsupported platform: {system}")
349
+
350
+ self._running = False
351
+
352
+ def add_export_task(
353
+ self,
354
+ source: str | pathlib.Path | os.PathLike[str],
355
+ dest_name: str,
356
+ *,
357
+ is_directory: bool = False,
358
+ ) -> None:
359
+ """Add a file or directory to export when USB is connected.
360
+
361
+ Args:
362
+ source: Source file or directory path.
363
+ dest_name: Destination name on USB (e.g., "logs/app.log" or "data").
364
+ is_directory: Whether the source is a directory.
365
+
366
+ Example:
367
+ ```python
368
+ # Export single file
369
+ manager.add_export_task(
370
+ source="/var/log/app.log",
371
+ dest_name="logs/app.log",
372
+ )
373
+
374
+ # Export directory
375
+ manager.add_export_task(
376
+ source="/data/sessions",
377
+ dest_name="sessions",
378
+ is_directory=True,
379
+ )
380
+ ```
381
+ """
382
+ source_path = pathlib.Path(source)
383
+ if not source_path.exists():
384
+ self.logger.warning(f"Source path does not exist: {source_path}")
385
+
386
+ task = USBExportTask(
387
+ source=source_path,
388
+ dest_name=dest_name,
389
+ is_directory=is_directory,
390
+ )
391
+ self.export_tasks.append(task)
392
+ self.logger.debug(f"Added export task: {source_path} -> {dest_name}")
393
+
394
+ def remove_export_task(self, dest_name: str) -> bool:
395
+ """Remove an export task by destination name.
396
+
397
+ Args:
398
+ dest_name: The destination name of the task to remove.
399
+
400
+ Returns:
401
+ True if task was removed, False if not found.
402
+ """
403
+ initial_count = len(self.export_tasks)
404
+ self.export_tasks = [t for t in self.export_tasks if t.dest_name != dest_name]
405
+ removed = len(self.export_tasks) < initial_count
406
+
407
+ if removed:
408
+ self.logger.debug(f"Removed export task: {dest_name}")
409
+
410
+ return removed
411
+
412
+ def clear_export_tasks(self) -> None:
413
+ """Clear all export tasks."""
414
+ self.export_tasks.clear()
415
+ self.logger.debug("Cleared all export tasks")
416
+
417
+ def list_devices(self) -> list[USBDevice]:
418
+ """List all currently connected USB storage devices.
419
+
420
+ Returns:
421
+ List of connected USB storage devices.
422
+
423
+ Example:
424
+ ```python
425
+ devices = manager.list_devices()
426
+ for device in devices:
427
+ print(
428
+ f"Found: {device.vendor} {device.model} at {device.mount_point}"
429
+ )
430
+ ```
431
+ """
432
+ return self._backend.list_devices()
433
+
434
+ async def export(
435
+ self,
436
+ device: USBDevice,
437
+ *,
438
+ tasks: list[USBExportTask] | None = None,
439
+ ) -> dict[str, bool]:
440
+ """Export files/directories to a USB device.
441
+
442
+ Args:
443
+ device: Target USB device.
444
+ tasks: List of export tasks. If None, uses self.export_tasks.
445
+
446
+ Returns:
447
+ Dictionary mapping dest_name to success status.
448
+
449
+ Example:
450
+ ```python
451
+ devices = manager.list_devices()
452
+ if devices:
453
+ results = await manager.export(devices[0])
454
+ for dest, success in results.items():
455
+ if success:
456
+ print(f"Exported {dest}")
457
+ else:
458
+ print(f"Failed to export {dest}")
459
+ ```
460
+ """
461
+ tasks_to_export = tasks or self.export_tasks
462
+ if not tasks_to_export:
463
+ self.logger.warning("No export tasks defined")
464
+ return {}
465
+
466
+ results: dict[str, bool] = {}
467
+ usb_root = pathlib.Path(device.mount_point)
468
+
469
+ self.logger.info(f"Starting export of {len(tasks_to_export)} tasks to {device.mount_point}")
470
+
471
+ for task in tasks_to_export:
472
+ try:
473
+ dest_path = usb_root / task.dest_name
474
+
475
+ # Check if source exists
476
+ if not task.source.exists():
477
+ self.logger.warning(f"Source does not exist: {task.source}")
478
+ results[task.dest_name] = False
479
+ continue
480
+
481
+ # Create parent directories
482
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
483
+
484
+ # Copy file or directory
485
+ if task.is_directory:
486
+ if dest_path.exists():
487
+ shutil.rmtree(dest_path)
488
+ shutil.copytree(task.source, dest_path)
489
+ self.logger.info(f"Exported directory: {task.source} -> {dest_path}")
490
+ else:
491
+ shutil.copy2(task.source, dest_path)
492
+ self.logger.info(f"Exported file: {task.source} -> {dest_path}")
493
+
494
+ results[task.dest_name] = True
495
+
496
+ except Exception as e:
497
+ self.logger.error(f"Failed to export {task.dest_name}: {e}", exc_info=True)
498
+ results[task.dest_name] = False
499
+
500
+ success_count = sum(1 for success in results.values() if success)
501
+ self.logger.info(f"Export completed: {success_count}/{len(tasks_to_export)} successful")
502
+
503
+ return results
504
+
505
+ async def start(self) -> None:
506
+ """Start monitoring for USB device connections.
507
+
508
+ Will automatically detect when USB devices are connected.
509
+
510
+ Example:
511
+ ```python
512
+ manager = USBManager()
513
+ manager.add_export_task(
514
+ "/data/logs", "logs", is_directory=True
515
+ )
516
+ await manager.start()
517
+ # Will monitor for USB connections
518
+ ```
519
+ """
520
+ if self._running:
521
+ self.logger.warning("USB monitor already running")
522
+ return
523
+
524
+ self._running = True
525
+ await self._backend.start_monitoring(self._handle_device_event)
526
+ self.logger.info("Started USB device monitoring")
527
+
528
+ async def stop(self) -> None:
529
+ """Stop monitoring for USB device connections."""
530
+ if not self._running:
531
+ return
532
+
533
+ self._running = False
534
+ await self._backend.stop_monitoring()
535
+ self.logger.info("Stopped USB device monitoring")
536
+
537
+ def _handle_device_event(self, action: str, device: t.Any) -> None:
538
+ """Handle USB device events.
539
+
540
+ Args:
541
+ action: Event action (add, remove, change).
542
+ device: The device that triggered the event.
543
+ """
544
+ if action != "add":
545
+ return
546
+
547
+ # For Linux, check device type
548
+ if hasattr(device, "device_type"):
549
+ if device.device_type != "partition":
550
+ return
551
+ if "usb" not in device.device_path.lower():
552
+ return
553
+
554
+ self.logger.info("USB device connected")
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+
5
+ from audex.exceptions import AudexError
6
+ from audex.helper.mixin import LoggingMixin
7
+
8
+
9
+ class VPR(LoggingMixin, abc.ABC):
10
+ group_id: str | None
11
+
12
+ def __init__(self, group_id: str | None = None) -> None:
13
+ super().__init__()
14
+ self.group_id = group_id
15
+
16
+ @abc.abstractmethod
17
+ async def create_group(self, name: str, gid: str | None = None) -> str: ...
18
+
19
+ @abc.abstractmethod
20
+ async def enroll(self, data: bytes, sr: int, uid: str | None = None) -> str: ...
21
+
22
+ @abc.abstractmethod
23
+ async def update(self, uid: str, data: bytes, sr: int) -> None: ...
24
+
25
+ @abc.abstractmethod
26
+ async def verify(self, uid: str, data: bytes, sr: int) -> float: ...
27
+
28
+
29
+ class VPRError(AudexError):
30
+ default_message = "VPR error occurred"
31
+
32
+
33
+ class GroupAlreadyExistsError(VPRError):
34
+ default_message = "VPR group already exists"
35
+
36
+
37
+ class GroupNotFoundError(VPRError):
38
+ default_message = "VPR group not found"