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,234 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from exdrf.label_dsl import (
|
|
6
|
+
ParsedIdentifier,
|
|
7
|
+
ParsedLiteral,
|
|
8
|
+
ParsedOp,
|
|
9
|
+
evaluate,
|
|
10
|
+
generate_python_code,
|
|
11
|
+
generate_typescript_code,
|
|
12
|
+
get_used_fields,
|
|
13
|
+
parse_expr,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DummyContext:
|
|
18
|
+
def __init__(self, **kwargs):
|
|
19
|
+
for key, value in kwargs.items():
|
|
20
|
+
setattr(self, key, value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_parse_expr_concat():
|
|
24
|
+
expr = '(concat "Hello" "World")'
|
|
25
|
+
ast = parse_expr(expr)
|
|
26
|
+
# Check that the AST is a list starting with an operator
|
|
27
|
+
assert isinstance(ast, list)
|
|
28
|
+
op = ast[0]
|
|
29
|
+
assert isinstance(op, ParsedOp)
|
|
30
|
+
assert op.value == "concat"
|
|
31
|
+
# Remaining tokens should be literals
|
|
32
|
+
lit1 = ast[1]
|
|
33
|
+
lit2 = ast[2]
|
|
34
|
+
assert isinstance(lit1, ParsedLiteral)
|
|
35
|
+
assert lit1.value == "Hello"
|
|
36
|
+
assert isinstance(lit2, ParsedLiteral)
|
|
37
|
+
assert lit2.value == "World"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_evaluate_concat_literals():
|
|
41
|
+
expr = '(concat "Hello" " " "World")'
|
|
42
|
+
ast = parse_expr(expr)
|
|
43
|
+
# All arguments are literals so context is not used (pass None)
|
|
44
|
+
result = evaluate(ast, None)
|
|
45
|
+
assert result == "Hello World"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_evaluate_identifier_with_upper():
|
|
49
|
+
expr = "(upper name)"
|
|
50
|
+
ast = parse_expr(expr)
|
|
51
|
+
context = DummyContext(name="test")
|
|
52
|
+
result = evaluate(ast, context)
|
|
53
|
+
assert result == "TEST"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_generate_python_code():
|
|
57
|
+
expr = '(concat "foo" "bar")'
|
|
58
|
+
ast = parse_expr(expr)
|
|
59
|
+
code = generate_python_code(ast)
|
|
60
|
+
# The generated code should join the literals with a Python plus operator
|
|
61
|
+
assert " + " in code
|
|
62
|
+
assert '"foo"' in code
|
|
63
|
+
assert '"bar"' in code
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_generate_typescript_code_with_identifier():
|
|
67
|
+
expr = '(concat "foo" bar)'
|
|
68
|
+
ast = parse_expr(expr)
|
|
69
|
+
ts_code = generate_typescript_code(ast)
|
|
70
|
+
assert '("foo" + record.bar)' == ts_code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_get_used_fields():
|
|
74
|
+
expr = "(concat first_name last.name)"
|
|
75
|
+
ast = parse_expr(expr)
|
|
76
|
+
fields = get_used_fields(ast)
|
|
77
|
+
# The current implementation collects ParsedOp values, so it returns the
|
|
78
|
+
# operator.
|
|
79
|
+
# This test checks that behavior.
|
|
80
|
+
assert fields == ["first_name", "last.name"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_nested_expression_evaluate():
|
|
84
|
+
expr = "(concat (upper first_name) (lower last_name))"
|
|
85
|
+
ast = parse_expr(expr)
|
|
86
|
+
context = DummyContext(first_name="john", last_name="DOE")
|
|
87
|
+
result = evaluate(ast, context)
|
|
88
|
+
assert result == "JOHNdoe"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_eq_same_value_same_class():
|
|
92
|
+
op1 = ParsedOp("test")
|
|
93
|
+
op2 = ParsedOp("test")
|
|
94
|
+
assert op1 == op2
|
|
95
|
+
lit1 = ParsedLiteral("hello")
|
|
96
|
+
lit2 = ParsedLiteral("hello")
|
|
97
|
+
assert lit1 == lit2
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_eq_same_value_different_subclasses():
|
|
101
|
+
# Even though they are different subclasses of Parsed,
|
|
102
|
+
# equality compares the 'value' attribute only.
|
|
103
|
+
op = ParsedOp("value")
|
|
104
|
+
lit = ParsedLiteral("value")
|
|
105
|
+
ident = ParsedIdentifier("value")
|
|
106
|
+
assert op == lit
|
|
107
|
+
assert op == ident
|
|
108
|
+
assert lit == ident
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_eq_with_string():
|
|
112
|
+
op = ParsedOp("example")
|
|
113
|
+
lit = ParsedLiteral("example")
|
|
114
|
+
ident = ParsedIdentifier("example")
|
|
115
|
+
assert op == "example"
|
|
116
|
+
assert lit == "example"
|
|
117
|
+
assert ident == "example"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_eq_different_values():
|
|
121
|
+
op = ParsedOp("one")
|
|
122
|
+
lit = ParsedLiteral("two")
|
|
123
|
+
ident = ParsedIdentifier("three")
|
|
124
|
+
assert not (op == lit)
|
|
125
|
+
assert not (lit == ident)
|
|
126
|
+
assert not (op == ident)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_eq_non_parsed_object():
|
|
130
|
+
op = ParsedOp("data")
|
|
131
|
+
# When comparing with an object that is not an instance of Parsed or str,
|
|
132
|
+
# equality should return False.
|
|
133
|
+
assert not (op == 123)
|
|
134
|
+
assert op is not None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_if():
|
|
138
|
+
context = DummyContext(name="test")
|
|
139
|
+
expr = '(if name "Yes" "No")'
|
|
140
|
+
ast = parse_expr(expr)
|
|
141
|
+
result = evaluate(ast, context)
|
|
142
|
+
assert result == "Yes"
|
|
143
|
+
|
|
144
|
+
py_code = generate_python_code(ast)
|
|
145
|
+
assert '("Yes" if record.name else "No")' in py_code
|
|
146
|
+
|
|
147
|
+
ts_code = generate_typescript_code(ast)
|
|
148
|
+
assert '(record.name ? "Yes" : "No")' in ts_code
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_upper():
|
|
152
|
+
context = DummyContext(name="test")
|
|
153
|
+
expr = "(upper name)"
|
|
154
|
+
ast = parse_expr(expr)
|
|
155
|
+
result = evaluate(ast, context)
|
|
156
|
+
assert result == "TEST"
|
|
157
|
+
|
|
158
|
+
py_code = generate_python_code(ast)
|
|
159
|
+
assert "str(record.name).upper()" in py_code
|
|
160
|
+
|
|
161
|
+
ts_code = generate_typescript_code(ast)
|
|
162
|
+
assert "String(record.name).toUpperCase()" in ts_code
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_lower():
|
|
166
|
+
context = DummyContext(name="TEST")
|
|
167
|
+
expr = "(lower name)"
|
|
168
|
+
ast = parse_expr(expr)
|
|
169
|
+
result = evaluate(ast, context)
|
|
170
|
+
assert result == "test"
|
|
171
|
+
|
|
172
|
+
py_code = generate_python_code(ast)
|
|
173
|
+
assert "str(record.name).lower()" in py_code
|
|
174
|
+
|
|
175
|
+
ts_code = generate_typescript_code(ast)
|
|
176
|
+
assert "String(record.name).toLowerCase()" in ts_code
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_is_none():
|
|
180
|
+
context = DummyContext(name=None)
|
|
181
|
+
expr = '(is_none name "Yes" "No")'
|
|
182
|
+
ast = parse_expr(expr)
|
|
183
|
+
result = evaluate(ast, context)
|
|
184
|
+
assert result == "Yes"
|
|
185
|
+
|
|
186
|
+
py_code = generate_python_code(ast)
|
|
187
|
+
assert '("Yes" if record.name is None else "No")' in py_code
|
|
188
|
+
|
|
189
|
+
ts_code = generate_typescript_code(ast)
|
|
190
|
+
assert '(record.name == null || record.name == undefined) ? "Yes" : "No"' in ts_code
|
|
191
|
+
|
|
192
|
+
context = DummyContext(name="test")
|
|
193
|
+
expr = '(is_none name "Yes" "No")'
|
|
194
|
+
ast = parse_expr(expr)
|
|
195
|
+
result = evaluate(ast, context)
|
|
196
|
+
assert result == "No"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_equals():
|
|
200
|
+
context = DummyContext(name="test")
|
|
201
|
+
expr = '(= name "test" "Yes" "No")'
|
|
202
|
+
ast = parse_expr(expr)
|
|
203
|
+
result = evaluate(ast, context)
|
|
204
|
+
assert result == "Yes"
|
|
205
|
+
|
|
206
|
+
py_code = generate_python_code(ast)
|
|
207
|
+
assert '("Yes" if record.name == "test" else "No")' in py_code
|
|
208
|
+
|
|
209
|
+
ts_code = generate_typescript_code(ast)
|
|
210
|
+
assert '((record.name == "test") ? "Yes" : "No")' in ts_code
|
|
211
|
+
|
|
212
|
+
context = DummyContext(name="not")
|
|
213
|
+
expr = '(= name "test" "Yes" "No")'
|
|
214
|
+
ast = parse_expr(expr)
|
|
215
|
+
result = evaluate(ast, context)
|
|
216
|
+
assert result == "No"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_date_str():
|
|
220
|
+
context = DummyContext(date=datetime(2023, 10, 1))
|
|
221
|
+
expr = '(date_str date "%Y-%m-%d")'
|
|
222
|
+
ast = parse_expr(expr)
|
|
223
|
+
result = evaluate(ast, context)
|
|
224
|
+
assert result == "2023-10-01"
|
|
225
|
+
|
|
226
|
+
py_code = generate_python_code(ast)
|
|
227
|
+
assert 'record.date.strftime("%Y-%m-%d")' in py_code
|
|
228
|
+
|
|
229
|
+
ts_code = generate_typescript_code(ast)
|
|
230
|
+
assert 'record.date.strftime("%Y-%m-%d")' in ts_code
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
pytest.main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from exdrf.resource import ExResource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_exresource_initialization():
|
|
9
|
+
resource = ExResource(name="TestResource")
|
|
10
|
+
assert resource.name == "TestResource"
|
|
11
|
+
assert resource.fields == []
|
|
12
|
+
assert resource.categories == []
|
|
13
|
+
assert resource.description == ""
|
|
14
|
+
assert resource.src is None
|
|
15
|
+
assert resource.label_ast is None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_exresource_repr():
|
|
19
|
+
resource = ExResource(name="TestResource")
|
|
20
|
+
assert repr(resource) == "<Resource TestResource (0 fields)>"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_exresource_add_field():
|
|
24
|
+
mock_field = Mock()
|
|
25
|
+
mock_field.name = "test_field"
|
|
26
|
+
mock_field.type_name = "string"
|
|
27
|
+
mock_field.category = None
|
|
28
|
+
|
|
29
|
+
resource = ExResource(name="TestResource")
|
|
30
|
+
resource.add_field(mock_field)
|
|
31
|
+
|
|
32
|
+
assert len(resource.fields) == 1
|
|
33
|
+
assert resource.fields[0] == mock_field
|
|
34
|
+
assert mock_field.resource == resource
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_exresource_getitem_by_index():
|
|
38
|
+
mock_field = Mock()
|
|
39
|
+
mock_field.name = "test_field"
|
|
40
|
+
mock_field.type_name = "string"
|
|
41
|
+
|
|
42
|
+
resource = ExResource(name="TestResource")
|
|
43
|
+
resource.add_field(mock_field)
|
|
44
|
+
|
|
45
|
+
assert resource[0] == mock_field
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_exresource_getitem_by_name():
|
|
49
|
+
mock_field = Mock()
|
|
50
|
+
mock_field.name = "test_field"
|
|
51
|
+
mock_field.type_name = "string"
|
|
52
|
+
|
|
53
|
+
resource = ExResource(name="TestResource")
|
|
54
|
+
resource.add_field(mock_field)
|
|
55
|
+
|
|
56
|
+
assert resource["test_field"] == mock_field
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_exresource_getitem_keyerror():
|
|
60
|
+
resource = ExResource(name="TestResource")
|
|
61
|
+
with pytest.raises(KeyError):
|
|
62
|
+
_ = resource["nonexistent_field"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_exresource_pascal_case_name():
|
|
66
|
+
resource = ExResource(name="TestResource")
|
|
67
|
+
assert resource.pascal_case_name == "TestResource"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_exresource_snake_case_name():
|
|
71
|
+
resource = ExResource(name="TestResource")
|
|
72
|
+
assert resource.snake_case_name == "test_resource"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_exresource_snake_case_name_plural():
|
|
76
|
+
resource = ExResource(name="TestResource")
|
|
77
|
+
assert resource.snake_case_name_plural == "test_resources"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_exresource_camel_case_name():
|
|
81
|
+
resource = ExResource(name="TestResource")
|
|
82
|
+
assert resource.camel_case_name == "testResource"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_exresource_text_name():
|
|
86
|
+
resource = ExResource(name="TestResource")
|
|
87
|
+
assert resource.text_name == "Test resource"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_exresource_doc_lines():
|
|
91
|
+
resource = ExResource(name="TestResource", description="This is a test resource.")
|
|
92
|
+
assert resource.doc_lines == ["This is a test resource."]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_exresource_get_dependencies():
|
|
96
|
+
mock_field = Mock()
|
|
97
|
+
mock_field.is_ref_type = True
|
|
98
|
+
mock_field.ref = Mock()
|
|
99
|
+
mock_field.ref.name = "DependencyResource"
|
|
100
|
+
mock_field.extra_ref = Mock(return_value=[])
|
|
101
|
+
|
|
102
|
+
resource = ExResource(name="TestResource")
|
|
103
|
+
resource.add_field(mock_field)
|
|
104
|
+
|
|
105
|
+
dependencies = resource.get_dependencies()
|
|
106
|
+
assert len(dependencies) == 1
|
|
107
|
+
assert list(dependencies)[0].name == "DependencyResource"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from exdrf.utils import doc_lines
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_doc_lines_single_line():
|
|
5
|
+
text = "This is a single line of text."
|
|
6
|
+
expected = ["This is a single line of text."]
|
|
7
|
+
assert doc_lines(text) == expected
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_doc_lines_multiple_lines():
|
|
11
|
+
text = "This is the first line.\nThis is the second line."
|
|
12
|
+
expected = ["This is the first line.", "", "This is the second line."]
|
|
13
|
+
assert doc_lines(text) == expected
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_doc_lines_wrapping():
|
|
17
|
+
text = (
|
|
18
|
+
"This is a very long line of text that should be wrapped into "
|
|
19
|
+
"multiple lines because it exceeds the width limit."
|
|
20
|
+
)
|
|
21
|
+
expected = [
|
|
22
|
+
"This is a very long line of text that should be wrapped into multiple",
|
|
23
|
+
"lines because it exceeds the width limit.",
|
|
24
|
+
]
|
|
25
|
+
assert doc_lines(text) == expected
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_doc_lines_empty_string():
|
|
29
|
+
text = ""
|
|
30
|
+
expected = []
|
|
31
|
+
assert doc_lines(text) == expected
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_doc_lines_with_leading_and_trailing_whitespace():
|
|
35
|
+
text = " This line has leading and trailing spaces. "
|
|
36
|
+
expected = ["This line has leading and trailing spaces."]
|
|
37
|
+
assert doc_lines(text) == expected
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_doc_lines_with_blank_lines():
|
|
41
|
+
text = "Line one.\n\nLine three."
|
|
42
|
+
expected = ["Line one.", "", "", "Line three."]
|
|
43
|
+
assert doc_lines(text) == expected
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
from exdrf.visitor import ExVisitor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestExVisitor:
|
|
7
|
+
def test_visit_dataset(self):
|
|
8
|
+
visitor = ExVisitor()
|
|
9
|
+
mock_dataset = MagicMock()
|
|
10
|
+
result = visitor.visit_dataset(mock_dataset)
|
|
11
|
+
assert result is True
|
|
12
|
+
|
|
13
|
+
def test_visit_category(self):
|
|
14
|
+
visitor = ExVisitor()
|
|
15
|
+
name = "category_name"
|
|
16
|
+
level = 1
|
|
17
|
+
content = {"sub_category": {}, "resource": {}}
|
|
18
|
+
result = visitor.visit_category(name, level, content)
|
|
19
|
+
assert result is True
|
|
20
|
+
|
|
21
|
+
def test_visit_resource(self):
|
|
22
|
+
visitor = ExVisitor()
|
|
23
|
+
mock_resource = MagicMock()
|
|
24
|
+
result = visitor.visit_resource(mock_resource)
|
|
25
|
+
assert result is True
|
|
26
|
+
|
|
27
|
+
def test_visit_field(self):
|
|
28
|
+
visitor = ExVisitor()
|
|
29
|
+
mock_field = MagicMock()
|
|
30
|
+
result = visitor.visit_field(mock_field)
|
|
31
|
+
assert result is True
|