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.
Files changed (57) hide show
  1. exdrf/__init__.py +0 -0
  2. exdrf/__version__.py +24 -0
  3. exdrf/api.py +51 -0
  4. exdrf/constants.py +30 -0
  5. exdrf/dataset.py +197 -0
  6. exdrf/field.py +554 -0
  7. exdrf/field_types/__init__.py +0 -0
  8. exdrf/field_types/api.py +78 -0
  9. exdrf/field_types/blob_field.py +44 -0
  10. exdrf/field_types/bool_field.py +47 -0
  11. exdrf/field_types/date_field.py +49 -0
  12. exdrf/field_types/date_time.py +52 -0
  13. exdrf/field_types/dur_field.py +44 -0
  14. exdrf/field_types/enum_field.py +41 -0
  15. exdrf/field_types/filter_field.py +11 -0
  16. exdrf/field_types/float_field.py +85 -0
  17. exdrf/field_types/float_list.py +18 -0
  18. exdrf/field_types/formatted.py +39 -0
  19. exdrf/field_types/int_field.py +70 -0
  20. exdrf/field_types/int_list.py +18 -0
  21. exdrf/field_types/ref_base.py +105 -0
  22. exdrf/field_types/ref_m2m.py +39 -0
  23. exdrf/field_types/ref_m2o.py +23 -0
  24. exdrf/field_types/ref_o2m.py +36 -0
  25. exdrf/field_types/ref_o2o.py +32 -0
  26. exdrf/field_types/sort_field.py +18 -0
  27. exdrf/field_types/str_field.py +77 -0
  28. exdrf/field_types/str_list.py +18 -0
  29. exdrf/field_types/time_field.py +49 -0
  30. exdrf/filter.py +653 -0
  31. exdrf/filter_dsl.py +950 -0
  32. exdrf/filter_op_catalog.py +222 -0
  33. exdrf/label_dsl.py +691 -0
  34. exdrf/moment.py +496 -0
  35. exdrf/py.typed +0 -0
  36. exdrf/py_support.py +21 -0
  37. exdrf/resource.py +901 -0
  38. exdrf/sa_fi_item.py +69 -0
  39. exdrf/sa_filter_op.py +324 -0
  40. exdrf/utils.py +17 -0
  41. exdrf/validator.py +45 -0
  42. exdrf/var_bag.py +328 -0
  43. exdrf/visitor.py +58 -0
  44. exdrf-0.0.1.dev0.dist-info/METADATA +42 -0
  45. exdrf-0.0.1.dev0.dist-info/RECORD +57 -0
  46. exdrf-0.0.1.dev0.dist-info/WHEEL +5 -0
  47. exdrf-0.0.1.dev0.dist-info/top_level.txt +3 -0
  48. exdrf_tests/__init__.py +0 -0
  49. exdrf_tests/test_dataset.py +422 -0
  50. exdrf_tests/test_field.py +109 -0
  51. exdrf_tests/test_filter.py +425 -0
  52. exdrf_tests/test_filter_dsl.py +556 -0
  53. exdrf_tests/test_label_dsl.py +234 -0
  54. exdrf_tests/test_resource.py +107 -0
  55. exdrf_tests/test_utils.py +43 -0
  56. exdrf_tests/test_visitor.py +31 -0
  57. 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.")