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,140 @@
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.session import Session
9
+ from audex.lib.repos.tables import BaseTable
10
+ from audex.valueobj.session import SessionStatus
11
+
12
+
13
+ class SessionTable(BaseTable[Session], table=True):
14
+ """Session table model for SQLite storage.
15
+
16
+ Maps the Session entity to the database table with all necessary fields
17
+ for tracking conversation sessions, their status, and timing.
18
+
19
+ Table: sessions
20
+ """
21
+
22
+ __tablename__ = "sessions"
23
+
24
+ doctor_id: str = sqlm.Field(
25
+ index=True,
26
+ max_length=50,
27
+ description="Foreign key to doctor who owns this session",
28
+ )
29
+ patient_name: str | None = sqlm.Field(
30
+ default=None,
31
+ nullable=True,
32
+ max_length=100,
33
+ description="Patient name for this session",
34
+ )
35
+ clinic_number: str | None = sqlm.Field(
36
+ default=None,
37
+ nullable=True,
38
+ max_length=50,
39
+ description="Clinic number or ID for this session",
40
+ )
41
+ medical_record_number: str | None = sqlm.Field(
42
+ default=None,
43
+ nullable=True,
44
+ max_length=50,
45
+ description="Medical record number for this session",
46
+ )
47
+ diagnosis: str | None = sqlm.Field(
48
+ default=None,
49
+ nullable=True,
50
+ max_length=255,
51
+ description="Preliminary diagnosis for this session",
52
+ )
53
+ status: str = sqlm.Field(
54
+ default=SessionStatus.DRAFT.value,
55
+ max_length=20,
56
+ index=True,
57
+ description="Session status (draft/in_progress/completed/cancelled)",
58
+ )
59
+ started_at: datetime.datetime | None = sqlm.Field(
60
+ default=None,
61
+ nullable=True,
62
+ description="Timestamp when session first started",
63
+ )
64
+ ended_at: datetime.datetime | None = sqlm.Field(
65
+ default=None,
66
+ nullable=True,
67
+ description="Timestamp when session ended",
68
+ )
69
+ notes: str | None = sqlm.Field(
70
+ default=None,
71
+ nullable=True,
72
+ description="Additional notes about the session",
73
+ )
74
+
75
+ @classmethod
76
+ def from_entity(cls, entity: Session) -> t.Self:
77
+ """Convert Session entity to table model.
78
+
79
+ Args:
80
+ entity: The Session entity to convert.
81
+
82
+ Returns:
83
+ SessionTable instance.
84
+ """
85
+ return cls(
86
+ id=entity.id,
87
+ doctor_id=entity.doctor_id,
88
+ patient_name=entity.patient_name,
89
+ clinic_number=entity.clinic_number,
90
+ medical_record_number=entity.medical_record_number,
91
+ diagnosis=entity.diagnosis,
92
+ status=entity.status.value,
93
+ started_at=entity.started_at,
94
+ ended_at=entity.ended_at,
95
+ notes=entity.notes,
96
+ created_at=entity.created_at,
97
+ updated_at=entity.updated_at,
98
+ )
99
+
100
+ def to_entity(self) -> Session:
101
+ """Convert table model to Session entity.
102
+
103
+ Returns:
104
+ Session entity instance.
105
+ """
106
+ return Session(
107
+ id=self.id,
108
+ doctor_id=self.doctor_id,
109
+ patient_name=self.patient_name,
110
+ clinic_number=self.clinic_number,
111
+ medical_record_number=self.medical_record_number,
112
+ diagnosis=self.diagnosis,
113
+ status=SessionStatus.parse(self.status),
114
+ started_at=self.started_at,
115
+ ended_at=self.ended_at,
116
+ notes=self.notes,
117
+ created_at=self.created_at,
118
+ updated_at=self.updated_at,
119
+ )
120
+
121
+ def update(self, entity: Session) -> None:
122
+ """Update table model fields from Session entity.
123
+
124
+ Args:
125
+ entity: The Session entity with updated data.
126
+ """
127
+ self.doctor_id = entity.doctor_id
128
+ self.patient_name = entity.patient_name
129
+ self.clinic_number = entity.clinic_number
130
+ self.medical_record_number = entity.medical_record_number
131
+ self.diagnosis = entity.diagnosis
132
+ self.status = entity.status.value
133
+ self.started_at = entity.started_at
134
+ self.ended_at = entity.ended_at
135
+ self.notes = entity.notes
136
+ self.updated_at = entity.updated_at
137
+
138
+
139
+ TABLES: set[type[sqlm.SQLModel]] = {SessionTable}
140
+ """Set of all table models for the repository."""
@@ -0,0 +1,131 @@
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.utterance import Utterance
9
+ from audex.lib.repos.tables import BaseTable
10
+ from audex.valueobj.utterance import Speaker
11
+
12
+
13
+ class UtteranceTable(BaseTable[Utterance], table=True):
14
+ """Utterance table model for SQLite storage.
15
+
16
+ Maps the Utterance entity to the database table with all necessary fields
17
+ for tracking speech utterances in conversations.
18
+
19
+ Table: utterances
20
+ """
21
+
22
+ __tablename__ = "utterances"
23
+
24
+ session_id: str = sqlm.Field(
25
+ index=True,
26
+ max_length=50,
27
+ description="Foreign key to session this utterance belongs to",
28
+ )
29
+ segment_id: str = sqlm.Field(
30
+ index=True,
31
+ max_length=50,
32
+ description="Foreign key to segment containing this utterance",
33
+ )
34
+ sequence: int = sqlm.Field(
35
+ nullable=False,
36
+ description="Sequence number within session",
37
+ )
38
+ speaker: str = sqlm.Field(
39
+ max_length=20,
40
+ nullable=False,
41
+ index=True,
42
+ description="Speaker identification (doctor/patient)",
43
+ )
44
+ text: str = sqlm.Field(
45
+ nullable=False,
46
+ description="Transcribed text content",
47
+ )
48
+ confidence: float | None = sqlm.Field(
49
+ default=None,
50
+ nullable=True,
51
+ description="ASR confidence score",
52
+ )
53
+ start_time_ms: int = sqlm.Field(
54
+ nullable=False,
55
+ description="Start time in segment (milliseconds)",
56
+ )
57
+ end_time_ms: int = sqlm.Field(
58
+ nullable=False,
59
+ description="End time in segment (milliseconds)",
60
+ )
61
+ timestamp: datetime.datetime = sqlm.Field(
62
+ nullable=False,
63
+ description="Absolute timestamp of utterance",
64
+ )
65
+
66
+ @classmethod
67
+ def from_entity(cls, entity: Utterance) -> t.Self:
68
+ """Convert Utterance entity to table model.
69
+
70
+ Args:
71
+ entity: The Utterance entity to convert.
72
+
73
+ Returns:
74
+ UtteranceTable instance.
75
+ """
76
+ return cls(
77
+ id=entity.id,
78
+ session_id=entity.session_id,
79
+ segment_id=entity.segment_id,
80
+ sequence=entity.sequence,
81
+ speaker=entity.speaker.value,
82
+ text=entity.text,
83
+ confidence=entity.confidence,
84
+ start_time_ms=entity.start_time_ms,
85
+ end_time_ms=entity.end_time_ms,
86
+ timestamp=entity.timestamp,
87
+ created_at=entity.created_at,
88
+ updated_at=entity.updated_at,
89
+ )
90
+
91
+ def to_entity(self) -> Utterance:
92
+ """Convert table model to Utterance entity.
93
+
94
+ Returns:
95
+ Utterance entity instance.
96
+ """
97
+ return Utterance(
98
+ id=self.id,
99
+ session_id=self.session_id,
100
+ segment_id=self.segment_id,
101
+ sequence=self.sequence,
102
+ speaker=Speaker.parse(self.speaker),
103
+ text=self.text,
104
+ confidence=self.confidence,
105
+ start_time_ms=self.start_time_ms,
106
+ end_time_ms=self.end_time_ms,
107
+ timestamp=self.timestamp,
108
+ created_at=self.created_at,
109
+ updated_at=self.updated_at,
110
+ )
111
+
112
+ def update(self, entity: Utterance) -> None:
113
+ """Update table model fields from Utterance entity.
114
+
115
+ Args:
116
+ entity: The Utterance entity with updated data.
117
+ """
118
+ self.session_id = entity.session_id
119
+ self.segment_id = entity.segment_id
120
+ self.sequence = entity.sequence
121
+ self.speaker = entity.speaker.value
122
+ self.text = entity.text
123
+ self.confidence = entity.confidence
124
+ self.start_time_ms = entity.start_time_ms
125
+ self.end_time_ms = entity.end_time_ms
126
+ self.timestamp = entity.timestamp
127
+ self.updated_at = entity.updated_at
128
+
129
+
130
+ TABLES: set[type[sqlm.SQLModel]] = {UtteranceTable}
131
+ """Set of all table models for the repository."""
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlmodel as sqlm
4
+
5
+ from audex.entity.vp import VP
6
+ from audex.lib.repos.tables import BaseTable
7
+
8
+
9
+ class VPTable(BaseTable[VP], table=True):
10
+ """Voiceprint (VP) table model for SQLite storage.
11
+
12
+ Maps the VP entity to the database table with fields for managing
13
+ voiceprint data associated with doctors.
14
+
15
+ Table: vps
16
+ """
17
+
18
+ __tablename__ = "vps"
19
+
20
+ doctor_id: str = sqlm.Field(
21
+ ...,
22
+ index=True,
23
+ max_length=50,
24
+ description="The ID of the doctor associated with this voiceprint.",
25
+ )
26
+ vpr_uid: str = sqlm.Field(
27
+ ...,
28
+ unique=True,
29
+ index=True,
30
+ max_length=100,
31
+ description="Unique voiceprint recognition UID from the VP service.",
32
+ )
33
+ vpr_group_id: str = sqlm.Field(
34
+ ...,
35
+ index=True,
36
+ max_length=100,
37
+ description="Voiceprint recognition group ID from the VP service.",
38
+ )
39
+ audio_key: str = sqlm.Field(
40
+ ...,
41
+ unique=True,
42
+ index=True,
43
+ max_length=150,
44
+ description="Storage key for the voiceprint audio file.",
45
+ )
46
+ text_content: str = sqlm.Field(
47
+ ...,
48
+ max_length=500,
49
+ description="The text content used for voiceprint enrollment.",
50
+ )
51
+ sample_rate: int = sqlm.Field(
52
+ ...,
53
+ description="Sample rate of the voiceprint audio in Hz.",
54
+ )
55
+ is_active: bool = sqlm.Field(
56
+ default=True,
57
+ description="Indicates whether the voiceprint is active for recognition.",
58
+ )
59
+
60
+ @classmethod
61
+ def from_entity(cls, entity: VP) -> VPTable:
62
+ """Create a VPTable instance from a VP entity."""
63
+ return cls(
64
+ id=entity.id,
65
+ doctor_id=entity.doctor_id,
66
+ vpr_uid=entity.vpr_uid,
67
+ vpr_group_id=entity.vpr_group_id,
68
+ audio_key=entity.audio_key,
69
+ text_content=entity.text_content,
70
+ sample_rate=entity.sample_rate,
71
+ is_active=entity.is_active,
72
+ created_at=entity.created_at,
73
+ updated_at=entity.updated_at,
74
+ )
75
+
76
+ def to_entity(self) -> VP:
77
+ return VP(
78
+ id=self.id,
79
+ doctor_id=self.doctor_id,
80
+ vpr_uid=self.vpr_uid,
81
+ vpr_group_id=self.vpr_group_id,
82
+ audio_key=self.audio_key,
83
+ text_content=self.text_content,
84
+ is_active=self.is_active,
85
+ sample_rate=self.sample_rate,
86
+ created_at=self.created_at,
87
+ updated_at=self.updated_at,
88
+ )
89
+
90
+ def update(self, entity: VP) -> None:
91
+ """Update the VPTable instance with data from a VP entity."""
92
+ self.doctor_id = entity.doctor_id
93
+ self.vpr_uid = entity.vpr_uid
94
+ self.vpr_group_id = entity.vpr_group_id
95
+ self.audio_key = entity.audio_key
96
+ self.text_content = entity.text_content
97
+ self.sample_rate = entity.sample_rate
98
+ self.is_active = entity.is_active
99
+ self.updated_at = entity.updated_at
100
+
101
+
102
+ TABLES: set[type[sqlm.SQLModel]] = {VPTable}
@@ -0,0 +1,288 @@
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.utterance import Utterance
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.utterance import UtteranceTable
14
+
15
+
16
+ class UtteranceRepository(SQLiteRepository[Utterance]):
17
+ """SQLite implementation of Utterance repository.
18
+
19
+ Provides CRUD operations for Utterance entities with additional
20
+ specialized query methods for utterance management by session and
21
+ segment.
22
+ """
23
+
24
+ __table__ = UtteranceTable
25
+ __tablename__ = UtteranceTable.__tablename__
26
+
27
+ def __init__(self, sqlite: SQLite) -> None:
28
+ super().__init__(sqlite)
29
+
30
+ async def create(self, data: Utterance, /) -> str:
31
+ """Create a new utterance in the database.
32
+
33
+ Args:
34
+ data: The utterance entity to create.
35
+
36
+ Returns:
37
+ The ID of the created utterance.
38
+ """
39
+ async with self.sqlite.session() as session:
40
+ utterance_table = UtteranceTable.from_entity(data)
41
+ session.add(utterance_table)
42
+ await session.commit()
43
+ await session.refresh(utterance_table)
44
+ return utterance_table.id
45
+
46
+ async def read(self, id: str, /) -> Utterance | None:
47
+ """Read an utterance by ID.
48
+
49
+ Args:
50
+ id: The ID (id) of the utterance to retrieve.
51
+
52
+ Returns:
53
+ The utterance entity if found, None otherwise.
54
+ """
55
+ async with self.sqlite.session() as session:
56
+ stmt = sqlm.select(UtteranceTable).where(UtteranceTable.id == id)
57
+ result = await session.execute(stmt)
58
+ utterance_obj = result.scalar_one_or_none()
59
+
60
+ if utterance_obj is None:
61
+ return None
62
+
63
+ return utterance_obj.to_entity()
64
+
65
+ async def first(self, filter: Filter) -> Utterance | None:
66
+ """Retrieve the first utterance matching the filter.
67
+
68
+ Args:
69
+ filter: Filter to apply when searching for the utterance.
70
+
71
+ Returns:
72
+ The first utterance 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(UtteranceTable)
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
+ utterance_obj = result.scalar_one_or_none()
89
+
90
+ if utterance_obj is None:
91
+ return None
92
+
93
+ return utterance_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[Utterance]:
102
+ """List utterances 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 utterance 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(UtteranceTable).where(sqlm.col(UtteranceTable.id).in_(arg))
119
+ result = await session.execute(stmt)
120
+ utterance_objs = result.scalars().all()
121
+ return [obj.to_entity() for obj in utterance_objs]
122
+
123
+ spec = self.build_query_spec(arg)
124
+ stmt = sqlm.select(UtteranceTable)
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
+ utterance_objs = result.scalars().all()
136
+ return [obj.to_entity() for obj in utterance_objs]
137
+
138
+ async def update(self, data: Utterance, /) -> str:
139
+ """Update an existing utterance.
140
+
141
+ Args:
142
+ data: The utterance entity with updated values.
143
+
144
+ Returns:
145
+ The ID of the updated utterance.
146
+
147
+ Raises:
148
+ ValueError: If the utterance with the given ID does not exist.
149
+ """
150
+ async with self.sqlite.session() as session:
151
+ stmt = sqlm.select(UtteranceTable).where(UtteranceTable.id == data.id)
152
+ result = await session.execute(stmt)
153
+ utterance_obj = result.scalar_one_or_none()
154
+
155
+ if utterance_obj is None:
156
+ raise ValueError(f"Utterance with id {data.id} not found")
157
+
158
+ utterance_obj.update(data)
159
+ session.add(utterance_obj)
160
+ await session.commit()
161
+ await session.refresh(utterance_obj)
162
+ return utterance_obj.id
163
+
164
+ async def update_many(self, datas: builtins.list[Utterance]) -> builtins.list[str]:
165
+ """Update multiple utterances in the database.
166
+
167
+ Args:
168
+ datas: List of utterance entities with updated values.
169
+
170
+ Returns:
171
+ List of IDs of the updated utterances.
172
+
173
+ Raises:
174
+ ValueError: If any utterance 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(UtteranceTable).where(sqlm.col(UtteranceTable.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"Utterances with IDs {missing_ids} not found")
189
+
190
+ for data in datas:
191
+ utterance_obj = table_objs[data.id]
192
+ utterance_obj.update(data)
193
+ session.add(utterance_obj)
194
+ updated_ids.append(utterance_obj.id)
195
+
196
+ await session.commit()
197
+ return updated_ids
198
+
199
+ async def delete(self, id: str, /) -> bool:
200
+ """Delete an utterance by ID.
201
+
202
+ Args:
203
+ id: The ID (id) of the utterance to delete.
204
+
205
+ Returns:
206
+ True if the utterance was deleted, False if not found.
207
+ """
208
+ async with self.sqlite.session() as session:
209
+ stmt = sqlm.select(UtteranceTable).where(UtteranceTable.id == id)
210
+ result = await session.execute(stmt)
211
+ utterance_obj = result.scalar_one_or_none()
212
+
213
+ if utterance_obj is None:
214
+ return False
215
+
216
+ await session.delete(utterance_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 utterances 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(UtteranceTable).where(sqlm.col(UtteranceTable.id).in_(arg))
239
+ result = await session.execute(stmt)
240
+ utterance_objs = result.scalars().all()
241
+
242
+ if not utterance_objs:
243
+ return []
244
+
245
+ utterance_ids = [obj.id for obj in utterance_objs]
246
+ for obj in utterance_objs:
247
+ await session.delete(obj)
248
+
249
+ await session.commit()
250
+ return utterance_ids
251
+
252
+ spec = self.build_query_spec(arg)
253
+ stmt = sqlm.select(UtteranceTable.id) # type: ignore
254
+ for clause in spec.where:
255
+ stmt = stmt.where(clause)
256
+
257
+ result = await session.execute(stmt)
258
+ utterance_ids = [row[0] for row in result.all()]
259
+
260
+ if not utterance_ids:
261
+ return []
262
+
263
+ delete_stmt = sa.delete(UtteranceTable).where(
264
+ sqlm.col(UtteranceTable.id).in_(utterance_ids)
265
+ )
266
+ await session.execute(delete_stmt)
267
+ await session.commit()
268
+ return utterance_ids
269
+
270
+ async def count(self, filter: t.Optional[Filter] = None) -> int: # noqa
271
+ """Count utterances matching the filter.
272
+
273
+ Args:
274
+ filter: Optional filter to apply. If None, counts all utterances.
275
+
276
+ Returns:
277
+ Number of utterances matching the filter.
278
+ """
279
+ spec = self.build_query_spec(filter)
280
+
281
+ async with self.sqlite.session() as session:
282
+ stmt = sqlm.select(sa.func.count()).select_from(UtteranceTable)
283
+
284
+ for clause in spec.where:
285
+ stmt = stmt.where(clause)
286
+
287
+ result = await session.execute(stmt)
288
+ return result.scalar_one()