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,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."""