exdrf 0.0.1.dev0__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.
- exdrf/__init__.py +0 -0
- exdrf/__version__.py +24 -0
- exdrf/api.py +51 -0
- exdrf/constants.py +30 -0
- exdrf/dataset.py +197 -0
- exdrf/field.py +554 -0
- exdrf/field_types/__init__.py +0 -0
- exdrf/field_types/api.py +78 -0
- exdrf/field_types/blob_field.py +44 -0
- exdrf/field_types/bool_field.py +47 -0
- exdrf/field_types/date_field.py +49 -0
- exdrf/field_types/date_time.py +52 -0
- exdrf/field_types/dur_field.py +44 -0
- exdrf/field_types/enum_field.py +41 -0
- exdrf/field_types/filter_field.py +11 -0
- exdrf/field_types/float_field.py +85 -0
- exdrf/field_types/float_list.py +18 -0
- exdrf/field_types/formatted.py +39 -0
- exdrf/field_types/int_field.py +70 -0
- exdrf/field_types/int_list.py +18 -0
- exdrf/field_types/ref_base.py +105 -0
- exdrf/field_types/ref_m2m.py +39 -0
- exdrf/field_types/ref_m2o.py +23 -0
- exdrf/field_types/ref_o2m.py +36 -0
- exdrf/field_types/ref_o2o.py +32 -0
- exdrf/field_types/sort_field.py +18 -0
- exdrf/field_types/str_field.py +77 -0
- exdrf/field_types/str_list.py +18 -0
- exdrf/field_types/time_field.py +49 -0
- exdrf/filter.py +653 -0
- exdrf/filter_dsl.py +950 -0
- exdrf/filter_op_catalog.py +222 -0
- exdrf/label_dsl.py +691 -0
- exdrf/moment.py +496 -0
- exdrf/py.typed +0 -0
- exdrf/py_support.py +21 -0
- exdrf/resource.py +901 -0
- exdrf/sa_fi_item.py +69 -0
- exdrf/sa_filter_op.py +324 -0
- exdrf/utils.py +17 -0
- exdrf/validator.py +45 -0
- exdrf/var_bag.py +328 -0
- exdrf/visitor.py +58 -0
- exdrf-0.0.1.dev0.dist-info/METADATA +42 -0
- exdrf-0.0.1.dev0.dist-info/RECORD +57 -0
- exdrf-0.0.1.dev0.dist-info/WHEEL +5 -0
- exdrf-0.0.1.dev0.dist-info/top_level.txt +3 -0
- exdrf_tests/__init__.py +0 -0
- exdrf_tests/test_dataset.py +422 -0
- exdrf_tests/test_field.py +109 -0
- exdrf_tests/test_filter.py +425 -0
- exdrf_tests/test_filter_dsl.py +556 -0
- exdrf_tests/test_label_dsl.py +234 -0
- exdrf_tests/test_resource.py +107 -0
- exdrf_tests/test_utils.py +43 -0
- exdrf_tests/test_visitor.py +31 -0
- exdrf_tests/var_bag_test.py +502 -0
exdrf/sa_fi_item.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""SQLAlchemy-oriented filter list items (Qt-free)."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from attrs import define
|
|
6
|
+
|
|
7
|
+
from exdrf.sa_filter_op import FiOp
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sqlalchemy import Select
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@define
|
|
14
|
+
class SqBaseFiItem:
|
|
15
|
+
"""An item in the list of filters.
|
|
16
|
+
|
|
17
|
+
The model simply calls the apply method of the filter with the initial
|
|
18
|
+
selection or the selection after the previous filter.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def apply(self, selection: "Select") -> "Select":
|
|
22
|
+
"""Apply this filter item to the selection.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
selection: The SQLAlchemy select statement to apply the filter to.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A new select statement with the filter applied.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
NotImplementedError: This method must be implemented by subclasses.
|
|
32
|
+
"""
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@define
|
|
37
|
+
class SqFiItem(SqBaseFiItem):
|
|
38
|
+
"""A concrete filter item that applies an operator to a field.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
field: The field (column descriptor) to filter on; often a Qt model
|
|
42
|
+
field or other object whose paired operator implements
|
|
43
|
+
``apply_filter``.
|
|
44
|
+
op: The filter operator to apply.
|
|
45
|
+
value: The value to compare against.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
field: Any
|
|
49
|
+
op: FiOp
|
|
50
|
+
value: Any
|
|
51
|
+
|
|
52
|
+
def apply(self, selection: "Select") -> "Select":
|
|
53
|
+
"""Apply this filter item to the selection.
|
|
54
|
+
|
|
55
|
+
Uses the operator's apply_filter method to apply the filter to the
|
|
56
|
+
given selection.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
selection: The SQLAlchemy select statement to apply the filter to.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A new select statement with the filter applied.
|
|
63
|
+
"""
|
|
64
|
+
return self.op.apply_filter(
|
|
65
|
+
selector=self.field,
|
|
66
|
+
value=self.value,
|
|
67
|
+
selection=selection,
|
|
68
|
+
item=self,
|
|
69
|
+
)
|
exdrf/sa_filter_op.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""SQLAlchemy filter operators shared by Qt and non-Qt code."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
6
|
+
|
|
7
|
+
from attrs import define, field
|
|
8
|
+
from sqlalchemy import String
|
|
9
|
+
from sqlalchemy import cast as al_cast
|
|
10
|
+
from sqlalchemy.sql.operators import (
|
|
11
|
+
_operator_fn,
|
|
12
|
+
comparison_op,
|
|
13
|
+
eq,
|
|
14
|
+
ge,
|
|
15
|
+
gt,
|
|
16
|
+
ilike_op,
|
|
17
|
+
in_op,
|
|
18
|
+
le,
|
|
19
|
+
lt,
|
|
20
|
+
ne,
|
|
21
|
+
regexp_match_op,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from sqlalchemy.sql.selectable import Select
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@define
|
|
31
|
+
class FiOp:
|
|
32
|
+
"""Base class for operators.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
uniq: The name of the operator.
|
|
36
|
+
predicate: The predicate function used by the operator.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
uniq: str
|
|
40
|
+
predicate: Any
|
|
41
|
+
|
|
42
|
+
def apply_filter(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
selector: Any,
|
|
46
|
+
value: Any,
|
|
47
|
+
selection: "Select[Any]",
|
|
48
|
+
item: Any,
|
|
49
|
+
) -> "Select[Any]":
|
|
50
|
+
"""Append ``predicate(selector, value)`` as a WHERE clause.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
selector: Column or SQL expression passed as the predicate's first
|
|
54
|
+
argument (same role as in Qt ``QtField.apply_filter``).
|
|
55
|
+
value: Value passed as the predicate's second argument.
|
|
56
|
+
selection: Existing SELECT to constrain.
|
|
57
|
+
item: The originating filter item (for API symmetry with Qt-side
|
|
58
|
+
call patterns).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
``selection`` with the predicate applied via ``WHERE``.
|
|
62
|
+
"""
|
|
63
|
+
expr = self.predicate(selector, value)
|
|
64
|
+
return selection.where(expr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@define
|
|
68
|
+
class EqFiOp(FiOp):
|
|
69
|
+
"""General equality operator.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
uniq: The name of the operator, set to "eq".
|
|
73
|
+
predicate: The equality predicate function.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
uniq: str = field(default="eq", init=False)
|
|
77
|
+
predicate: Any = field(default=eq)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@define
|
|
81
|
+
class NotEqFiOp(FiOp):
|
|
82
|
+
"""Not equal operator.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
uniq: The name of the operator, set to "not_eq".
|
|
86
|
+
predicate: The not equal predicate function.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
uniq: str = field(default="not_eq", init=False)
|
|
90
|
+
predicate: Any = field(default=ne)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@define
|
|
94
|
+
class ILikeFiOp(FiOp):
|
|
95
|
+
"""Case-insensitive pattern matching operator.
|
|
96
|
+
|
|
97
|
+
The provided text should be found inside the target.
|
|
98
|
+
|
|
99
|
+
Attributes:
|
|
100
|
+
uniq: The name of the operator, set to "ilike".
|
|
101
|
+
predicate: The ILIKE predicate function that handles type
|
|
102
|
+
casting for non-string columns.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
uniq: str = field(default="ilike", init=False)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _predicate(column: Any, value: Any) -> Any:
|
|
109
|
+
"""Return the ILIKE operator for a column and value.
|
|
110
|
+
|
|
111
|
+
Handles type casting for non-string columns to String type.
|
|
112
|
+
Returns False if the column is not a SQLAlchemy column.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
column: The SQLAlchemy column to apply the operator to.
|
|
116
|
+
value: The value to match against.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The ILIKE operator expression or False if column is invalid.
|
|
120
|
+
"""
|
|
121
|
+
# op = filter_op_registry[item.op]
|
|
122
|
+
if hasattr(column, "type"):
|
|
123
|
+
col_type = column.type.__class__.__name__
|
|
124
|
+
if col_type not in ("String",):
|
|
125
|
+
column = al_cast(column, String)
|
|
126
|
+
else:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"ILIKE default implementation only works with "
|
|
129
|
+
"SQLAlchemy columns. Got: %s (type: %s)",
|
|
130
|
+
column,
|
|
131
|
+
type(column),
|
|
132
|
+
)
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
return ilike_op(column, value)
|
|
136
|
+
|
|
137
|
+
predicate: Any = field(default=_predicate)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@define
|
|
141
|
+
class RegexFiOp(FiOp):
|
|
142
|
+
"""Regular expression matching operator.
|
|
143
|
+
|
|
144
|
+
The provided pattern should match the target using regex.
|
|
145
|
+
|
|
146
|
+
The search is case insensitive (i) and we're using the
|
|
147
|
+
multi-line (m) flag (^ matches the start of each line, $ matches the end
|
|
148
|
+
of each line instead of them applying to the whole string).
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
uniq: The name of the operator, set to "regex".
|
|
152
|
+
predicate: The regex match predicate function.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
uniq: str = field(default="regex", init=False)
|
|
156
|
+
predicate: Any = field(default=partial(regexp_match_op, flags="im"))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@comparison_op
|
|
160
|
+
@_operator_fn
|
|
161
|
+
def is_none(a: Any, b: Any) -> Any:
|
|
162
|
+
"""Check if a SQLAlchemy column expression is None.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
a: The column expression to check.
|
|
166
|
+
b: Unused parameter (required by operator interface).
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A SQLAlchemy comparison expression checking if a is None.
|
|
170
|
+
"""
|
|
171
|
+
return a.is_(None)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@define
|
|
175
|
+
class IsNoneFiOp(FiOp):
|
|
176
|
+
"""Operator that checks if the target is None.
|
|
177
|
+
|
|
178
|
+
Attributes:
|
|
179
|
+
uniq: The name of the operator, set to "none".
|
|
180
|
+
predicate: The is None predicate function.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
uniq: str = field(default="none", init=False)
|
|
184
|
+
predicate: Any = field(default=is_none)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@define
|
|
188
|
+
class GreaterFiOp(FiOp):
|
|
189
|
+
"""Greater than operator.
|
|
190
|
+
|
|
191
|
+
The target should be larger than the value.
|
|
192
|
+
|
|
193
|
+
Attributes:
|
|
194
|
+
uniq: The name of the operator, set to "gt".
|
|
195
|
+
predicate: The greater than predicate function.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
uniq: str = field(default="gt", init=False)
|
|
199
|
+
predicate: Any = field(default=gt)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@define
|
|
203
|
+
class SmallerFiOp(FiOp):
|
|
204
|
+
"""Less than operator.
|
|
205
|
+
|
|
206
|
+
The target should be smaller than the value.
|
|
207
|
+
|
|
208
|
+
Attributes:
|
|
209
|
+
uniq: The name of the operator, set to "lt".
|
|
210
|
+
predicate: The less than predicate function.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
uniq: str = field(default="lt", init=False)
|
|
214
|
+
predicate: Any = field(default=lt)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@define
|
|
218
|
+
class GreaterOrEqFiOp(FiOp):
|
|
219
|
+
"""Greater than or equal operator.
|
|
220
|
+
|
|
221
|
+
Attributes:
|
|
222
|
+
uniq: The name of the operator, set to "ge".
|
|
223
|
+
predicate: The greater-or-equal predicate function.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
uniq: str = field(default="ge", init=False)
|
|
227
|
+
predicate: Any = field(default=ge)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@define
|
|
231
|
+
class LessOrEqFiOp(FiOp):
|
|
232
|
+
"""Less than or equal operator.
|
|
233
|
+
|
|
234
|
+
Attributes:
|
|
235
|
+
uniq: The name of the operator, set to "le".
|
|
236
|
+
predicate: The less-or-equal predicate function.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
uniq: str = field(default="le", init=False)
|
|
240
|
+
predicate: Any = field(default=le)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@define
|
|
244
|
+
class InFiOp(FiOp):
|
|
245
|
+
"""In operator for membership testing.
|
|
246
|
+
|
|
247
|
+
The target needs to be one of the values.
|
|
248
|
+
|
|
249
|
+
This can be used when the selector is simple and the `column` method
|
|
250
|
+
can be used to construct the filter. For more complex cases use
|
|
251
|
+
the `InExFiOp` class.
|
|
252
|
+
|
|
253
|
+
Attributes:
|
|
254
|
+
uniq: The name of the operator, set to "in".
|
|
255
|
+
predicate: The in predicate function.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
uniq: str = field(default="in", init=False)
|
|
259
|
+
predicate: Any = field(default=in_op)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@define
|
|
263
|
+
class FiOpRegistry:
|
|
264
|
+
"""Registry for operators.
|
|
265
|
+
|
|
266
|
+
Attributes:
|
|
267
|
+
_registry: The registry of operators.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
_registry: dict[str, FiOp] = field(factory=dict, repr=False)
|
|
271
|
+
|
|
272
|
+
def __attrs_post_init__(self) -> None:
|
|
273
|
+
"""Initialize the registry with all operator instances and aliases.
|
|
274
|
+
|
|
275
|
+
Registers all operator classes and their symbolic aliases (e.g.,
|
|
276
|
+
"==" for "eq", ">" for "gt").
|
|
277
|
+
"""
|
|
278
|
+
self._registry = {
|
|
279
|
+
"eq": EqFiOp(),
|
|
280
|
+
"not_eq": NotEqFiOp(),
|
|
281
|
+
"ilike": ILikeFiOp(),
|
|
282
|
+
"regex": RegexFiOp(),
|
|
283
|
+
"none": IsNoneFiOp(),
|
|
284
|
+
"gt": GreaterFiOp(),
|
|
285
|
+
"lt": SmallerFiOp(),
|
|
286
|
+
"ge": GreaterOrEqFiOp(),
|
|
287
|
+
"le": LessOrEqFiOp(),
|
|
288
|
+
"in": InFiOp(),
|
|
289
|
+
}
|
|
290
|
+
self._registry["=="] = self._registry["eq"]
|
|
291
|
+
self._registry["~="] = self._registry["ilike"]
|
|
292
|
+
self._registry[">"] = self._registry["gt"]
|
|
293
|
+
self._registry["<"] = self._registry["lt"]
|
|
294
|
+
self._registry[">="] = self._registry["ge"]
|
|
295
|
+
self._registry["<="] = self._registry["le"]
|
|
296
|
+
self._registry["!="] = self._registry["not_eq"]
|
|
297
|
+
self._registry["ne"] = self._registry["not_eq"]
|
|
298
|
+
self._registry["gte"] = self._registry["ge"]
|
|
299
|
+
self._registry["lte"] = self._registry["le"]
|
|
300
|
+
|
|
301
|
+
def __getitem__(self, key: str) -> FiOp:
|
|
302
|
+
"""Return the operator by name.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
key: The name of the operator.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
The operator.
|
|
309
|
+
"""
|
|
310
|
+
return self._registry[key]
|
|
311
|
+
|
|
312
|
+
def get(self, key: str) -> Union[FiOp, None]:
|
|
313
|
+
"""Return the operator by name.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
key: The name of the operator.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The operator.
|
|
320
|
+
"""
|
|
321
|
+
return self._registry.get(key, None)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
filter_op_registry = FiOpRegistry()
|
exdrf/utils.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from textwrap import wrap
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
import inflect
|
|
5
|
+
|
|
6
|
+
inflect_e = inflect.engine()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def doc_lines(text: str) -> List[str]:
|
|
10
|
+
"""Get the docstring as a set of lines."""
|
|
11
|
+
docs = (text or "").split("\n")
|
|
12
|
+
result = []
|
|
13
|
+
for i, d in enumerate(docs):
|
|
14
|
+
if i > 0:
|
|
15
|
+
result.append("")
|
|
16
|
+
result.extend(wrap(d.strip(), width=70))
|
|
17
|
+
return result
|
exdrf/validator.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
2
|
+
|
|
3
|
+
from attrs import define, field
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@define
|
|
9
|
+
class ValidationResult(Generic[T]):
|
|
10
|
+
"""Validation result class.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
result: A code indicating the error.
|
|
14
|
+
reason: The error message if validation fails.
|
|
15
|
+
value: The validated value of type T if validation succeeds.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
reason: Optional[str] = field(default=None)
|
|
19
|
+
error: Optional[str] = field(default=None)
|
|
20
|
+
value: Optional[T] = field(default=None)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_valid(self) -> bool:
|
|
24
|
+
"""Check if the validation result is valid."""
|
|
25
|
+
return self.error is None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_invalid(self) -> bool:
|
|
29
|
+
"""Check if the validation result is invalid."""
|
|
30
|
+
return not self.is_valid
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Validator(Generic[T]):
|
|
34
|
+
"""A class that can validate a value."""
|
|
35
|
+
|
|
36
|
+
def validate_value(self, in_value: Any) -> ValidationResult[T]:
|
|
37
|
+
"""Validate the input value.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
in_value: The value to validate.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
ValidationResult: The result of the validation.
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError("Subclasses must implement validate_value method.")
|