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
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from typing import List, cast
|
|
2
|
+
|
|
3
|
+
from exdrf.filter import (
|
|
4
|
+
FieldFilter,
|
|
5
|
+
FilterType,
|
|
6
|
+
FilterVisitor,
|
|
7
|
+
LogicAndType,
|
|
8
|
+
LogicNotType,
|
|
9
|
+
LogicOrType,
|
|
10
|
+
SearchType,
|
|
11
|
+
insert_quick_search,
|
|
12
|
+
validate_filter,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FilterVisitorCollector(FilterVisitor):
|
|
17
|
+
"""Test visitor that collects visited items for verification."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, filter: FilterType):
|
|
20
|
+
super().__init__(filter)
|
|
21
|
+
self.visited_and: List[LogicAndType] = []
|
|
22
|
+
self.visited_or: List[LogicOrType] = []
|
|
23
|
+
self.visited_not: List[LogicNotType] = []
|
|
24
|
+
self.visited_field: List[FieldFilter] = []
|
|
25
|
+
|
|
26
|
+
def visit_and(self, filter: LogicAndType):
|
|
27
|
+
self.visited_and.append(filter)
|
|
28
|
+
|
|
29
|
+
def visit_or(self, filter: LogicOrType):
|
|
30
|
+
self.visited_or.append(filter)
|
|
31
|
+
|
|
32
|
+
def visit_not(self, filter: LogicNotType):
|
|
33
|
+
self.visited_not.append(filter)
|
|
34
|
+
|
|
35
|
+
def visit_field(self, filter: FieldFilter):
|
|
36
|
+
self.visited_field.append(filter)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_field_filter_creation():
|
|
40
|
+
"""Test creating a FieldFilter instance."""
|
|
41
|
+
filter = FieldFilter(fld="name", op="eq", vl="test")
|
|
42
|
+
assert filter.fld == "name"
|
|
43
|
+
assert filter.op == "eq"
|
|
44
|
+
assert filter.vl == "test"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_simple_field_filter_visitor() -> None:
|
|
48
|
+
"""Test visiting a simple field filter."""
|
|
49
|
+
filter: FilterType = [FieldFilter(fld="name", op="eq", vl="test")]
|
|
50
|
+
visitor = FilterVisitorCollector(filter)
|
|
51
|
+
visitor.run(filter)
|
|
52
|
+
|
|
53
|
+
assert len(visitor.visited_field) == 1
|
|
54
|
+
assert visitor.visited_field[0].fld == "name"
|
|
55
|
+
assert visitor.visited_field[0].op == "eq"
|
|
56
|
+
assert visitor.visited_field[0].vl == "test"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_and_filter_visitor() -> None:
|
|
60
|
+
"""Test visiting an AND filter."""
|
|
61
|
+
filter: FilterType = cast(
|
|
62
|
+
FilterType,
|
|
63
|
+
[
|
|
64
|
+
"and",
|
|
65
|
+
[
|
|
66
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
67
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
68
|
+
],
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
visitor = FilterVisitorCollector(filter)
|
|
72
|
+
visitor.run(filter)
|
|
73
|
+
|
|
74
|
+
assert len(visitor.visited_and) == 1
|
|
75
|
+
assert len(visitor.visited_field) == 2
|
|
76
|
+
assert visitor.visited_field[0].fld == "id"
|
|
77
|
+
assert visitor.visited_field[1].fld == "name"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_or_filter_visitor() -> None:
|
|
81
|
+
"""Test visiting an OR filter."""
|
|
82
|
+
filter: FilterType = cast(
|
|
83
|
+
FilterType,
|
|
84
|
+
[
|
|
85
|
+
"or",
|
|
86
|
+
[
|
|
87
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
88
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
89
|
+
],
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
visitor = FilterVisitorCollector(filter)
|
|
93
|
+
visitor.run(filter)
|
|
94
|
+
|
|
95
|
+
assert len(visitor.visited_or) == 1
|
|
96
|
+
assert len(visitor.visited_field) == 2
|
|
97
|
+
assert visitor.visited_field[0].fld == "id"
|
|
98
|
+
assert visitor.visited_field[1].fld == "name"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_not_filter_visitor() -> None:
|
|
102
|
+
"""Test visiting a NOT filter."""
|
|
103
|
+
filter: FilterType = cast(FilterType, ["not", FieldFilter(fld="id", op="eq", vl=1)])
|
|
104
|
+
visitor = FilterVisitorCollector(filter)
|
|
105
|
+
visitor.run(filter)
|
|
106
|
+
|
|
107
|
+
assert len(visitor.visited_not) == 1
|
|
108
|
+
assert len(visitor.visited_field) == 1
|
|
109
|
+
assert visitor.visited_field[0].fld == "id"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_nested_filter_visitor() -> None:
|
|
113
|
+
"""Test visiting a nested filter structure."""
|
|
114
|
+
filter: FilterType = cast(
|
|
115
|
+
FilterType,
|
|
116
|
+
[
|
|
117
|
+
"and",
|
|
118
|
+
[
|
|
119
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
120
|
+
[
|
|
121
|
+
"or",
|
|
122
|
+
[
|
|
123
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
124
|
+
["not", FieldFilter(fld="active", op="eq", vl=True)],
|
|
125
|
+
],
|
|
126
|
+
],
|
|
127
|
+
],
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
visitor = FilterVisitorCollector(filter)
|
|
131
|
+
visitor.run(filter)
|
|
132
|
+
|
|
133
|
+
assert len(visitor.visited_and) == 1
|
|
134
|
+
assert len(visitor.visited_or) == 1
|
|
135
|
+
assert len(visitor.visited_not) == 1
|
|
136
|
+
assert len(visitor.visited_field) == 3
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_validate_filter_valid() -> None:
|
|
140
|
+
"""Test validation of valid filters."""
|
|
141
|
+
# Test single field filter
|
|
142
|
+
assert validate_filter([FieldFilter(fld="name", op="eq", vl="test")]) == []
|
|
143
|
+
|
|
144
|
+
# Test AND filter
|
|
145
|
+
and_filter: FilterType = cast(
|
|
146
|
+
FilterType,
|
|
147
|
+
[
|
|
148
|
+
"and",
|
|
149
|
+
[
|
|
150
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
151
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
152
|
+
],
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
assert validate_filter(and_filter) == []
|
|
156
|
+
|
|
157
|
+
# Test OR filter
|
|
158
|
+
or_filter: FilterType = cast(
|
|
159
|
+
FilterType,
|
|
160
|
+
[
|
|
161
|
+
"or",
|
|
162
|
+
[
|
|
163
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
164
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
165
|
+
],
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
assert validate_filter(or_filter) == []
|
|
169
|
+
|
|
170
|
+
# Test NOT filter
|
|
171
|
+
not_filter: FilterType = cast(
|
|
172
|
+
FilterType, ["not", FieldFilter(fld="id", op="eq", vl=1)]
|
|
173
|
+
)
|
|
174
|
+
assert validate_filter(not_filter) == []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_validate_filter_invalid() -> None:
|
|
178
|
+
"""Test validation of invalid filters."""
|
|
179
|
+
# Test invalid field filter
|
|
180
|
+
invalid_filter: FilterType = cast(FilterType, [{"invalid": "field"}])
|
|
181
|
+
assert validate_filter(invalid_filter) == ["invalid_field_filter", "and[0]"]
|
|
182
|
+
|
|
183
|
+
# Test invalid logic operator
|
|
184
|
+
assert validate_filter(cast(FilterType, ["invalid", []])) == [
|
|
185
|
+
"unknown_logic_operator"
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
# Test invalid logic argument
|
|
189
|
+
assert validate_filter(cast(FilterType, ["and", "not_a_list"])) == [
|
|
190
|
+
"logic_arg_not_a_list",
|
|
191
|
+
"and",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
# Test invalid logic group length
|
|
195
|
+
assert validate_filter(cast(FilterType, ["and", []])) == []
|
|
196
|
+
single_filter: FilterType = cast(
|
|
197
|
+
FilterType, ["and", [FieldFilter(fld="id", op="eq", vl=1)]]
|
|
198
|
+
)
|
|
199
|
+
assert validate_filter(single_filter) == []
|
|
200
|
+
|
|
201
|
+
# Test invalid filter type
|
|
202
|
+
assert validate_filter(cast(FilterType, 123)) == ["unknown_filter_type"]
|
|
203
|
+
|
|
204
|
+
# Test None filter
|
|
205
|
+
assert validate_filter(cast(FilterType, None)) == ["none"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_validate_filter_complex() -> None:
|
|
209
|
+
"""Test validation of complex nested filters."""
|
|
210
|
+
complex_filter: FilterType = cast(
|
|
211
|
+
FilterType,
|
|
212
|
+
[
|
|
213
|
+
"and",
|
|
214
|
+
[
|
|
215
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
216
|
+
[
|
|
217
|
+
"or",
|
|
218
|
+
[
|
|
219
|
+
FieldFilter(fld="name", op="eq", vl="test"),
|
|
220
|
+
["not", FieldFilter(fld="active", op="eq", vl=True)],
|
|
221
|
+
],
|
|
222
|
+
],
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
assert validate_filter(complex_filter) == []
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_validate_filter_edge_cases() -> None:
|
|
230
|
+
"""Test validation of edge cases."""
|
|
231
|
+
# Empty list is valid
|
|
232
|
+
assert validate_filter([]) == []
|
|
233
|
+
|
|
234
|
+
# Empty logic group is valid
|
|
235
|
+
assert validate_filter(cast(FilterType, ["and", []])) == []
|
|
236
|
+
assert validate_filter(cast(FilterType, ["or", []])) == []
|
|
237
|
+
|
|
238
|
+
# Single field filter as dict is valid
|
|
239
|
+
dict_filter: FilterType = cast(FilterType, {"fld": "id", "op": "eq", "vl": 1})
|
|
240
|
+
assert validate_filter(dict_filter) == []
|
|
241
|
+
|
|
242
|
+
# Single field filter as FieldFilter instance is valid
|
|
243
|
+
field_filter: FilterType = cast(FilterType, [FieldFilter(fld="id", op="eq", vl=1)])
|
|
244
|
+
assert validate_filter(field_filter) == []
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_insert_quick_search_basic() -> None:
|
|
248
|
+
"""Test basic quick search insertion."""
|
|
249
|
+
# Test with None filter
|
|
250
|
+
result = insert_quick_search("name", "test", search_type=SearchType.EXTENDED)
|
|
251
|
+
assert isinstance(result, list)
|
|
252
|
+
assert len(result) == 1
|
|
253
|
+
assert isinstance(result[0], FieldFilter)
|
|
254
|
+
assert result[0].fld == "name"
|
|
255
|
+
assert result[0].op == "ilike"
|
|
256
|
+
assert result[0].vl == "%test%"
|
|
257
|
+
|
|
258
|
+
# Test with empty list
|
|
259
|
+
result = insert_quick_search(
|
|
260
|
+
"name", "test", cast(FilterType, []), search_type=SearchType.EXTENDED
|
|
261
|
+
)
|
|
262
|
+
assert isinstance(result, list)
|
|
263
|
+
assert len(result) == 1
|
|
264
|
+
assert isinstance(result[0], FieldFilter)
|
|
265
|
+
assert result[0].fld == "name"
|
|
266
|
+
assert result[0].op == "ilike"
|
|
267
|
+
assert result[0].vl == "%test%"
|
|
268
|
+
|
|
269
|
+
# Test with existing filter
|
|
270
|
+
existing_filter = cast(FilterType, [FieldFilter(fld="id", op="eq", vl=1)])
|
|
271
|
+
result = insert_quick_search(
|
|
272
|
+
"name", "test", existing_filter, search_type=SearchType.EXTENDED
|
|
273
|
+
)
|
|
274
|
+
assert isinstance(result, list)
|
|
275
|
+
assert len(result) == 2
|
|
276
|
+
assert existing_filter[0] in result
|
|
277
|
+
assert isinstance(result[0], FieldFilter)
|
|
278
|
+
assert result[0].fld == "name"
|
|
279
|
+
assert result[0].op == "ilike"
|
|
280
|
+
assert result[0].vl == "%test%"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_insert_quick_search_exact() -> None:
|
|
284
|
+
"""Test exact quick search insertion."""
|
|
285
|
+
# Test exact search
|
|
286
|
+
result = insert_quick_search("name", "test", search_type=SearchType.EXACT)
|
|
287
|
+
assert isinstance(result, list)
|
|
288
|
+
assert len(result) == 1
|
|
289
|
+
assert isinstance(result[0], FieldFilter)
|
|
290
|
+
assert result[0].fld == "name"
|
|
291
|
+
assert result[0].op == "eq"
|
|
292
|
+
assert result[0].vl == "test"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_insert_quick_search_wildcards() -> None:
|
|
296
|
+
"""Test quick search with wildcards and spaces."""
|
|
297
|
+
# Test with spaces
|
|
298
|
+
result = insert_quick_search("name", "test value", search_type=SearchType.EXTENDED)
|
|
299
|
+
assert isinstance(result, list)
|
|
300
|
+
assert len(result) == 1
|
|
301
|
+
assert isinstance(result[0], FieldFilter)
|
|
302
|
+
assert result[0].vl == "%test%value%"
|
|
303
|
+
|
|
304
|
+
# Test with asterisks
|
|
305
|
+
result = insert_quick_search("name", "test*value", search_type=SearchType.EXTENDED)
|
|
306
|
+
assert isinstance(result, list)
|
|
307
|
+
assert len(result) == 1
|
|
308
|
+
assert isinstance(result[0], FieldFilter)
|
|
309
|
+
assert result[0].vl == "test%value"
|
|
310
|
+
|
|
311
|
+
# Test with mixed wildcards
|
|
312
|
+
result = insert_quick_search("name", "test* value", search_type=SearchType.EXTENDED)
|
|
313
|
+
assert isinstance(result, list)
|
|
314
|
+
assert len(result) == 1
|
|
315
|
+
assert isinstance(result[0], FieldFilter)
|
|
316
|
+
assert result[0].vl == "test%value"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_insert_quick_search_replace() -> None:
|
|
320
|
+
"""Test replacing existing quick searches."""
|
|
321
|
+
# Test replacing existing quick search
|
|
322
|
+
existing_filter = cast(
|
|
323
|
+
FilterType, [FieldFilter(fld="name", op="ilike", vl="%old%")]
|
|
324
|
+
)
|
|
325
|
+
result = insert_quick_search(
|
|
326
|
+
"name", "new", existing_filter, search_type=SearchType.EXTENDED
|
|
327
|
+
)
|
|
328
|
+
assert isinstance(result, list)
|
|
329
|
+
assert len(result) == 1
|
|
330
|
+
assert isinstance(result[0], FieldFilter)
|
|
331
|
+
assert result[0].vl == "%new%"
|
|
332
|
+
|
|
333
|
+
# Test replacing in AND group
|
|
334
|
+
and_filter = cast(
|
|
335
|
+
FilterType,
|
|
336
|
+
[
|
|
337
|
+
"and",
|
|
338
|
+
[
|
|
339
|
+
FieldFilter(fld="name", op="ilike", vl="%old%"),
|
|
340
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
341
|
+
],
|
|
342
|
+
],
|
|
343
|
+
)
|
|
344
|
+
result = insert_quick_search(
|
|
345
|
+
"name", "new", and_filter, search_type=SearchType.EXTENDED
|
|
346
|
+
)
|
|
347
|
+
assert isinstance(result, list)
|
|
348
|
+
assert len(result) == 2
|
|
349
|
+
assert result[0] == "and"
|
|
350
|
+
assert isinstance(result[1], list)
|
|
351
|
+
assert len(result[1]) == 2
|
|
352
|
+
field_filter = result[1][0]
|
|
353
|
+
assert isinstance(field_filter, FieldFilter)
|
|
354
|
+
assert field_filter.vl == "%new%"
|
|
355
|
+
id_filter = result[1][1]
|
|
356
|
+
assert isinstance(id_filter, FieldFilter)
|
|
357
|
+
assert id_filter.fld == "id"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_insert_quick_search_empty() -> None:
|
|
361
|
+
"""Test quick search with empty values."""
|
|
362
|
+
# Test with empty string
|
|
363
|
+
result = insert_quick_search("name", "")
|
|
364
|
+
assert isinstance(result, list)
|
|
365
|
+
assert len(result) == 0
|
|
366
|
+
|
|
367
|
+
# Test with whitespace only
|
|
368
|
+
result = insert_quick_search("name", " ")
|
|
369
|
+
assert isinstance(result, list)
|
|
370
|
+
assert len(result) == 0
|
|
371
|
+
|
|
372
|
+
# Test with empty string and existing filter
|
|
373
|
+
existing_filter = cast(FilterType, [FieldFilter(fld="id", op="eq", vl=1)])
|
|
374
|
+
result = insert_quick_search("name", "", existing_filter)
|
|
375
|
+
assert result == existing_filter
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_insert_quick_search_edge_cases() -> None:
|
|
379
|
+
"""Test edge cases for quick search insertion."""
|
|
380
|
+
# Test with None value
|
|
381
|
+
result = insert_quick_search("name", None) # type: ignore
|
|
382
|
+
assert isinstance(result, list)
|
|
383
|
+
assert len(result) == 0
|
|
384
|
+
|
|
385
|
+
# Test with existing quick search in dict form
|
|
386
|
+
dict_filter = cast(FilterType, [{"fld": "name", "op": "ilike", "vl": "%old%"}])
|
|
387
|
+
result = insert_quick_search(
|
|
388
|
+
"name", "new", dict_filter, search_type=SearchType.EXTENDED
|
|
389
|
+
)
|
|
390
|
+
assert isinstance(result, list)
|
|
391
|
+
assert len(result) == 1
|
|
392
|
+
assert isinstance(result[0], FieldFilter)
|
|
393
|
+
assert result[0].vl == "%new%"
|
|
394
|
+
|
|
395
|
+
# Test with complex nested filter
|
|
396
|
+
complex_filter = cast(
|
|
397
|
+
FilterType,
|
|
398
|
+
[
|
|
399
|
+
"and",
|
|
400
|
+
[
|
|
401
|
+
FieldFilter(fld="name", op="ilike", vl="%old%"),
|
|
402
|
+
[
|
|
403
|
+
"or",
|
|
404
|
+
[
|
|
405
|
+
FieldFilter(fld="id", op="eq", vl=1),
|
|
406
|
+
FieldFilter(fld="active", op="eq", vl=True),
|
|
407
|
+
],
|
|
408
|
+
],
|
|
409
|
+
],
|
|
410
|
+
],
|
|
411
|
+
)
|
|
412
|
+
result = insert_quick_search(
|
|
413
|
+
"name", "new", complex_filter, search_type=SearchType.EXTENDED
|
|
414
|
+
)
|
|
415
|
+
assert isinstance(result, list)
|
|
416
|
+
assert len(result) == 2
|
|
417
|
+
assert result[0] == "and"
|
|
418
|
+
assert isinstance(result[1], list)
|
|
419
|
+
assert len(result[1]) == 2
|
|
420
|
+
field_filter = result[1][0]
|
|
421
|
+
assert isinstance(field_filter, FieldFilter)
|
|
422
|
+
assert field_filter.vl == "%new%"
|
|
423
|
+
or_group = result[1][1]
|
|
424
|
+
assert isinstance(or_group, list)
|
|
425
|
+
assert or_group[0] == "or"
|