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