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
audex/entity/__init__.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import functools as ft
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
from audex import utils
|
|
8
|
+
from audex.entity.fields import DateTimeField
|
|
9
|
+
from audex.entity.fields import FieldSpec
|
|
10
|
+
from audex.entity.fields import StringField
|
|
11
|
+
from audex.filters import FilterBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EntityMeta(type):
|
|
15
|
+
"""Metaclass for Entity that collects all Field descriptors.
|
|
16
|
+
|
|
17
|
+
This metaclass automatically discovers all Field descriptors defined in the
|
|
18
|
+
class hierarchy and stores them in the _fields class attribute. It processes
|
|
19
|
+
field inheritance by scanning through the method resolution order (MRO) to
|
|
20
|
+
ensure proper field override behavior.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
_fields: Class-level dictionary mapping field names to their FieldSpec
|
|
24
|
+
descriptors.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
class User(Entity):
|
|
29
|
+
username = StringField()
|
|
30
|
+
email = StringField()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Admin(User):
|
|
34
|
+
privileges = StringField(default="admin")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Admin._fields contains: username, email, privileges
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_fields: dict[str, FieldSpec[t.Any]]
|
|
42
|
+
|
|
43
|
+
def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, t.Any]) -> EntityMeta:
|
|
44
|
+
"""Create a new Entity class with field collection.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Name of the class being created.
|
|
48
|
+
bases: Base classes of the new class.
|
|
49
|
+
namespace: Class namespace containing attributes and methods.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
New EntityMeta class with collected fields in _fields attribute.
|
|
53
|
+
"""
|
|
54
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
55
|
+
|
|
56
|
+
# Collect fields from base classes and the current class
|
|
57
|
+
fields: dict[str, FieldSpec[t.Any]] = {}
|
|
58
|
+
|
|
59
|
+
# Helper: scan a class __dict__ for Field instances
|
|
60
|
+
def scan_dict_for_fields(obj: type) -> dict[str, FieldSpec[t.Any]]:
|
|
61
|
+
result: dict[str, FieldSpec[t.Any]] = {}
|
|
62
|
+
for k, v in getattr(obj, "__dict__", {}).items():
|
|
63
|
+
if isinstance(v, FieldSpec):
|
|
64
|
+
result[k] = v
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
# Collect fields from base classes in inheritance order (base classes first).
|
|
68
|
+
# This makes later subclass definitions override earlier ones.
|
|
69
|
+
for base in reversed(cls.__mro__[:-1]): # exclude 'object'
|
|
70
|
+
# If base has _fields prepared by EntityMeta, reuse it (fast, canonical).
|
|
71
|
+
base_fields = getattr(base, "_fields", None)
|
|
72
|
+
if isinstance(base_fields, dict):
|
|
73
|
+
# copy to avoid accidental mutation
|
|
74
|
+
for k, v in base_fields.items():
|
|
75
|
+
fields[k] = v
|
|
76
|
+
else:
|
|
77
|
+
# Fallback: scan base.__dict__ for Field descriptors (covers mixins)
|
|
78
|
+
scanned = scan_dict_for_fields(base)
|
|
79
|
+
for k, v in scanned.items():
|
|
80
|
+
# Only add when not already present (earlier base or subclass will override)
|
|
81
|
+
if k not in fields:
|
|
82
|
+
fields[k] = v
|
|
83
|
+
|
|
84
|
+
# Finally, collect fields declared on this class namespace (these override bases).
|
|
85
|
+
for key, value in namespace.items():
|
|
86
|
+
if isinstance(value, FieldSpec):
|
|
87
|
+
fields[key] = value
|
|
88
|
+
|
|
89
|
+
cls._fields = fields
|
|
90
|
+
return cls
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Entity(metaclass=EntityMeta):
|
|
94
|
+
"""Base entity class with field-based attribute management.
|
|
95
|
+
|
|
96
|
+
This class uses Field descriptors to provide type-safe, validated attributes
|
|
97
|
+
with support for defaults, immutability, and nullable values. All entity
|
|
98
|
+
classes should inherit from this base class to get field management
|
|
99
|
+
capabilities.
|
|
100
|
+
|
|
101
|
+
The Entity class automatically discovers Field descriptors defined in the
|
|
102
|
+
class hierarchy and manages their values through the metaclass. It provides
|
|
103
|
+
serialization, representation, and field introspection capabilities.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
_fields: Class-level dictionary of all Field descriptors collected from
|
|
107
|
+
the class hierarchy.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
```python
|
|
111
|
+
class User(Entity):
|
|
112
|
+
username = StringField()
|
|
113
|
+
email = StringField(nullable=True)
|
|
114
|
+
created_at = DateTimeField(default_factory=utils.utcnow)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
user = User(username="john", email="john@example.com")
|
|
118
|
+
print(
|
|
119
|
+
user.dumps()
|
|
120
|
+
) # {"username": "john", "email": "john@example.com", ...}
|
|
121
|
+
print(
|
|
122
|
+
user
|
|
123
|
+
) # ENTITY <User(username='john', email='john@example.com', ...)>
|
|
124
|
+
```
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Type hints for instance attributes
|
|
128
|
+
if t.TYPE_CHECKING:
|
|
129
|
+
_fields: t.ClassVar[dict[str, FieldSpec[t.Any]]]
|
|
130
|
+
|
|
131
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
132
|
+
"""Initialize an entity with field values.
|
|
133
|
+
|
|
134
|
+
Sets field values from the provided keyword arguments. Only fields
|
|
135
|
+
defined in the class hierarchy are accepted.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
**kwargs: Field names and their values. Field names must match
|
|
139
|
+
those defined in the class hierarchy.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
```python
|
|
143
|
+
user = User(
|
|
144
|
+
username="john",
|
|
145
|
+
email="john@example.com",
|
|
146
|
+
is_active=True,
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
for field_name, _field in self._fields.items():
|
|
151
|
+
if field_name in kwargs:
|
|
152
|
+
setattr(self, field_name, kwargs[field_name])
|
|
153
|
+
|
|
154
|
+
def dumps(self) -> dict[str, t.Any]:
|
|
155
|
+
"""Convert entity to a dictionary.
|
|
156
|
+
|
|
157
|
+
Serializes all field values to a dictionary. Nested Entity instances
|
|
158
|
+
are recursively converted to dictionaries. Only fields that have been
|
|
159
|
+
set (have a value) are included in the output.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A dictionary mapping field names to their values. Nested entities
|
|
163
|
+
are recursively converted to dictionaries.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
```python
|
|
167
|
+
user = User(username="john", email="john@example.com")
|
|
168
|
+
data = user.dumps()
|
|
169
|
+
# {"username": "john", "email": "john@example.com", ...}
|
|
170
|
+
|
|
171
|
+
# With nested entities
|
|
172
|
+
profile = Profile(bio="Developer")
|
|
173
|
+
user = User(username="john", profile=profile)
|
|
174
|
+
data = user.dumps()
|
|
175
|
+
# {"username": "john", "profile": {"bio": "Developer"}, ...}
|
|
176
|
+
```
|
|
177
|
+
"""
|
|
178
|
+
result: dict[str, t.Any] = {}
|
|
179
|
+
for field_name in self._fields:
|
|
180
|
+
if hasattr(self, f"_field_{field_name}"):
|
|
181
|
+
value = getattr(self, field_name)
|
|
182
|
+
if isinstance(value, Entity):
|
|
183
|
+
value = value.dumps()
|
|
184
|
+
result[field_name] = value
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def __repr__(self) -> str:
|
|
188
|
+
"""Generate a string representation of the entity.
|
|
189
|
+
|
|
190
|
+
Creates a detailed string showing the class name and all field values
|
|
191
|
+
that have been set.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A string showing the class name and all field values.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
```python
|
|
198
|
+
user = User(username="john", email="john@example.com")
|
|
199
|
+
print(repr(user))
|
|
200
|
+
# ENTITY <User(username='john', email='john@example.com')>
|
|
201
|
+
```
|
|
202
|
+
"""
|
|
203
|
+
attrs: list[str] = []
|
|
204
|
+
for field_name in self._fields:
|
|
205
|
+
if hasattr(self, f"_field_{field_name}"):
|
|
206
|
+
value = getattr(self, field_name)
|
|
207
|
+
attrs.append(f"{field_name}={value!r}")
|
|
208
|
+
return f"ENTITY <{self.__class__.__name__}({', '.join(attrs)})>"
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def is_field_sortable(cls, field_name: str) -> bool:
|
|
212
|
+
"""Check if a field supports sorting operations.
|
|
213
|
+
|
|
214
|
+
Determines whether a field can be used in sorting operations based on
|
|
215
|
+
its field specification.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
field_name: The name of the field to check.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if the field exists and supports sorting, False otherwise.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
```python
|
|
225
|
+
class User(Entity):
|
|
226
|
+
name = StringField()
|
|
227
|
+
tags = ListField[str](default_factory=list)
|
|
228
|
+
profile = ForeignField[Profile]()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
User.is_field_sortable("name") # True
|
|
232
|
+
User.is_field_sortable("tags") # False
|
|
233
|
+
User.is_field_sortable("profile") # False
|
|
234
|
+
User.is_field_sortable("invalid") # False
|
|
235
|
+
```
|
|
236
|
+
"""
|
|
237
|
+
field = cls._fields.get(field_name)
|
|
238
|
+
if field is None:
|
|
239
|
+
return False
|
|
240
|
+
return field.sortable
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
E = t.TypeVar("E", bound=Entity)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_field_sortable(entity: Entity, field_name: str) -> bool:
|
|
247
|
+
"""Check if a field of an entity instance supports sorting
|
|
248
|
+
operations.
|
|
249
|
+
|
|
250
|
+
Convenience function to check field sortability on an entity instance
|
|
251
|
+
rather than the class.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
entity: The entity instance.
|
|
255
|
+
field_name: The name of the field to check.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if the field exists and supports sorting, False otherwise.
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
```python
|
|
262
|
+
user = User(name="John")
|
|
263
|
+
is_field_sortable(user, "name") # True
|
|
264
|
+
is_field_sortable(user, "tags") # False
|
|
265
|
+
is_field_sortable(user, "invalid") # False
|
|
266
|
+
```
|
|
267
|
+
"""
|
|
268
|
+
return entity.__class__.is_field_sortable(field_name)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class BaseEntity(Entity):
|
|
272
|
+
"""Base entity with ID and timestamp fields.
|
|
273
|
+
|
|
274
|
+
Provides standard fields for entity identification and tracking that are
|
|
275
|
+
commonly needed across all entity types:
|
|
276
|
+
- id: Immutable unique identifier
|
|
277
|
+
- created_at: Immutable creation timestamp
|
|
278
|
+
- updated_at: Mutable last update timestamp
|
|
279
|
+
|
|
280
|
+
This class also provides filtering capabilities and equality/hashing based
|
|
281
|
+
on the entity ID.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
```python
|
|
285
|
+
class User(BaseEntity):
|
|
286
|
+
username = StringField()
|
|
287
|
+
email = StringField()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
user = User(username="john", email="john@example.com")
|
|
291
|
+
print(user.id) # Auto-generated unique ID
|
|
292
|
+
print(user.created_at) # Timestamp when created
|
|
293
|
+
|
|
294
|
+
user.touch() # Updates updated_at timestamp
|
|
295
|
+
print(user.updated_at) # Current timestamp
|
|
296
|
+
```
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# Use TYPE_CHECKING to separate runtime descriptor from type hint
|
|
300
|
+
id: str = StringField(immutable=True, default_factory=utils.gen_id)
|
|
301
|
+
created_at: datetime.datetime = DateTimeField(default_factory=utils.utcnow, immutable=True)
|
|
302
|
+
updated_at: datetime.datetime | None = DateTimeField(nullable=True)
|
|
303
|
+
|
|
304
|
+
def touch(self) -> None:
|
|
305
|
+
"""Update the updated_at timestamp to the current time.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
```python
|
|
309
|
+
user = User(username="john")
|
|
310
|
+
print(user.updated_at) # None
|
|
311
|
+
|
|
312
|
+
user.touch()
|
|
313
|
+
print(user.updated_at) # Current timestamp
|
|
314
|
+
```
|
|
315
|
+
"""
|
|
316
|
+
self.updated_at = utils.utcnow()
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def filter(cls: type[E]) -> FilterBuilder[E]:
|
|
320
|
+
"""Create a type-safe filter builder for this entity.
|
|
321
|
+
|
|
322
|
+
Returns a FilterBuilder instance that provides a fluent interface for
|
|
323
|
+
constructing type-safe filters and queries.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
A FilterBuilder instance for constructing filters.
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
```python
|
|
330
|
+
# Simple filter
|
|
331
|
+
filter1 = User.filter().username.eq("john")
|
|
332
|
+
|
|
333
|
+
# Chained conditions
|
|
334
|
+
filter2 = (
|
|
335
|
+
User.filter()
|
|
336
|
+
.is_active.eq(True)
|
|
337
|
+
.tier.in_([UserTier.PREMIUM, UserTier.VIP])
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Complex filter with sorting
|
|
341
|
+
filter3 = (
|
|
342
|
+
User.filter()
|
|
343
|
+
.created_at.gte(yesterday)
|
|
344
|
+
.username.contains("admin")
|
|
345
|
+
.created_at.desc()
|
|
346
|
+
)
|
|
347
|
+
```
|
|
348
|
+
"""
|
|
349
|
+
return FilterBuilder(cls)
|
|
350
|
+
|
|
351
|
+
def __eq__(self, other: object) -> bool:
|
|
352
|
+
"""Check equality based on entity ID.
|
|
353
|
+
|
|
354
|
+
Two BaseEntity instances are considered equal if they have the same ID,
|
|
355
|
+
regardless of their other field values or class types.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
other: Another object to compare with.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if both entities have the same ID, False otherwise.
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
```python
|
|
365
|
+
user1 = User(id="123", username="john")
|
|
366
|
+
user2 = User(id="123", username="jane")
|
|
367
|
+
user3 = User(id="456", username="john")
|
|
368
|
+
|
|
369
|
+
print(user1 == user2) # True (same ID)
|
|
370
|
+
print(user1 == user3) # False (different ID)
|
|
371
|
+
```
|
|
372
|
+
"""
|
|
373
|
+
if not isinstance(other, BaseEntity):
|
|
374
|
+
return NotImplemented
|
|
375
|
+
return self.id == other.id
|
|
376
|
+
|
|
377
|
+
def __hash__(self) -> int:
|
|
378
|
+
"""Compute hash based on entity ID.
|
|
379
|
+
|
|
380
|
+
Allows BaseEntity instances to be used in sets and as dictionary keys.
|
|
381
|
+
The hash is based solely on the entity ID.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Hash value of the entity ID.
|
|
385
|
+
|
|
386
|
+
Example:
|
|
387
|
+
```python
|
|
388
|
+
user1 = User(id="123", username="john")
|
|
389
|
+
user2 = User(id="123", username="jane")
|
|
390
|
+
|
|
391
|
+
user_set = {user1, user2}
|
|
392
|
+
print(len(user_set)) # 1 (same hash, considered equal)
|
|
393
|
+
|
|
394
|
+
user_dict = {user1: "first", user2: "second"}
|
|
395
|
+
print(user_dict[user1]) # "second" (user2 overwrote user1)
|
|
396
|
+
```
|
|
397
|
+
"""
|
|
398
|
+
return hash(self.id)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
P = t.ParamSpec("P")
|
|
402
|
+
R = t.TypeVar("R")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class Touchable(t.Protocol):
|
|
406
|
+
"""Protocol for entities that can be touched (have their timestamp
|
|
407
|
+
updated).
|
|
408
|
+
|
|
409
|
+
This protocol defines the interface for entities that support
|
|
410
|
+
automatic timestamp updating through the touch() method.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def touch(self) -> None:
|
|
414
|
+
"""Update the entity's timestamp."""
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
TT = t.TypeVar("TT", bound=Touchable)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def touch_after(func: t.Callable[t.Concatenate[TT, P], R]) -> t.Callable[..., R]:
|
|
421
|
+
"""Decorator that automatically calls touch() after method
|
|
422
|
+
execution.
|
|
423
|
+
|
|
424
|
+
This decorator wraps entity methods to automatically update the updated_at
|
|
425
|
+
timestamp after successful execution. The touch() method is only called if
|
|
426
|
+
the wrapped method completes without raising an exception.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
func: The method to wrap. Must be a method of a class that implements
|
|
430
|
+
the Touchable protocol.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
The wrapped method that calls touch() after successful execution.
|
|
434
|
+
|
|
435
|
+
Example:
|
|
436
|
+
```python
|
|
437
|
+
class MyEntity(BaseEntity):
|
|
438
|
+
name = StringField()
|
|
439
|
+
|
|
440
|
+
@touch_after
|
|
441
|
+
def update_name(self, name: str) -> None:
|
|
442
|
+
self.name = name
|
|
443
|
+
# touch() is automatically called after this method
|
|
444
|
+
|
|
445
|
+
@touch_after
|
|
446
|
+
def risky_operation(self) -> str:
|
|
447
|
+
if some_condition:
|
|
448
|
+
raise ValueError("Operation failed")
|
|
449
|
+
return "success"
|
|
450
|
+
# touch() only called if no exception is raised
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
entity = MyEntity(name="old")
|
|
454
|
+
entity.update_name("new") # updated_at is automatically set
|
|
455
|
+
```
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
@ft.wraps(func)
|
|
459
|
+
def wrapper(self: TT, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
460
|
+
try:
|
|
461
|
+
result = func(self, *args, **kwargs)
|
|
462
|
+
except Exception:
|
|
463
|
+
raise
|
|
464
|
+
else:
|
|
465
|
+
self.touch()
|
|
466
|
+
return result
|
|
467
|
+
|
|
468
|
+
return wrapper # type: ignore[return-value]
|
audex/entity/doctor.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from audex import utils
|
|
4
|
+
from audex.entity import BaseEntity
|
|
5
|
+
from audex.entity import touch_after
|
|
6
|
+
from audex.entity.fields import BoolField
|
|
7
|
+
from audex.entity.fields import StringBackedField
|
|
8
|
+
from audex.entity.fields import StringField
|
|
9
|
+
from audex.valueobj.common.auth import HashedPassword
|
|
10
|
+
from audex.valueobj.common.auth import Password
|
|
11
|
+
from audex.valueobj.common.email import Email
|
|
12
|
+
from audex.valueobj.common.phone import CNPhone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Doctor(BaseEntity):
|
|
16
|
+
"""Doctor entity representing a registered doctor account.
|
|
17
|
+
|
|
18
|
+
Represents a doctor who can log in, register voiceprint, create sessions,
|
|
19
|
+
and record conversations. This entity serves as the primary user identity
|
|
20
|
+
for the clinic voice recording system.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
id: The unique identifier of the doctor. Auto-generated with "doctor-"
|
|
24
|
+
prefix for clear categorization.
|
|
25
|
+
eid: Employee/staff ID in the hospital system.
|
|
26
|
+
password_hash: The hashed password for secure authentication.
|
|
27
|
+
name: The doctor's real name for display and records.
|
|
28
|
+
department: Department or specialty. Optional.
|
|
29
|
+
title: Professional title (e.g., "主治医师", "副主任医师"). Optional.
|
|
30
|
+
hospital: Hospital or clinic name. Optional.
|
|
31
|
+
phone: Contact phone number. Optional.
|
|
32
|
+
email: Contact email address. Optional.
|
|
33
|
+
is_active: Indicates whether the doctor account is active. Boolean flag
|
|
34
|
+
controlling login ability, defaults to True.
|
|
35
|
+
|
|
36
|
+
Inherited Attributes:
|
|
37
|
+
created_at: Timestamp when the doctor account was created.
|
|
38
|
+
updated_at: Timestamp when the doctor account was last updated.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
# Create new doctor
|
|
43
|
+
doctor = Doctor(
|
|
44
|
+
eid="EMP001",
|
|
45
|
+
password_hash="hashed_password_here",
|
|
46
|
+
name="张医生",
|
|
47
|
+
department="内科",
|
|
48
|
+
title="主治医师",
|
|
49
|
+
hospital="XX市人民医院",
|
|
50
|
+
phone="13800138000",
|
|
51
|
+
email="zhang@hospital.com",
|
|
52
|
+
is_active=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Activate/deactivate account
|
|
56
|
+
doctor.deactivate()
|
|
57
|
+
doctor.activate()
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
id: str = StringField(default_factory=lambda: utils.gen_id(prefix="doctor-"))
|
|
62
|
+
eid: str = StringField()
|
|
63
|
+
password_hash: HashedPassword = StringBackedField(HashedPassword)
|
|
64
|
+
name: str = StringField()
|
|
65
|
+
department: str | None = StringField(nullable=True)
|
|
66
|
+
title: str | None = StringField(nullable=True)
|
|
67
|
+
hospital: str | None = StringField(nullable=True)
|
|
68
|
+
phone: CNPhone | None = StringField(nullable=True)
|
|
69
|
+
email: Email | None = StringBackedField(Email, nullable=True)
|
|
70
|
+
is_active: bool = BoolField(default=True)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_authenticated(self) -> bool:
|
|
74
|
+
"""Check if the doctor is authenticated and can access the
|
|
75
|
+
system.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the doctor is active, False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
return self.is_active
|
|
81
|
+
|
|
82
|
+
@touch_after
|
|
83
|
+
def activate(self) -> None:
|
|
84
|
+
"""Activate the doctor account by setting is_active to True.
|
|
85
|
+
|
|
86
|
+
Note:
|
|
87
|
+
The updated_at timestamp is automatically updated.
|
|
88
|
+
"""
|
|
89
|
+
self.is_active = True
|
|
90
|
+
|
|
91
|
+
@touch_after
|
|
92
|
+
def deactivate(self) -> None:
|
|
93
|
+
"""Deactivate the doctor account by setting is_active to False.
|
|
94
|
+
|
|
95
|
+
Note:
|
|
96
|
+
The updated_at timestamp is automatically updated.
|
|
97
|
+
"""
|
|
98
|
+
self.is_active = False
|
|
99
|
+
|
|
100
|
+
def verify_password(self, password: Password) -> bool:
|
|
101
|
+
"""Verify a password against the doctor's stored password hash.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
password: The plain text password to verify.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if the password matches the stored hash, False otherwise.
|
|
108
|
+
"""
|
|
109
|
+
return self.password_hash == password
|
audex/entity/doctor.pyi
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# This file is auto-generated by PrototypeX stub generator.
|
|
2
|
+
# Do not edit manually - changes will be overwritten.
|
|
3
|
+
# Regenerate using: python -m scripts.genstubs gen
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
|
|
9
|
+
from audex.entity import BaseEntity
|
|
10
|
+
from audex.valueobj.common.auth import HashedPassword
|
|
11
|
+
from audex.valueobj.common.auth import Password
|
|
12
|
+
from audex.valueobj.common.email import Email
|
|
13
|
+
from audex.valueobj.common.phone import CNPhone
|
|
14
|
+
|
|
15
|
+
class Doctor(BaseEntity):
|
|
16
|
+
id: str
|
|
17
|
+
created_at: datetime.datetime
|
|
18
|
+
updated_at: datetime.datetime | None
|
|
19
|
+
eid: str
|
|
20
|
+
password_hash: HashedPassword
|
|
21
|
+
name: str
|
|
22
|
+
department: str | None
|
|
23
|
+
title: str | None
|
|
24
|
+
hospital: str | None
|
|
25
|
+
phone: CNPhone | None
|
|
26
|
+
email: Email | None
|
|
27
|
+
is_active: bool
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
id: str = ...,
|
|
32
|
+
created_at: datetime.datetime = ...,
|
|
33
|
+
updated_at: datetime.datetime | None = None,
|
|
34
|
+
eid: str,
|
|
35
|
+
password_hash: HashedPassword,
|
|
36
|
+
name: str,
|
|
37
|
+
department: str | None = None,
|
|
38
|
+
title: str | None = None,
|
|
39
|
+
hospital: str | None = None,
|
|
40
|
+
phone: CNPhone | None = None,
|
|
41
|
+
email: Email | None = None,
|
|
42
|
+
is_active: bool = True,
|
|
43
|
+
) -> None: ...
|
|
44
|
+
@property
|
|
45
|
+
def is_authenticated(self) -> bool: ...
|
|
46
|
+
def activate(self) -> None: ...
|
|
47
|
+
def deactivate(self) -> None: ...
|
|
48
|
+
def verify_password(
|
|
49
|
+
self,
|
|
50
|
+
password: Password,
|
|
51
|
+
) -> bool: ...
|