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,74 @@
1
+ from __future__ import annotations
2
+
3
+ import functools as ft
4
+ import typing as t
5
+
6
+ if t.TYPE_CHECKING:
7
+ from audex.lib.repos import BaseRepository
8
+
9
+ RepositoryMethodT = t.TypeVar("RepositoryMethodT", bound=t.Callable[..., t.Awaitable[t.Any]])
10
+
11
+
12
+ def log_repo_call(func: RepositoryMethodT) -> RepositoryMethodT:
13
+ """Decorator to log repository method calls.
14
+
15
+ Logs the operation with repo type, collection/table name, and operation
16
+ name. This decorator should be applied BEFORE track_repo_call decorator.
17
+
18
+ Example:
19
+ ```python
20
+ class UserRepository(MongoRepository[User]):
21
+ @log_repo_call
22
+ @track_repo_call
23
+ async def create(self, data: User) -> str:
24
+ # Implementation
25
+ return user_id
26
+ ```
27
+ """
28
+
29
+ @ft.wraps(func)
30
+ async def wrapper(self: BaseRepository[t.Any], *args: t.Any, **kwargs: t.Any) -> t.Any:
31
+ operation = func.__name__
32
+ repo_type = self.__repotype__
33
+
34
+ # Get collection name from subclass
35
+ if hasattr(self, "__collname__"):
36
+ collection = self.__collname__
37
+ elif hasattr(self, "__tablename__"):
38
+ collection = self.__tablename__
39
+ else:
40
+ collection = "unknown"
41
+
42
+ # Log the call
43
+ self.logger.info( # type: ignore
44
+ f"Repository operation: {repo_type}.{collection}.{operation}",
45
+ repo_type=repo_type,
46
+ collection=collection,
47
+ operation=operation,
48
+ )
49
+
50
+ try:
51
+ result = await func(self, *args, **kwargs)
52
+
53
+ self.logger.info( # type: ignore
54
+ f"Repository operation completed: {repo_type}.{collection}.{operation}",
55
+ repo_type=repo_type,
56
+ collection=collection,
57
+ operation=operation,
58
+ status="success",
59
+ )
60
+ return result
61
+
62
+ except Exception as e:
63
+ self.logger.error( # type: ignore
64
+ f"Repository operation failed: {repo_type}.{collection}.{operation}: {e}",
65
+ repo_type=repo_type,
66
+ collection=collection,
67
+ operation=operation,
68
+ error=str(e),
69
+ error_type=type(e).__name__,
70
+ status="error",
71
+ )
72
+ raise
73
+
74
+ return wrapper # type: ignore
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ import typing as t
5
+
6
+ import sqlalchemy as sa
7
+ import sqlmodel as sqlm
8
+
9
+ from audex.entity.doctor import Doctor
10
+ from audex.filters import Filter
11
+ from audex.lib.database.sqlite import SQLite
12
+ from audex.lib.repos.database.sqlite import SQLiteRepository
13
+ from audex.lib.repos.tables.doctor import DoctorTable
14
+
15
+
16
+ class DoctorRepository(SQLiteRepository[Doctor]):
17
+ """SQLite implementation of Doctor repository.
18
+
19
+ Provides CRUD operations for Doctor entities with additional
20
+ specialized query methods for authentication and voiceprint
21
+ management.
22
+ """
23
+
24
+ __table__ = DoctorTable
25
+ __tablename__ = DoctorTable.__tablename__
26
+
27
+ def __init__(self, sqlite: SQLite) -> None:
28
+ super().__init__(sqlite)
29
+
30
+ async def create(self, data: Doctor, /) -> str:
31
+ """Create a new doctor in the database.
32
+
33
+ Args:
34
+ data: The doctor entity to create.
35
+
36
+ Returns:
37
+ The ID of the created doctor.
38
+ """
39
+ async with self.sqlite.session() as session:
40
+ doctor_table = DoctorTable.from_entity(data)
41
+ session.add(doctor_table)
42
+ await session.commit()
43
+ await session.refresh(doctor_table)
44
+ return doctor_table.id
45
+
46
+ async def read(self, id: str, /) -> Doctor | None:
47
+ """Read a doctor by ID.
48
+
49
+ Args:
50
+ id: The ID (id) of the doctor to retrieve.
51
+
52
+ Returns:
53
+ The doctor entity if found, None otherwise.
54
+ """
55
+ async with self.sqlite.session() as session:
56
+ stmt = sqlm.select(DoctorTable).where(DoctorTable.id == id)
57
+ result = await session.execute(stmt)
58
+ doctor_obj = result.scalar_one_or_none()
59
+
60
+ if doctor_obj is None:
61
+ return None
62
+
63
+ return doctor_obj.to_entity()
64
+
65
+ async def first(self, filter: Filter) -> Doctor | None:
66
+ """Retrieve the first doctor matching the filter.
67
+
68
+ Args:
69
+ filter: Filter to apply when searching for the doctor.
70
+
71
+ Returns:
72
+ The first doctor entity matching the filter, or None if no match.
73
+ """
74
+ spec = self.build_query_spec(filter)
75
+
76
+ async with self.sqlite.session() as session:
77
+ stmt = sqlm.select(DoctorTable)
78
+
79
+ for clause in spec.where:
80
+ stmt = stmt.where(clause)
81
+
82
+ for order in spec.order_by:
83
+ stmt = stmt.order_by(order)
84
+
85
+ stmt = stmt.limit(1)
86
+
87
+ result = await session.execute(stmt)
88
+ doctor_obj = result.scalar_one_or_none()
89
+
90
+ if doctor_obj is None:
91
+ return None
92
+
93
+ return doctor_obj.to_entity()
94
+
95
+ async def list(
96
+ self,
97
+ arg: builtins.list[str] | t.Optional[Filter] = None, # noqa
98
+ *,
99
+ page_index: int = 0,
100
+ page_size: int = 100,
101
+ ) -> builtins.list[Doctor]:
102
+ """List doctors by IDs or with optional filtering and
103
+ pagination.
104
+
105
+ Args:
106
+ arg: Either a list of IDs to retrieve, or an optional filter.
107
+ page_index: Zero-based page index for pagination.
108
+ page_size: Number of items per page.
109
+
110
+ Returns:
111
+ List of doctor entities matching the criteria.
112
+ """
113
+ async with self.sqlite.session() as session:
114
+ if isinstance(arg, list):
115
+ if not arg:
116
+ return []
117
+
118
+ stmt = sqlm.select(DoctorTable).where(sqlm.col(DoctorTable.id).in_(arg))
119
+ result = await session.execute(stmt)
120
+ doctor_objs = result.scalars().all()
121
+ return [obj.to_entity() for obj in doctor_objs]
122
+
123
+ spec = self.build_query_spec(arg)
124
+ stmt = sqlm.select(DoctorTable)
125
+
126
+ for clause in spec.where:
127
+ stmt = stmt.where(clause)
128
+
129
+ for order in spec.order_by:
130
+ stmt = stmt.order_by(order)
131
+
132
+ stmt = stmt.offset(page_index * page_size).limit(page_size)
133
+
134
+ result = await session.execute(stmt)
135
+ doctor_objs = result.scalars().all()
136
+ return [obj.to_entity() for obj in doctor_objs]
137
+
138
+ async def update(self, data: Doctor, /) -> str:
139
+ """Update an existing doctor.
140
+
141
+ Args:
142
+ data: The doctor entity with updated values.
143
+
144
+ Returns:
145
+ The ID of the updated doctor.
146
+
147
+ Raises:
148
+ ValueError: If the doctor with the given ID does not exist.
149
+ """
150
+ async with self.sqlite.session() as session:
151
+ stmt = sqlm.select(DoctorTable).where(DoctorTable.id == data.id)
152
+ result = await session.execute(stmt)
153
+ doctor_obj = result.scalar_one_or_none()
154
+
155
+ if doctor_obj is None:
156
+ raise ValueError(f"Doctor with id {data.id} not found")
157
+
158
+ doctor_obj.update(data)
159
+ session.add(doctor_obj)
160
+ await session.commit()
161
+ await session.refresh(doctor_obj)
162
+ return doctor_obj.id
163
+
164
+ async def update_many(self, datas: builtins.list[Doctor]) -> builtins.list[str]:
165
+ """Update multiple doctors in the database.
166
+
167
+ Args:
168
+ datas: List of doctor entities with updated values.
169
+
170
+ Returns:
171
+ List of IDs of the updated doctors.
172
+
173
+ Raises:
174
+ ValueError: If any doctor with the given ID does not exist.
175
+ """
176
+ if not datas:
177
+ return []
178
+
179
+ updated_ids: builtins.list[str] = []
180
+ async with self.sqlite.session() as session:
181
+ ids = [data.id for data in datas]
182
+ stmt = sqlm.select(DoctorTable).where(sqlm.col(DoctorTable.id).in_(ids))
183
+ result = await session.execute(stmt)
184
+ table_objs = {obj.id: obj for obj in result.scalars().all()}
185
+
186
+ missing_ids = set(ids) - set(table_objs.keys())
187
+ if missing_ids:
188
+ raise ValueError(f"Doctors with IDs {missing_ids} not found")
189
+
190
+ for data in datas:
191
+ doctor_obj = table_objs[data.id]
192
+ doctor_obj.update(data)
193
+ session.add(doctor_obj)
194
+ updated_ids.append(doctor_obj.id)
195
+
196
+ await session.commit()
197
+ return updated_ids
198
+
199
+ async def delete(self, id: str, /) -> bool:
200
+ """Delete a doctor by ID.
201
+
202
+ Args:
203
+ id: The ID (id) of the doctor to delete.
204
+
205
+ Returns:
206
+ True if the doctor was deleted, False if not found.
207
+ """
208
+ async with self.sqlite.session() as session:
209
+ stmt = sqlm.select(DoctorTable).where(DoctorTable.id == id)
210
+ result = await session.execute(stmt)
211
+ doctor_obj = result.scalar_one_or_none()
212
+
213
+ if doctor_obj is None:
214
+ return False
215
+
216
+ await session.delete(doctor_obj)
217
+ await session.commit()
218
+ return True
219
+
220
+ async def delete_many(
221
+ self,
222
+ arg: builtins.list[str] | t.Optional[Filter] = None, # noqa
223
+ ) -> builtins.list[str]:
224
+ """Delete multiple doctors by IDs or matching a filter.
225
+
226
+ Args:
227
+ arg: Either a list of IDs to delete, or an optional filter.
228
+
229
+ Returns:
230
+ If deleting by IDs, returns list of deleted IDs.
231
+ If deleting by filter, returns count of deleted records.
232
+ """
233
+ async with self.sqlite.session() as session:
234
+ if isinstance(arg, list):
235
+ if not arg:
236
+ return []
237
+
238
+ stmt = sqlm.select(DoctorTable).where(sqlm.col(DoctorTable.id).in_(arg))
239
+ result = await session.execute(stmt)
240
+ doctor_objs = result.scalars().all()
241
+
242
+ if not doctor_objs:
243
+ return []
244
+
245
+ doctor_ids = [obj.id for obj in doctor_objs]
246
+ for obj in doctor_objs:
247
+ await session.delete(obj)
248
+
249
+ await session.commit()
250
+ return doctor_ids
251
+
252
+ spec = self.build_query_spec(arg)
253
+ stmt = sqlm.select(DoctorTable.id) # type: ignore
254
+ for clause in spec.where:
255
+ stmt = stmt.where(clause)
256
+
257
+ result = await session.execute(stmt)
258
+ doctor_ids = [row[0] for row in result.all()]
259
+
260
+ if not doctor_ids:
261
+ return []
262
+
263
+ delete_stmt = sa.delete(DoctorTable).where(sqlm.col(DoctorTable.id).in_(doctor_ids))
264
+ await session.execute(delete_stmt)
265
+ await session.commit()
266
+ return doctor_ids
267
+
268
+ async def count(self, filter: t.Optional[Filter] = None) -> int: # noqa
269
+ """Count doctors matching the filter.
270
+
271
+ Args:
272
+ filter: Optional filter to apply. If None, counts all doctors.
273
+
274
+ Returns:
275
+ Number of doctors matching the filter.
276
+ """
277
+ spec = self.build_query_spec(filter)
278
+
279
+ async with self.sqlite.session() as session:
280
+ stmt = sqlm.select(sa.func.count()).select_from(DoctorTable)
281
+
282
+ for clause in spec.where:
283
+ stmt = stmt.where(clause)
284
+
285
+ result = await session.execute(stmt)
286
+ return result.scalar_one()
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ import typing as t
5
+
6
+ import sqlalchemy as sa
7
+ import sqlmodel as sqlm
8
+
9
+ from audex.entity.segment import Segment
10
+ from audex.filters import Filter
11
+ from audex.lib.database.sqlite import SQLite
12
+ from audex.lib.repos.database.sqlite import SQLiteRepository
13
+ from audex.lib.repos.tables.segment import SegmentTable
14
+
15
+
16
+ class SegmentRepository(SQLiteRepository[Segment]):
17
+ """SQLite implementation of Segment repository.
18
+
19
+ Provides CRUD operations for Segment entities with additional
20
+ specialized query methods for segment management by session.
21
+ """
22
+
23
+ __table__ = SegmentTable
24
+ __tablename__ = SegmentTable.__tablename__
25
+
26
+ def __init__(self, sqlite: SQLite) -> None:
27
+ super().__init__(sqlite)
28
+
29
+ async def create(self, data: Segment, /) -> str:
30
+ """Create a new segment in the database.
31
+
32
+ Args:
33
+ data: The segment entity to create.
34
+
35
+ Returns:
36
+ The ID of the created segment.
37
+ """
38
+ async with self.sqlite.session() as session:
39
+ segment_table = SegmentTable.from_entity(data)
40
+ session.add(segment_table)
41
+ await session.commit()
42
+ await session.refresh(segment_table)
43
+ return segment_table.id
44
+
45
+ async def read(self, id: str, /) -> Segment | None:
46
+ """Read a segment by ID.
47
+
48
+ Args:
49
+ id: The ID (id) of the segment to retrieve.
50
+
51
+ Returns:
52
+ The segment entity if found, None otherwise.
53
+ """
54
+ async with self.sqlite.session() as session:
55
+ stmt = sqlm.select(SegmentTable).where(SegmentTable.id == id)
56
+ result = await session.execute(stmt)
57
+ segment_obj = result.scalar_one_or_none()
58
+
59
+ if segment_obj is None:
60
+ return None
61
+
62
+ return segment_obj.to_entity()
63
+
64
+ async def first(self, filter: Filter) -> Segment | None:
65
+ """Retrieve the first segment matching the filter.
66
+
67
+ Args:
68
+ filter: Filter to apply when searching for the segment.
69
+
70
+ Returns:
71
+ The first segment entity matching the filter, or None if no match.
72
+ """
73
+ spec = self.build_query_spec(filter)
74
+
75
+ async with self.sqlite.session() as session:
76
+ stmt = sqlm.select(SegmentTable)
77
+
78
+ for clause in spec.where:
79
+ stmt = stmt.where(clause)
80
+
81
+ for order in spec.order_by:
82
+ stmt = stmt.order_by(order)
83
+
84
+ stmt = stmt.limit(1)
85
+
86
+ result = await session.execute(stmt)
87
+ segment_obj = result.scalar_one_or_none()
88
+
89
+ if segment_obj is None:
90
+ return None
91
+
92
+ return segment_obj.to_entity()
93
+
94
+ async def list(
95
+ self,
96
+ arg: builtins.list[str] | t.Optional[Filter] = None, # noqa
97
+ *,
98
+ page_index: int = 0,
99
+ page_size: int = 100,
100
+ ) -> builtins.list[Segment]:
101
+ """List segments by IDs or with optional filtering and
102
+ pagination.
103
+
104
+ Args:
105
+ arg: Either a list of IDs to retrieve, or an optional filter.
106
+ page_index: Zero-based page index for pagination.
107
+ page_size: Number of items per page.
108
+
109
+ Returns:
110
+ List of segment entities matching the criteria.
111
+ """
112
+ async with self.sqlite.session() as session:
113
+ if isinstance(arg, list):
114
+ if not arg:
115
+ return []
116
+
117
+ stmt = sqlm.select(SegmentTable).where(sqlm.col(SegmentTable.id).in_(arg))
118
+ result = await session.execute(stmt)
119
+ segment_objs = result.scalars().all()
120
+ return [obj.to_entity() for obj in segment_objs]
121
+
122
+ spec = self.build_query_spec(arg)
123
+ stmt = sqlm.select(SegmentTable)
124
+
125
+ for clause in spec.where:
126
+ stmt = stmt.where(clause)
127
+
128
+ for order in spec.order_by:
129
+ stmt = stmt.order_by(order)
130
+
131
+ stmt = stmt.offset(page_index * page_size).limit(page_size)
132
+
133
+ result = await session.execute(stmt)
134
+ segment_objs = result.scalars().all()
135
+ return [obj.to_entity() for obj in segment_objs]
136
+
137
+ async def update(self, data: Segment, /) -> str:
138
+ """Update an existing segment.
139
+
140
+ Args:
141
+ data: The segment entity with updated values.
142
+
143
+ Returns:
144
+ The ID of the updated segment.
145
+
146
+ Raises:
147
+ ValueError: If the segment with the given ID does not exist.
148
+ """
149
+ async with self.sqlite.session() as session:
150
+ stmt = sqlm.select(SegmentTable).where(SegmentTable.id == data.id)
151
+ result = await session.execute(stmt)
152
+ segment_obj = result.scalar_one_or_none()
153
+
154
+ if segment_obj is None:
155
+ raise ValueError(f"Segment with id {data.id} not found")
156
+
157
+ segment_obj.update(data)
158
+ session.add(segment_obj)
159
+ await session.commit()
160
+ await session.refresh(segment_obj)
161
+ return segment_obj.id
162
+
163
+ async def update_many(self, datas: builtins.list[Segment]) -> builtins.list[str]:
164
+ """Update multiple segments in the database.
165
+
166
+ Args:
167
+ datas: List of segment entities with updated values.
168
+
169
+ Returns:
170
+ List of IDs of the updated segments.
171
+
172
+ Raises:
173
+ ValueError: If any segment with the given ID does not exist.
174
+ """
175
+ if not datas:
176
+ return []
177
+
178
+ updated_ids: builtins.list[str] = []
179
+ async with self.sqlite.session() as session:
180
+ ids = [data.id for data in datas]
181
+ stmt = sqlm.select(SegmentTable).where(sqlm.col(SegmentTable.id).in_(ids))
182
+ result = await session.execute(stmt)
183
+ table_objs = {obj.id: obj for obj in result.scalars().all()}
184
+
185
+ missing_ids = set(ids) - set(table_objs.keys())
186
+ if missing_ids:
187
+ raise ValueError(f"Segments with IDs {missing_ids} not found")
188
+
189
+ for data in datas:
190
+ segment_obj = table_objs[data.id]
191
+ segment_obj.update(data)
192
+ session.add(segment_obj)
193
+ updated_ids.append(segment_obj.id)
194
+
195
+ await session.commit()
196
+ return updated_ids
197
+
198
+ async def delete(self, id: str, /) -> bool:
199
+ """Delete a segment by ID.
200
+
201
+ Args:
202
+ id: The ID (id) of the segment to delete.
203
+
204
+ Returns:
205
+ True if the segment was deleted, False if not found.
206
+ """
207
+ async with self.sqlite.session() as session:
208
+ stmt = sqlm.select(SegmentTable).where(SegmentTable.id == id)
209
+ result = await session.execute(stmt)
210
+ segment_obj = result.scalar_one_or_none()
211
+
212
+ if segment_obj is None:
213
+ return False
214
+
215
+ await session.delete(segment_obj)
216
+ await session.commit()
217
+ return True
218
+
219
+ async def delete_many(
220
+ self,
221
+ arg: builtins.list[str] | t.Optional[Filter] = None, # noqa
222
+ ) -> builtins.list[str]:
223
+ """Delete multiple segments by IDs or matching a filter.
224
+
225
+ Args:
226
+ arg: Either a list of IDs to delete, or an optional filter.
227
+
228
+ Returns:
229
+ If deleting by IDs, returns list of deleted IDs.
230
+ If deleting by filter, returns count of deleted records.
231
+ """
232
+ async with self.sqlite.session() as session:
233
+ if isinstance(arg, list):
234
+ if not arg:
235
+ return []
236
+
237
+ stmt = sqlm.select(SegmentTable).where(sqlm.col(SegmentTable.id).in_(arg))
238
+ result = await session.execute(stmt)
239
+ segment_objs = result.scalars().all()
240
+
241
+ if not segment_objs:
242
+ return []
243
+
244
+ segment_ids = [obj.id for obj in segment_objs]
245
+ for obj in segment_objs:
246
+ await session.delete(obj)
247
+
248
+ await session.commit()
249
+ return segment_ids
250
+
251
+ spec = self.build_query_spec(arg)
252
+ stmt = sqlm.select(SegmentTable.id) # type: ignore
253
+ for clause in spec.where:
254
+ stmt = stmt.where(clause)
255
+
256
+ result = await session.execute(stmt)
257
+ segment_ids = [row[0] for row in result.all()]
258
+
259
+ if not segment_ids:
260
+ return []
261
+
262
+ delete_stmt = sa.delete(SegmentTable).where(sqlm.col(SegmentTable.id).in_(segment_ids))
263
+ await session.execute(delete_stmt)
264
+ await session.commit()
265
+ return segment_ids
266
+
267
+ async def count(self, filter: t.Optional[Filter] = None) -> int: # noqa
268
+ """Count segments matching the filter.
269
+
270
+ Args:
271
+ filter: Optional filter to apply. If None, counts all segments.
272
+
273
+ Returns:
274
+ Number of segments matching the filter.
275
+ """
276
+ spec = self.build_query_spec(filter)
277
+
278
+ async with self.sqlite.session() as session:
279
+ stmt = sqlm.select(sa.func.count()).select_from(SegmentTable)
280
+
281
+ for clause in spec.where:
282
+ stmt = stmt.where(clause)
283
+
284
+ result = await session.execute(stmt)
285
+ return result.scalar_one()
286
+
287
+ async def sum_duration_by_session(self, session_id: str) -> int:
288
+ """Sum the duration of all segments in a given session.
289
+
290
+ Args:
291
+ session_id: The ID of the session to sum durations for.
292
+
293
+ Returns:
294
+ The total duration of segments in the session (in milliseconds).
295
+ """
296
+ async with self.sqlite.session() as session:
297
+ stmt = sqlm.select(sa.func.sum(SegmentTable.duration_ms)).where(
298
+ SegmentTable.session_id == session_id
299
+ )
300
+ result = await session.execute(stmt)
301
+ total_duration = result.scalar_one()
302
+ return total_duration if total_duration is not None else 0