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,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
@@ -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: ...