audex 1.0.7a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,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
|