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,692 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from audex.valueobj.common.ops import Op
6
+ from audex.valueobj.common.ops import Order
7
+
8
+ if t.TYPE_CHECKING:
9
+ from audex.entity import Entity
10
+ from audex.entity.fields import FieldSpec
11
+
12
+ E = t.TypeVar("E", bound="Entity")
13
+ T = t.TypeVar("T")
14
+
15
+
16
+ class SortSpec:
17
+ """Sort specification for a single field.
18
+
19
+ Attributes:
20
+ field: The field name to sort by.
21
+ order: The sort order (ASC or DESC).
22
+ """
23
+
24
+ field: str
25
+ order: Order
26
+
27
+ __slots__ = ("field", "order")
28
+
29
+ def __init__(self, field: str, order: Order = Order.ASC) -> None:
30
+ object.__setattr__(self, "field", field)
31
+ object.__setattr__(self, "order", order)
32
+
33
+ def __setattr__(self, key: str, value: t.Any) -> None:
34
+ raise AttributeError("SortSpec instances are immutable")
35
+
36
+ def __eq__(self, other: object) -> bool:
37
+ if not isinstance(other, SortSpec):
38
+ return NotImplemented
39
+ return self.field == other.field and self.order == other.order
40
+
41
+ def __hash__(self) -> int:
42
+ return hash((self.field, self.order))
43
+
44
+ def __repr__(self) -> str:
45
+ return f"SortSpec(field={self.field!r}, order={self.order.value})"
46
+
47
+ def __str__(self) -> str:
48
+ direction = "↑" if self.order == Order.ASC else "↓"
49
+ return f"{self.field} {direction}"
50
+
51
+
52
+ class ConditionSpec:
53
+ """Immutable filter condition.
54
+
55
+ Represents a single filter condition with field name, operation, and
56
+ value(s).
57
+
58
+ Attributes:
59
+ field: The field name the condition applies to.
60
+ op: The operation (from Op enum).
61
+ value: The value to compare against.
62
+ value2: The second value for operations like BETWEEN (optional).
63
+ """
64
+
65
+ field: str
66
+ op: Op
67
+ value: object
68
+ value2: object | None
69
+
70
+ __slots__ = ("field", "op", "value", "value2")
71
+
72
+ def __init__(self, field: str, op: Op, value: object, value2: object | None = None) -> None:
73
+ object.__setattr__(self, "field", field)
74
+ object.__setattr__(self, "op", op)
75
+ object.__setattr__(self, "value", value)
76
+ object.__setattr__(self, "value2", value2)
77
+
78
+ def __setattr__(self, key: str, value: t.Any) -> None:
79
+ raise AttributeError("Condition instances are immutable")
80
+
81
+ def __eq__(self, other: object) -> bool:
82
+ if not isinstance(other, ConditionSpec):
83
+ return NotImplemented
84
+ return (
85
+ self.field == other.field
86
+ and self.op == other.op
87
+ and self.value == other.value
88
+ and self.value2 == other.value2
89
+ )
90
+
91
+ def __hash__(self) -> int:
92
+ return hash((self.field, self.op, self.value, self.value2))
93
+
94
+ def __repr__(self) -> str:
95
+ return (
96
+ f"Condition(field={self.field!r}, op={self.op!r}, "
97
+ f"value={self.value!r}, value2={self.value2!r})"
98
+ )
99
+
100
+ def __str__(self) -> str:
101
+ if self.value2 is not None:
102
+ return f"{self.field} {self.op.name} {self.value}, {self.value2}"
103
+ return f"{self.field} {self.op.name} {self.value}"
104
+
105
+
106
+ class ConditionGroup:
107
+ """Group of conditions with AND/OR logic.
108
+
109
+ Attributes:
110
+ conditions: List of conditions or nested groups.
111
+ operator: "AND" or "OR" - how to combine conditions.
112
+ negated: Whether to negate the entire group.
113
+ """
114
+
115
+ __slots__ = ("conditions", "negated", "operator")
116
+
117
+ def __init__(
118
+ self,
119
+ conditions: list[ConditionSpec | ConditionGroup] | None = None,
120
+ operator: t.Literal["AND", "OR"] = "AND",
121
+ negated: bool = False,
122
+ ) -> None:
123
+ self.conditions: list[ConditionSpec | ConditionGroup] = conditions or []
124
+ self.operator = operator
125
+ self.negated = negated
126
+
127
+ def add(self, condition: ConditionSpec | ConditionGroup) -> None:
128
+ """Add a condition or group to this group."""
129
+ self.conditions.append(condition)
130
+
131
+ def __repr__(self) -> str:
132
+ return f"ConditionGroup(operator={self.operator}, conditions={self.conditions})"
133
+
134
+ def __str__(self) -> str:
135
+ if not self.conditions:
136
+ return "(empty)"
137
+
138
+ inner = f" {self.operator} ".join(
139
+ f"({c})" if isinstance(c, ConditionGroup) else str(c) for c in self.conditions
140
+ )
141
+ s = f"({inner})"
142
+ return f"NOT {s}" if self.negated else s
143
+
144
+
145
+ class Filter:
146
+ """Container for filter conditions with AND/OR support.
147
+
148
+ Attributes:
149
+ _condition_group: Root condition group (can contain nested groups).
150
+ _sorts: List of sort specifications.
151
+ _entity_class: The entity class this filter applies to.
152
+ """
153
+
154
+ __slots__ = ("_builder", "_condition_group", "_entity_class", "_sorts")
155
+
156
+ def __init__(
157
+ self,
158
+ entity_class: type[Entity],
159
+ builder: FilterBuilder[t.Any] | None = None,
160
+ ) -> None:
161
+ self._condition_group = ConditionGroup(operator="AND")
162
+ self._sorts: list[SortSpec] = []
163
+ self._entity_class = entity_class
164
+ self._builder = builder
165
+
166
+ @property
167
+ def condition_group(self) -> ConditionGroup:
168
+ """Get the root condition group."""
169
+ return self._condition_group
170
+
171
+ @property
172
+ def sorts(self) -> list[SortSpec]:
173
+ """Get the list of sort specifications."""
174
+ return self._sorts
175
+
176
+ @property
177
+ def entity_class(self) -> type[Entity]:
178
+ """Get the entity class this filter applies to."""
179
+ return self._entity_class
180
+
181
+ @property
182
+ def conditions(self) -> list[ConditionSpec]:
183
+ """Legacy property for backward compatibility.
184
+
185
+ Returns flat list of conditions from the root AND group.
186
+ Warning: This loses OR grouping information!
187
+ """
188
+ return [c for c in self._condition_group.conditions if isinstance(c, ConditionSpec)]
189
+
190
+ def _add_condition(self, condition: ConditionSpec) -> t.Self:
191
+ """Add a condition to the current filter (AND logic).
192
+
193
+ Args:
194
+ condition: The condition to add.
195
+
196
+ Returns:
197
+ Self for method chaining.
198
+ """
199
+ self._condition_group.add(condition)
200
+ return self
201
+
202
+ def _add_sort(self, sort: SortSpec) -> t.Self:
203
+ """Add a sort specification to the filter.
204
+
205
+ Args:
206
+ sort: The sort specification to add.
207
+
208
+ Returns:
209
+ Self for method chaining.
210
+ """
211
+ self._sorts.append(sort)
212
+ return self
213
+
214
+ def or_(self, *filters: Filter) -> t.Self:
215
+ """Combine conditions with OR logic.
216
+
217
+ Creates a new OR group containing:
218
+ - Current filter's conditions
219
+ - All provided filters' conditions
220
+
221
+ Args:
222
+ *filters: Other filters to combine with OR logic.
223
+
224
+ Returns:
225
+ Self for method chaining.
226
+
227
+ Example:
228
+ ```python
229
+ # Find users with username "john" OR email "john@example.com"
230
+ filter1 = user_filter().username.eq("john")
231
+ filter2 = user_filter().email.eq("john@example.com")
232
+ combined = filter1.or_(filter2)
233
+
234
+ # Or using method chaining:
235
+ combined = (
236
+ user_filter()
237
+ .username.eq("john")
238
+ .or_(user_filter().email.eq("john@example.com"))
239
+ )
240
+ ```
241
+ """
242
+ # Validate same entity type
243
+ for f in filters:
244
+ if self._entity_class != f._entity_class:
245
+ raise ValueError(
246
+ f"Cannot combine filters for different entity types: "
247
+ f"{self._entity_class.__name__} and {f._entity_class.__name__}"
248
+ )
249
+
250
+ # Create new OR group
251
+ or_group = ConditionGroup(operator="OR")
252
+
253
+ # Add current conditions
254
+ if self._condition_group.conditions:
255
+ if len(self._condition_group.conditions) == 1:
256
+ or_group.add(self._condition_group.conditions[0])
257
+ else:
258
+ # Wrap in AND group if multiple conditions
259
+ or_group.add(
260
+ ConditionGroup(
261
+ conditions=self._condition_group.conditions.copy(), operator="AND"
262
+ )
263
+ )
264
+
265
+ # Add other filters' conditions
266
+ for f in filters:
267
+ if f._condition_group.conditions:
268
+ if len(f._condition_group.conditions) == 1:
269
+ or_group.add(f._condition_group.conditions[0])
270
+ else:
271
+ or_group.add(
272
+ ConditionGroup(
273
+ conditions=f._condition_group.conditions.copy(), operator="AND"
274
+ )
275
+ )
276
+
277
+ # Replace root group with OR group
278
+ self._condition_group = or_group
279
+
280
+ # Merge sorts (keep unique)
281
+ for f in filters:
282
+ for sort in f._sorts:
283
+ if sort not in self._sorts:
284
+ self._sorts.append(sort)
285
+
286
+ return self
287
+
288
+ def not_(self) -> Filter:
289
+ """Mark this filter as negated (NOT)."""
290
+ new_filter = Filter(self._entity_class, self._builder)
291
+ new_filter._condition_group = ConditionGroup(
292
+ conditions=self._condition_group.conditions.copy(),
293
+ operator=self._condition_group.operator,
294
+ negated=True,
295
+ )
296
+ new_filter._sorts = self._sorts.copy()
297
+ return new_filter
298
+
299
+ def __getattr__(self, name: str) -> FieldFilter[t.Any]:
300
+ """Allow chaining through the original builder.
301
+
302
+ This enables: filter().field1.eq(x).field2.eq(y)
303
+ """
304
+ if name.startswith("_"):
305
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
306
+
307
+ # Delegate to the builder if available
308
+ if self._builder is not None:
309
+ return getattr(self._builder, name) # type: ignore
310
+
311
+ # Fallback: check if field exists
312
+ if name not in self._entity_class._fields:
313
+ raise AttributeError(f"Entity '{self._entity_class.__name__}' has no field '{name}'")
314
+
315
+ field = self._entity_class._fields[name]
316
+
317
+ from audex.entity.fields import ListFieldSpec as ListField
318
+ from audex.entity.fields import StringBackedFieldSpec as StringBackedField
319
+ from audex.entity.fields import StringFieldSpec as StringField
320
+
321
+ if isinstance(field, StringField):
322
+ return StringFieldFilter(name, self)
323
+ if isinstance(field, StringBackedField):
324
+ return StringBackedFieldFilter(name, self)
325
+ if isinstance(field, ListField):
326
+ return ListFieldFilter(name, self)
327
+
328
+ return FieldFilter(name, self)
329
+
330
+ def __and__(self, other: Filter) -> Filter:
331
+ """Combine two filters with AND logic.
332
+
333
+ Args:
334
+ other: Another filter to combine.
335
+
336
+ Returns:
337
+ A new filter with combined conditions.
338
+
339
+ Raises:
340
+ ValueError: If filters are for different entity types.
341
+ """
342
+ if self._entity_class != other._entity_class:
343
+ raise ValueError(
344
+ f"Cannot combine filters for different entity types: "
345
+ f"{self._entity_class.__name__} and {other._entity_class.__name__}"
346
+ )
347
+
348
+ new_filter = Filter(self._entity_class, self._builder)
349
+
350
+ # Combine condition groups with AND
351
+ new_filter._condition_group = ConditionGroup(operator="AND")
352
+
353
+ # Add self's conditions
354
+ if self._condition_group.conditions:
355
+ if (
356
+ len(self._condition_group.conditions) == 1
357
+ and self._condition_group.operator == "AND"
358
+ ):
359
+ new_filter._condition_group.add(self._condition_group.conditions[0])
360
+ else:
361
+ new_filter._condition_group.add(self._condition_group)
362
+
363
+ # Add other's conditions
364
+ if other._condition_group.conditions:
365
+ if (
366
+ len(other._condition_group.conditions) == 1
367
+ and other._condition_group.operator == "AND"
368
+ ):
369
+ new_filter._condition_group.add(other._condition_group.conditions[0])
370
+ else:
371
+ new_filter._condition_group.add(other._condition_group)
372
+
373
+ # Merge sorts
374
+ new_filter._sorts = self._sorts + other._sorts
375
+
376
+ return new_filter
377
+
378
+ def __or__(self, other: Filter) -> Filter:
379
+ """Combine two filters with OR logic (operator overload).
380
+
381
+ Args:
382
+ other: Another filter to combine.
383
+
384
+ Returns:
385
+ A new filter with OR combination.
386
+
387
+ Example:
388
+ ```python
389
+ filter1 = user_filter().username.eq("john")
390
+ filter2 = user_filter().email.eq("john@example.com")
391
+ combined = filter1 | filter2 # Using | operator
392
+ ```
393
+ """
394
+ if self._entity_class != other._entity_class:
395
+ raise ValueError(
396
+ f"Cannot combine filters for different entity types: "
397
+ f"{self._entity_class.__name__} and {other._entity_class.__name__}"
398
+ )
399
+
400
+ new_filter = Filter(self._entity_class, self._builder)
401
+ new_filter._condition_group = ConditionGroup(operator="OR")
402
+
403
+ # Add self's conditions
404
+ if self._condition_group.conditions:
405
+ if len(self._condition_group.conditions) == 1:
406
+ new_filter._condition_group.add(self._condition_group.conditions[0])
407
+ else:
408
+ new_filter._condition_group.add(
409
+ ConditionGroup(
410
+ conditions=self._condition_group.conditions.copy(),
411
+ operator=self._condition_group.operator,
412
+ )
413
+ )
414
+
415
+ # Add other's conditions
416
+ if other._condition_group.conditions:
417
+ if len(other._condition_group.conditions) == 1:
418
+ new_filter._condition_group.add(other._condition_group.conditions[0])
419
+ else:
420
+ new_filter._condition_group.add(
421
+ ConditionGroup(
422
+ conditions=other._condition_group.conditions.copy(),
423
+ operator=other._condition_group.operator,
424
+ )
425
+ )
426
+
427
+ # Merge sorts
428
+ new_filter._sorts = self._sorts + other._sorts
429
+
430
+ return new_filter
431
+
432
+ def __invert__(self) -> Filter:
433
+ """Negate the filter (NOT).
434
+
435
+ Returns:
436
+ A new filter that is the negation of this filter.
437
+ """
438
+ new_filter = Filter(self._entity_class, self._builder)
439
+ new_filter._condition_group = ConditionGroup(
440
+ conditions=self._condition_group.conditions.copy(),
441
+ operator=self._condition_group.operator,
442
+ negated=True,
443
+ )
444
+ new_filter._sorts = self._sorts.copy()
445
+ return new_filter
446
+
447
+ def __repr__(self) -> str:
448
+ return (
449
+ f"FILTER<{self._entity_class.__name__}>({self._condition_group}, sorts={self._sorts})"
450
+ )
451
+
452
+
453
+ class FieldFilter(t.Generic[T]):
454
+ """Type-safe field filter builder.
455
+
456
+ Provides comparison methods that return the parent Filter for
457
+ chaining.
458
+ """
459
+
460
+ __slots__ = ("_field_name", "_filter")
461
+
462
+ def __init__(self, field_name: str, filter_obj: Filter) -> None:
463
+ self._field_name = field_name
464
+ self._filter = filter_obj
465
+
466
+ def eq(self, value: T) -> Filter:
467
+ """Field equals value."""
468
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.EQ, value))
469
+ return self._filter
470
+
471
+ def ne(self, value: T) -> Filter:
472
+ """Field not equals value."""
473
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.NE, value))
474
+ return self._filter
475
+
476
+ def gt(self, value: T) -> Filter:
477
+ """Field greater than value."""
478
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.GT, value))
479
+ return self._filter
480
+
481
+ def lt(self, value: T) -> Filter:
482
+ """Field less than value."""
483
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.LT, value))
484
+ return self._filter
485
+
486
+ def gte(self, value: T) -> Filter:
487
+ """Field greater than or equal to value."""
488
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.GTE, value))
489
+ return self._filter
490
+
491
+ def lte(self, value: T) -> Filter:
492
+ """Field less than or equal to value."""
493
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.LTE, value))
494
+ return self._filter
495
+
496
+ def in_(self, values: t.Sequence[T]) -> Filter:
497
+ """Field in list of values."""
498
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.IN, values))
499
+ return self._filter
500
+
501
+ def nin(self, values: t.Sequence[T]) -> Filter:
502
+ """Field not in list of values."""
503
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.NIN, values))
504
+ return self._filter
505
+
506
+ def between(self, value1: T, value2: T) -> Filter:
507
+ """Field between two values (inclusive)."""
508
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.BETWEEN, value1, value2))
509
+ return self._filter
510
+
511
+ def is_null(self) -> Filter:
512
+ """Field is NULL/None."""
513
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.EQ, None))
514
+ return self._filter
515
+
516
+ def is_not_null(self) -> Filter:
517
+ """Field is not NULL/None."""
518
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.NE, None))
519
+ return self._filter
520
+
521
+ def asc(self) -> Filter:
522
+ """Sort field in ascending order."""
523
+ self._filter._add_sort(SortSpec(self._field_name, Order.ASC))
524
+ return self._filter
525
+
526
+ def desc(self) -> Filter:
527
+ """Sort field in descending order."""
528
+ self._filter._add_sort(SortSpec(self._field_name, Order.DESC))
529
+ return self._filter
530
+
531
+ def __repr__(self) -> str:
532
+ return f"FieldFilter<{self._field_name}>()"
533
+
534
+
535
+ class StringFieldFilter(FieldFilter[str]):
536
+ """String-specific field filter with additional operations."""
537
+
538
+ def contains(self, value: str) -> Filter:
539
+ """Field contains substring."""
540
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, value))
541
+ return self._filter
542
+
543
+ def startswith(self, value: str) -> Filter:
544
+ """Field starts with substring (uses ^prefix pattern)."""
545
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, f"^{value}"))
546
+ return self._filter
547
+
548
+ def endswith(self, value: str) -> Filter:
549
+ """Field ends with substring (uses suffix$ pattern)."""
550
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, f"{value}$"))
551
+ return self._filter
552
+
553
+
554
+ class StringBackedFieldFilter(FieldFilter[T]):
555
+ """Filter for fields that are persisted as strings but have custom
556
+ types.
557
+
558
+ Supports string operations like contains, startswith, endswith.
559
+ """
560
+
561
+ def contains(self, value: str) -> Filter:
562
+ """Field contains substring."""
563
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, value))
564
+ return self._filter
565
+
566
+ def startswith(self, value: str) -> Filter:
567
+ """Field starts with substring (uses ^prefix pattern)."""
568
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, f"^{value}"))
569
+ return self._filter
570
+
571
+ def endswith(self, value: str) -> Filter:
572
+ """Field ends with substring (uses suffix$ pattern)."""
573
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.CONTAINS, f"{value}$"))
574
+ return self._filter
575
+
576
+
577
+ class ListFieldFilter(FieldFilter[list[T]]):
578
+ """Filter for list/collection fields.
579
+
580
+ Supports contains operation to check if a value exists in the list.
581
+ """
582
+
583
+ def has(self, value: T) -> Filter:
584
+ """Check if list contains value."""
585
+ self._filter._add_condition(ConditionSpec(self._field_name, Op.HAS, value))
586
+ return self._filter
587
+
588
+
589
+ class FilterBuilder(t.Generic[E]):
590
+ """Type-safe filter builder for entities.
591
+
592
+ This class dynamically creates properties for each field in the entity,
593
+ allowing type-safe filter construction with IDE autocomplete.
594
+
595
+ Example:
596
+ ```python
597
+ # Single condition
598
+ filter1 = User.filter().username.eq("john")
599
+
600
+ # Multiple conditions (AND - chained)
601
+ filter2 = (
602
+ User.filter()
603
+ .username.contains("test")
604
+ .is_active.eq(True)
605
+ .tier.in_([UserTier.PREMIUM, UserTier.VIP])
606
+ )
607
+
608
+ # OR combination - Method 1: or_() method
609
+ filter3 = (
610
+ User.filter()
611
+ .username.eq("john")
612
+ .or_(User.filter().email.eq("john@example.com"))
613
+ )
614
+
615
+ # OR combination - Method 2: | operator
616
+ filter4 = User.filter().username.eq(
617
+ "john"
618
+ ) | User.filter().email.eq("john@example.com")
619
+
620
+ # Complex: (username = "john" OR email = "john@ex.com") AND is_active = True
621
+ filter5 = (
622
+ User.filter().username.eq("john")
623
+ | User.filter().email.eq("john@example.com")
624
+ ) & User.filter().is_active.eq(True)
625
+
626
+ # Combining filters with &
627
+ active_filter = User.filter().is_active.eq(True)
628
+ premium_filter = User.filter().tier.eq(UserTier.PREMIUM)
629
+ combined = active_filter & premium_filter
630
+ ```
631
+ """
632
+
633
+ __slots__ = ("_entity_class", "_filter")
634
+
635
+ def __init__(self, entity_class: type[E]) -> None:
636
+ object.__setattr__(self, "_entity_class", entity_class)
637
+ # Pass self to Filter so it can delegate back to us
638
+ filter_obj = Filter(entity_class, builder=self)
639
+ object.__setattr__(self, "_filter", filter_obj)
640
+
641
+ def __getattr__(self, name: str) -> FieldFilter[t.Any]:
642
+ """Dynamically create field filters for entity fields.
643
+
644
+ Args:
645
+ name: The field name to filter on.
646
+
647
+ Returns:
648
+ A FieldFilter for the requested field.
649
+
650
+ Raises:
651
+ AttributeError: If the field doesn't exist in the entity.
652
+ """
653
+ if name.startswith("_"):
654
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
655
+
656
+ entity_class: type[E] = object.__getattribute__(self, "_entity_class")
657
+ if name not in entity_class._fields:
658
+ raise AttributeError(f"Entity '{entity_class.__name__}' has no field '{name}'")
659
+
660
+ field: FieldSpec[t.Any] = entity_class._fields[name]
661
+ filter_obj: Filter = object.__getattribute__(self, "_filter")
662
+
663
+ # Return appropriate filter type based on field type
664
+ from audex.entity.fields import ListFieldSpec as ListField
665
+ from audex.entity.fields import StringBackedFieldSpec as StringBackedField
666
+ from audex.entity.fields import StringFieldSpec as StringField
667
+
668
+ if isinstance(field, StringField):
669
+ return StringFieldFilter(name, filter_obj)
670
+ if isinstance(field, StringBackedField):
671
+ return StringBackedFieldFilter(name, filter_obj)
672
+ if isinstance(field, ListField):
673
+ return ListFieldFilter(name, filter_obj)
674
+
675
+ return FieldFilter(name, filter_obj)
676
+
677
+ def __setattr__(self, name: str, value: t.Any) -> None:
678
+ """Prevent attribute assignment to maintain immutability."""
679
+ raise AttributeError("FilterBuilder attributes cannot be modified")
680
+
681
+ def build(self) -> Filter:
682
+ """Build and return the final Filter object.
683
+
684
+ Returns:
685
+ The constructed Filter with all conditions.
686
+
687
+ Note:
688
+ This method is optional. The Filter is automatically returned
689
+ after each condition method call, so you can use the filter
690
+ directly without calling build().
691
+ """
692
+ return object.__getattribute__(self, "_filter") # type: ignore