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,285 @@
|
|
|
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.session import Session
|
|
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.session import SessionTable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionRepository(SQLiteRepository[Session]):
|
|
17
|
+
"""SQLite implementation of Session repository.
|
|
18
|
+
|
|
19
|
+
Provides CRUD operations for Session entities with additional
|
|
20
|
+
specialized query methods for session management by doctor.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__table__ = SessionTable
|
|
24
|
+
__tablename__ = SessionTable.__tablename__
|
|
25
|
+
|
|
26
|
+
def __init__(self, sqlite: SQLite) -> None:
|
|
27
|
+
super().__init__(sqlite)
|
|
28
|
+
|
|
29
|
+
async def create(self, data: Session, /) -> str:
|
|
30
|
+
"""Create a new session in the database.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data: The session entity to create.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The ID of the created session.
|
|
37
|
+
"""
|
|
38
|
+
async with self.sqlite.session() as session:
|
|
39
|
+
session_table = SessionTable.from_entity(data)
|
|
40
|
+
session.add(session_table)
|
|
41
|
+
await session.commit()
|
|
42
|
+
await session.refresh(session_table)
|
|
43
|
+
return session_table.id
|
|
44
|
+
|
|
45
|
+
async def read(self, id: str, /) -> Session | None:
|
|
46
|
+
"""Read a session by ID.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
id: The ID (id) of the session to retrieve.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The session entity if found, None otherwise.
|
|
53
|
+
"""
|
|
54
|
+
async with self.sqlite.session() as session:
|
|
55
|
+
stmt = sqlm.select(SessionTable).where(SessionTable.id == id)
|
|
56
|
+
result = await session.execute(stmt)
|
|
57
|
+
session_obj = result.scalar_one_or_none()
|
|
58
|
+
|
|
59
|
+
if session_obj is None:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return session_obj.to_entity()
|
|
63
|
+
|
|
64
|
+
async def first(self, filter: Filter) -> Session | None:
|
|
65
|
+
"""Retrieve the first session matching the filter.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
filter: Filter to apply when searching for the session.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The first session 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(SessionTable)
|
|
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
|
+
session_obj = result.scalar_one_or_none()
|
|
88
|
+
|
|
89
|
+
if session_obj is None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return session_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[Session]:
|
|
101
|
+
"""List sessions 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 session 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(SessionTable).where(sqlm.col(SessionTable.id).in_(arg))
|
|
118
|
+
result = await session.execute(stmt)
|
|
119
|
+
session_objs = result.scalars().all()
|
|
120
|
+
return [obj.to_entity() for obj in session_objs]
|
|
121
|
+
|
|
122
|
+
spec = self.build_query_spec(arg)
|
|
123
|
+
stmt = sqlm.select(SessionTable)
|
|
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
|
+
session_objs = result.scalars().all()
|
|
135
|
+
return [obj.to_entity() for obj in session_objs]
|
|
136
|
+
|
|
137
|
+
async def update(self, data: Session, /) -> str:
|
|
138
|
+
"""Update an existing session.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
data: The session entity with updated values.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The ID of the updated session.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If the session with the given ID does not exist.
|
|
148
|
+
"""
|
|
149
|
+
async with self.sqlite.session() as session:
|
|
150
|
+
stmt = sqlm.select(SessionTable).where(SessionTable.id == data.id)
|
|
151
|
+
result = await session.execute(stmt)
|
|
152
|
+
session_obj = result.scalar_one_or_none()
|
|
153
|
+
|
|
154
|
+
if session_obj is None:
|
|
155
|
+
raise ValueError(f"Session with id {data.id} not found")
|
|
156
|
+
|
|
157
|
+
session_obj.update(data)
|
|
158
|
+
session.add(session_obj)
|
|
159
|
+
await session.commit()
|
|
160
|
+
await session.refresh(session_obj)
|
|
161
|
+
return session_obj.id
|
|
162
|
+
|
|
163
|
+
async def update_many(self, datas: builtins.list[Session]) -> builtins.list[str]:
|
|
164
|
+
"""Update multiple sessions in the database.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
datas: List of session entities with updated values.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of IDs of the updated sessions.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: If any session 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(SessionTable).where(sqlm.col(SessionTable.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"Sessions with IDs {missing_ids} not found")
|
|
188
|
+
|
|
189
|
+
for data in datas:
|
|
190
|
+
session_obj = table_objs[data.id]
|
|
191
|
+
session_obj.update(data)
|
|
192
|
+
session.add(session_obj)
|
|
193
|
+
updated_ids.append(session_obj.id)
|
|
194
|
+
|
|
195
|
+
await session.commit()
|
|
196
|
+
return updated_ids
|
|
197
|
+
|
|
198
|
+
async def delete(self, id: str, /) -> bool:
|
|
199
|
+
"""Delete a session by ID.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
id: The ID (id) of the session to delete.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if the session was deleted, False if not found.
|
|
206
|
+
"""
|
|
207
|
+
async with self.sqlite.session() as session:
|
|
208
|
+
stmt = sqlm.select(SessionTable).where(SessionTable.id == id)
|
|
209
|
+
result = await session.execute(stmt)
|
|
210
|
+
session_obj = result.scalar_one_or_none()
|
|
211
|
+
|
|
212
|
+
if session_obj is None:
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
await session.delete(session_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 sessions 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(SessionTable).where(sqlm.col(SessionTable.id).in_(arg))
|
|
238
|
+
result = await session.execute(stmt)
|
|
239
|
+
session_objs = result.scalars().all()
|
|
240
|
+
|
|
241
|
+
if not session_objs:
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
session_ids = [obj.id for obj in session_objs]
|
|
245
|
+
for obj in session_objs:
|
|
246
|
+
await session.delete(obj)
|
|
247
|
+
|
|
248
|
+
await session.commit()
|
|
249
|
+
return session_ids
|
|
250
|
+
|
|
251
|
+
spec = self.build_query_spec(arg)
|
|
252
|
+
stmt = sqlm.select(SessionTable.id) # type: ignore
|
|
253
|
+
for clause in spec.where:
|
|
254
|
+
stmt = stmt.where(clause)
|
|
255
|
+
|
|
256
|
+
result = await session.execute(stmt)
|
|
257
|
+
session_ids = [row[0] for row in result.all()]
|
|
258
|
+
|
|
259
|
+
if not session_ids:
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
delete_stmt = sa.delete(SessionTable).where(sqlm.col(SessionTable.id).in_(session_ids))
|
|
263
|
+
await session.execute(delete_stmt)
|
|
264
|
+
await session.commit()
|
|
265
|
+
return session_ids
|
|
266
|
+
|
|
267
|
+
async def count(self, filter: t.Optional[Filter] = None) -> int: # noqa
|
|
268
|
+
"""Count sessions matching the filter.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
filter: Optional filter to apply. If None, counts all sessions.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Number of sessions 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(SessionTable)
|
|
280
|
+
|
|
281
|
+
for clause in spec.where:
|
|
282
|
+
stmt = stmt.where(clause)
|
|
283
|
+
|
|
284
|
+
result = await session.execute(stmt)
|
|
285
|
+
return result.scalar_one()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import datetime
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
import sqlmodel as sqlm
|
|
8
|
+
|
|
9
|
+
from audex import utils
|
|
10
|
+
from audex.entity import BaseEntity
|
|
11
|
+
|
|
12
|
+
E = t.TypeVar("E", bound=BaseEntity)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseTable(sqlm.SQLModel, abc.ABC, t.Generic[E], table=False):
|
|
16
|
+
"""Base __table__ model with common fields for entities.
|
|
17
|
+
|
|
18
|
+
All entity __table__ models should inherit from this class to ensure
|
|
19
|
+
consistent structure across the database schema.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__tablename__: t.ClassVar[str]
|
|
23
|
+
|
|
24
|
+
sid: int | None = sqlm.Field(
|
|
25
|
+
default=None,
|
|
26
|
+
primary_key=True,
|
|
27
|
+
description="Auto-increment primary key",
|
|
28
|
+
)
|
|
29
|
+
id: str = sqlm.Field(
|
|
30
|
+
index=True,
|
|
31
|
+
unique=True,
|
|
32
|
+
max_length=50,
|
|
33
|
+
default_factory=utils.gen_id,
|
|
34
|
+
description="Business identifier (UUID/ULID)",
|
|
35
|
+
)
|
|
36
|
+
created_at: datetime.datetime = sqlm.Field(
|
|
37
|
+
default_factory=utils.utcnow,
|
|
38
|
+
nullable=False,
|
|
39
|
+
description="Creation timestamp",
|
|
40
|
+
)
|
|
41
|
+
updated_at: datetime.datetime | None = sqlm.Field(
|
|
42
|
+
default=None,
|
|
43
|
+
nullable=True,
|
|
44
|
+
sa_column_kwargs={"onupdate": utils.utcnow},
|
|
45
|
+
description="Last update timestamp",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"TABLE <{self.__class__.__name__}(uid={self.id!r})>"
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def from_entity(cls, entity: E) -> t.Self: ...
|
|
54
|
+
|
|
55
|
+
@abc.abstractmethod
|
|
56
|
+
def to_entity(self) -> E: ...
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def update(self, entity: E) -> None: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
from audex.lib.repos.tables import doctor # noqa: E402
|
|
63
|
+
from audex.lib.repos.tables import segment # noqa: E402
|
|
64
|
+
from audex.lib.repos.tables import session # noqa: E402
|
|
65
|
+
from audex.lib.repos.tables import utterance # noqa: E402
|
|
66
|
+
from audex.lib.repos.tables import vp # noqa: E402
|
|
67
|
+
|
|
68
|
+
TABLES: set[type[sqlm.SQLModel]] = (
|
|
69
|
+
doctor.TABLES | segment.TABLES | session.TABLES | utterance.TABLES | vp.TABLES
|
|
70
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import sqlmodel as sqlm
|
|
6
|
+
|
|
7
|
+
from audex.entity.doctor import Doctor
|
|
8
|
+
from audex.lib.repos.tables import BaseTable
|
|
9
|
+
from audex.valueobj.common.auth import HashedPassword
|
|
10
|
+
from audex.valueobj.common.email import Email
|
|
11
|
+
from audex.valueobj.common.phone import CNPhone
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DoctorTable(BaseTable[Doctor], table=True):
|
|
15
|
+
"""Doctor table model for SQLite storage.
|
|
16
|
+
|
|
17
|
+
Maps the Doctor entity to the database table with all necessary fields
|
|
18
|
+
for authentication, voiceprint management, and account status.
|
|
19
|
+
|
|
20
|
+
Table: doctors
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__tablename__ = "doctors"
|
|
24
|
+
|
|
25
|
+
eid: str = sqlm.Field(
|
|
26
|
+
...,
|
|
27
|
+
unique=True,
|
|
28
|
+
index=True,
|
|
29
|
+
max_length=100,
|
|
30
|
+
description="Employee/staff ID in the hospital system.",
|
|
31
|
+
)
|
|
32
|
+
password_hash: str = sqlm.Field(
|
|
33
|
+
...,
|
|
34
|
+
max_length=255,
|
|
35
|
+
description="The hashed password for secure authentication.",
|
|
36
|
+
)
|
|
37
|
+
name: str = sqlm.Field(
|
|
38
|
+
...,
|
|
39
|
+
max_length=100,
|
|
40
|
+
description="The doctor's real name for display and records.",
|
|
41
|
+
)
|
|
42
|
+
department: str | None = sqlm.Field(
|
|
43
|
+
default=None,
|
|
44
|
+
nullable=True,
|
|
45
|
+
max_length=100,
|
|
46
|
+
description="Department or specialty. Optional.",
|
|
47
|
+
)
|
|
48
|
+
title: str | None = sqlm.Field(
|
|
49
|
+
default=None,
|
|
50
|
+
nullable=True,
|
|
51
|
+
max_length=100,
|
|
52
|
+
description="Professional title (e.g., Attending, Resident). Optional.",
|
|
53
|
+
)
|
|
54
|
+
hospital: str | None = sqlm.Field(
|
|
55
|
+
default=None,
|
|
56
|
+
nullable=True,
|
|
57
|
+
max_length=150,
|
|
58
|
+
description="Affiliated hospital name. Optional.",
|
|
59
|
+
)
|
|
60
|
+
phone: str | None = sqlm.Field(
|
|
61
|
+
default=None,
|
|
62
|
+
nullable=True,
|
|
63
|
+
max_length=20,
|
|
64
|
+
description="Contact phone number. Optional.",
|
|
65
|
+
)
|
|
66
|
+
email: str | None = sqlm.Field(
|
|
67
|
+
default=None,
|
|
68
|
+
nullable=True,
|
|
69
|
+
max_length=100,
|
|
70
|
+
description="Contact email address. Optional.",
|
|
71
|
+
)
|
|
72
|
+
is_active: bool = sqlm.Field(
|
|
73
|
+
default=True,
|
|
74
|
+
description="Indicates if the doctor's account is active.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_entity(cls, entity: Doctor) -> t.Self:
|
|
79
|
+
"""Convert Doctor entity to table model.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
entity: The Doctor entity to convert.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
DoctorTable instance.
|
|
86
|
+
"""
|
|
87
|
+
return cls(
|
|
88
|
+
id=entity.id,
|
|
89
|
+
eid=entity.eid,
|
|
90
|
+
password_hash=entity.password_hash.value,
|
|
91
|
+
name=entity.name,
|
|
92
|
+
department=entity.department,
|
|
93
|
+
title=entity.title,
|
|
94
|
+
hospital=entity.hospital,
|
|
95
|
+
phone=entity.phone.value if entity.phone else None,
|
|
96
|
+
email=entity.email.value if entity.email else None,
|
|
97
|
+
is_active=entity.is_active,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_entity(self) -> Doctor:
|
|
101
|
+
"""Convert table model to Doctor entity.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Doctor entity instance.
|
|
105
|
+
"""
|
|
106
|
+
return Doctor(
|
|
107
|
+
id=self.id,
|
|
108
|
+
eid=self.eid,
|
|
109
|
+
password_hash=HashedPassword.parse(self.password_hash, validate=False),
|
|
110
|
+
name=self.name,
|
|
111
|
+
department=self.department,
|
|
112
|
+
title=self.title,
|
|
113
|
+
hospital=self.hospital,
|
|
114
|
+
phone=CNPhone.parse(self.phone) if self.phone else None,
|
|
115
|
+
email=Email.parse(self.email, validate=False) if self.email else None,
|
|
116
|
+
is_active=self.is_active,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def update(self, entity: Doctor) -> None:
|
|
120
|
+
"""Update table model fields from Doctor entity.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
entity: The Doctor entity with updated data.
|
|
124
|
+
"""
|
|
125
|
+
self.eid = entity.eid
|
|
126
|
+
self.password_hash = entity.password_hash.value
|
|
127
|
+
self.name = entity.name
|
|
128
|
+
self.department = entity.department
|
|
129
|
+
self.title = entity.title
|
|
130
|
+
self.hospital = entity.hospital
|
|
131
|
+
self.phone = entity.phone.value if entity.phone else None
|
|
132
|
+
self.email = entity.email.value if entity.email else None
|
|
133
|
+
self.is_active = entity.is_active
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
TABLES: set[type[sqlm.SQLModel]] = {DoctorTable}
|
|
137
|
+
"""Set of all table models for the repository."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import sqlmodel as sqlm
|
|
7
|
+
|
|
8
|
+
from audex.entity.segment import Segment
|
|
9
|
+
from audex.lib.repos.tables import BaseTable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SegmentTable(BaseTable[Segment], table=True):
|
|
13
|
+
"""Segment table model for SQLite storage.
|
|
14
|
+
|
|
15
|
+
Maps the Segment entity to the database table with all necessary fields
|
|
16
|
+
for tracking audio recording segments within sessions.
|
|
17
|
+
|
|
18
|
+
Table: segments
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__tablename__ = "segments"
|
|
22
|
+
|
|
23
|
+
session_id: str = sqlm.Field(
|
|
24
|
+
index=True,
|
|
25
|
+
max_length=50,
|
|
26
|
+
description="Foreign key to session this segment belongs to",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sequence: int = sqlm.Field(
|
|
30
|
+
nullable=False,
|
|
31
|
+
description="Sequence number within session",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
audio_key: str = sqlm.Field(
|
|
35
|
+
max_length=500,
|
|
36
|
+
nullable=False,
|
|
37
|
+
description="Audio file key/path in storage",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
started_at: datetime.datetime = sqlm.Field(
|
|
41
|
+
nullable=False,
|
|
42
|
+
description="Timestamp when segment started recording",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ended_at: datetime.datetime | None = sqlm.Field(
|
|
46
|
+
default=None,
|
|
47
|
+
nullable=True,
|
|
48
|
+
description="Timestamp when segment stopped recording",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
duration_ms: int | None = sqlm.Field(
|
|
52
|
+
default=None,
|
|
53
|
+
nullable=True,
|
|
54
|
+
description="Duration of segment in milliseconds",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_entity(cls, entity: Segment) -> t.Self:
|
|
59
|
+
"""Convert Segment entity to table model.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
entity: The Segment entity to convert.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
SegmentTable instance.
|
|
66
|
+
"""
|
|
67
|
+
return cls(
|
|
68
|
+
id=entity.id,
|
|
69
|
+
session_id=entity.session_id,
|
|
70
|
+
sequence=entity.sequence,
|
|
71
|
+
audio_key=entity.audio_key,
|
|
72
|
+
started_at=entity.started_at,
|
|
73
|
+
ended_at=entity.ended_at,
|
|
74
|
+
duration_ms=entity.duration_ms,
|
|
75
|
+
created_at=entity.created_at,
|
|
76
|
+
updated_at=entity.updated_at,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def to_entity(self) -> Segment:
|
|
80
|
+
"""Convert table model to Segment entity.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Segment entity instance.
|
|
84
|
+
"""
|
|
85
|
+
return Segment(
|
|
86
|
+
id=self.id,
|
|
87
|
+
session_id=self.session_id,
|
|
88
|
+
sequence=self.sequence,
|
|
89
|
+
audio_key=self.audio_key,
|
|
90
|
+
started_at=self.started_at,
|
|
91
|
+
ended_at=self.ended_at,
|
|
92
|
+
duration_ms=self.duration_ms,
|
|
93
|
+
created_at=self.created_at,
|
|
94
|
+
updated_at=self.updated_at,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def update(self, entity: Segment) -> None:
|
|
98
|
+
"""Update table model fields from Segment entity.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
entity: The Segment entity with updated data.
|
|
102
|
+
"""
|
|
103
|
+
self.session_id = entity.session_id
|
|
104
|
+
self.sequence = entity.sequence
|
|
105
|
+
self.audio_key = entity.audio_key
|
|
106
|
+
self.started_at = entity.started_at
|
|
107
|
+
self.ended_at = entity.ended_at
|
|
108
|
+
self.duration_ms = entity.duration_ms
|
|
109
|
+
self.updated_at = entity.updated_at
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
TABLES: set[type[sqlm.SQLModel]] = {SegmentTable}
|
|
113
|
+
"""Set of all table models for the repository."""
|