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
@@ -0,0 +1,422 @@
1
+ import pytest
2
+
3
+ from exdrf.dataset import ExDataset
4
+
5
+
6
+ def test_exdataset_hash():
7
+ # Create two datasets with the same name
8
+ dataset1 = ExDataset(name="TestDataset")
9
+ dataset2 = ExDataset(name="TestDataset")
10
+
11
+ # Create a dataset with a different name
12
+ dataset3 = ExDataset(name="AnotherDataset")
13
+
14
+ # Assert that datasets with the same name have the same hash
15
+ assert hash(dataset1) == hash(dataset2)
16
+
17
+ # Assert that datasets with different names have different hashes
18
+ assert hash(dataset1) != hash(dataset3)
19
+
20
+
21
+ class TestExDatasetGetItem:
22
+ def test_by_index(self):
23
+ # Create a mock resource
24
+ mock_resource = type("MockResource", (), {"name": "Resource1"})()
25
+
26
+ # Create a dataset and add the mock resource
27
+ dataset = ExDataset()
28
+ dataset.resources.append(mock_resource)
29
+
30
+ # Access the resource by index
31
+ assert dataset[0] == mock_resource
32
+
33
+ def test_by_name(self):
34
+ # Create mock resources
35
+ mock_resource1 = type("MockResource", (), {"name": "Resource1"})()
36
+ mock_resource2 = type("MockResource", (), {"name": "Resource2"})()
37
+
38
+ # Create a dataset and add the mock resources
39
+ dataset = ExDataset()
40
+ dataset.resources.extend([mock_resource1, mock_resource2])
41
+
42
+ # Access the resources by name
43
+ assert dataset["Resource1"] == mock_resource1
44
+ assert dataset["Resource2"] == mock_resource2
45
+
46
+ def test_invalid_index(self):
47
+ # Create a dataset with no resources
48
+ dataset = ExDataset()
49
+
50
+ # Attempt to access an invalid index
51
+ with pytest.raises(IndexError):
52
+ _ = dataset[0]
53
+
54
+ def test_invalid_name(self):
55
+ # Create a mock resource
56
+ mock_resource = type("MockResource", (), {"name": "Resource1"})()
57
+
58
+ # Create a dataset and add the mock resource
59
+ dataset = ExDataset()
60
+ dataset.resources.append(mock_resource)
61
+
62
+ # Attempt to access a resource by an invalid name
63
+ with pytest.raises(KeyError, match="No resource found for key: InvalidName"):
64
+ _ = dataset["InvalidName"]
65
+
66
+
67
+ class TestExDatasetAddResource:
68
+ def test_add_valid_resource(self):
69
+ # Create a mock resource
70
+ mock_resource = type(
71
+ "MockResource",
72
+ (),
73
+ {"name": "Resource1", "categories": ["Category1", "SubCategory1"]},
74
+ )()
75
+
76
+ # Create a dataset and add the mock resource
77
+ dataset = ExDataset(res_class=type(mock_resource))
78
+ dataset.add_resource(mock_resource)
79
+
80
+ # Assert that the resource is added to the dataset
81
+ assert mock_resource in dataset.resources
82
+ assert (
83
+ dataset.category_map["Category1"]["SubCategory1"]["Resource1"]
84
+ == mock_resource
85
+ )
86
+
87
+ def test_add_invalid_resource_type(self):
88
+ # Create a mock resource of a different type
89
+ mock_resource = type("InvalidResource", (), {"name": "Resource1"})()
90
+
91
+ # Create a dataset with a specific resource class
92
+ dataset = ExDataset(res_class=type("ValidResource", (), {}))
93
+
94
+ # Attempt to add the invalid resource and assert a TypeError is raised
95
+ with pytest.raises(TypeError, match="Expected resource of type"):
96
+ dataset.add_resource(mock_resource)
97
+
98
+ def test_add_resource_updates_category_map(self):
99
+ # Create a shared MockResource class
100
+ MockResource = type(
101
+ "MockResource",
102
+ (),
103
+ {},
104
+ )
105
+
106
+ # Create mock resources
107
+ mock_resource1 = MockResource()
108
+ mock_resource1.name = "Resource1"
109
+ mock_resource1.categories = ["Category1"]
110
+
111
+ mock_resource2 = MockResource()
112
+ mock_resource2.name = "Resource2"
113
+ mock_resource2.categories = ["Category1", "SubCategory1"]
114
+
115
+ # Create a dataset and add the mock resources
116
+ dataset = ExDataset(res_class=MockResource)
117
+ dataset.add_resource(mock_resource1)
118
+ dataset.add_resource(mock_resource2)
119
+
120
+ # Assert that the category map is updated correctly
121
+ assert "Category1" in dataset.category_map
122
+ assert "SubCategory1" in dataset.category_map["Category1"]
123
+ assert dataset.category_map["Category1"]["Resource1"] == mock_resource1
124
+ assert (
125
+ dataset.category_map["Category1"]["SubCategory1"]["Resource2"]
126
+ == mock_resource2
127
+ )
128
+
129
+
130
+ class TestExDatasetVisit:
131
+ def test_visit_dataset(self, mocker):
132
+ # Create a mock visitor
133
+ mock_visitor = mocker.Mock()
134
+ mock_visitor.visit_dataset.return_value = True
135
+
136
+ # Create a mock resource
137
+ mock_resource = mocker.Mock()
138
+ mock_resource.visit.return_value = True
139
+ mock_resource.name = "TestResource"
140
+
141
+ # Create a dataset and add the mock resource
142
+ dataset = ExDataset()
143
+ dataset.resources.append(mock_resource)
144
+ dataset.category_map["TestResource"] = mock_resource
145
+
146
+ # Call the visit method
147
+ result = dataset.visit(mock_visitor)
148
+
149
+ # Assert that the visitor's visit_dataset method was called
150
+ mock_visitor.visit_dataset.assert_called_once_with(dataset)
151
+
152
+ # Assert that the resource's visit method was called
153
+ mock_resource.visit.assert_called_once_with(mock_visitor, omit_fields=False)
154
+
155
+ # Assert that the visit method returned True
156
+ assert result is True
157
+
158
+ def test_visit_dataset_stops_on_false(self, mocker):
159
+ # Create a mock visitor
160
+ mock_visitor = mocker.Mock()
161
+ mock_visitor.visit_dataset.return_value = False
162
+
163
+ # Create a dataset
164
+ dataset = ExDataset()
165
+
166
+ # Call the visit method
167
+ result = dataset.visit(mock_visitor)
168
+
169
+ # Assert that the visitor's visit_dataset method was called
170
+ mock_visitor.visit_dataset.assert_called_once_with(dataset)
171
+
172
+ # Assert that the visit method returned False
173
+ assert result is False
174
+
175
+ def test_visit_omit_categories(self, mocker):
176
+ # Create a mock visitor
177
+ mock_visitor = mocker.Mock()
178
+ mock_visitor.visit_dataset.return_value = True
179
+
180
+ # Create mock resources
181
+ mock_resource1 = mocker.Mock()
182
+ mock_resource1.visit.return_value = True
183
+ mock_resource2 = mocker.Mock()
184
+ mock_resource2.visit.return_value = True
185
+
186
+ # Create a dataset and add the mock resources
187
+ dataset = ExDataset()
188
+ dataset.resources.extend([mock_resource1, mock_resource2])
189
+
190
+ # Call the visit method with omit_categories=True
191
+ result = dataset.visit(mock_visitor, omit_categories=True)
192
+
193
+ # Assert that the visitor's visit_dataset method was called
194
+ mock_visitor.visit_dataset.assert_called_once_with(dataset)
195
+
196
+ # Assert that each resource's visit method was called
197
+ mock_resource1.visit.assert_called_once_with(mock_visitor, omit_fields=False)
198
+ mock_resource2.visit.assert_called_once_with(mock_visitor, omit_fields=False)
199
+
200
+ # Assert that the visit method returned True
201
+ assert result is True
202
+
203
+ def test_visit_category_map(self, mocker):
204
+ # Create a mock visitor
205
+ mock_visitor = mocker.Mock()
206
+ mock_visitor.visit_dataset.return_value = True
207
+ mock_visitor.visit_category = mocker.Mock()
208
+
209
+ # Create mock resources
210
+ mock_resource1 = mocker.Mock()
211
+ mock_resource1.visit.return_value = True
212
+ mock_resource1.name = "Resource1"
213
+ mock_resource2 = mocker.Mock()
214
+ mock_resource2.visit.return_value = True
215
+ mock_resource2.name = "Resource2"
216
+
217
+ # Create a dataset and add the mock resources
218
+ dataset = ExDataset()
219
+ dataset.category_map = {
220
+ "Category1": {
221
+ "SubCategory1": {"Resource1": mock_resource1},
222
+ "Resource2": mock_resource2,
223
+ }
224
+ }
225
+
226
+ # Call the visit method
227
+ result = dataset.visit(mock_visitor)
228
+
229
+ # Assert that the visitor's visit_dataset method was called
230
+ mock_visitor.visit_dataset.assert_called_once_with(dataset)
231
+
232
+ # Assert visit_category was called for each category.
233
+ mock_visitor.visit_category.assert_any_call("Category1", 0, mocker.ANY)
234
+ mock_visitor.visit_category.assert_any_call("SubCategory1", 1, mocker.ANY)
235
+
236
+ # Assert that each resource's visit method was called
237
+ mock_resource1.visit.assert_called_once_with(mock_visitor, omit_fields=False)
238
+ mock_resource2.visit.assert_called_once_with(mock_visitor, omit_fields=False)
239
+
240
+ # Assert that the visit method returned True
241
+ assert result is True
242
+
243
+ def test_visit_category_map_stops_on_false(self, mocker):
244
+ # Create a mock visitor
245
+ mock_visitor = mocker.Mock()
246
+ mock_visitor.visit_dataset.return_value = True
247
+ mock_visitor.visit_category = mocker.Mock(return_value=True)
248
+
249
+ # Create a mock resource that returns False for visit
250
+ mock_resource = mocker.Mock()
251
+ mock_resource.visit.return_value = False
252
+ mock_resource.name = "Resource1"
253
+
254
+ # Create a dataset and add the mock resource
255
+ dataset = ExDataset()
256
+ dataset.category_map = {"Category1": {"Resource1": mock_resource}}
257
+
258
+ # Call the visit method
259
+ result = dataset.visit(mock_visitor)
260
+
261
+ # Assert that the visitor's visit_dataset method was called
262
+ mock_visitor.visit_dataset.assert_called_once_with(dataset)
263
+
264
+ # Assert that the resource's visit method was called
265
+ mock_resource.visit.assert_called_once_with(mock_visitor, omit_fields=False)
266
+
267
+ # Assert that the visit method returned False
268
+ assert result is False
269
+
270
+
271
+ class TestExDatasetSortedByDeps:
272
+ def test_sorted_by_deps_no_dependencies(self):
273
+ # Create mock resources with no dependencies
274
+ mock_resource1 = type(
275
+ "MockResource",
276
+ (),
277
+ {
278
+ "name": "Resource1",
279
+ "get_dependencies": lambda self, fk_only=False: [],
280
+ },
281
+ )()
282
+ mock_resource2 = type(
283
+ "MockResource",
284
+ (),
285
+ {
286
+ "name": "Resource2",
287
+ "get_dependencies": lambda self, fk_only=False: [],
288
+ },
289
+ )()
290
+
291
+ # Create a dataset and add the mock resources
292
+ dataset = ExDataset()
293
+ dataset.resources.extend([mock_resource1, mock_resource2])
294
+
295
+ # Call sorted_by_deps
296
+ sorted_resources = dataset.sorted_by_deps()
297
+
298
+ # Assert that the resources are sorted correctly
299
+ assert sorted_resources == [mock_resource1, mock_resource2]
300
+
301
+ def test_sorted_by_deps_with_dependencies(self):
302
+ # Create mock resources with dependencies
303
+ mock_resource1 = type(
304
+ "MockResource",
305
+ (),
306
+ {
307
+ "name": "Resource1",
308
+ "get_dependencies": lambda self, fk_only=False: [],
309
+ },
310
+ )()
311
+ mock_resource2 = type(
312
+ "MockResource",
313
+ (),
314
+ {
315
+ "name": "Resource2",
316
+ "get_dependencies": lambda self, fk_only=False: (
317
+ [] if fk_only else [mock_resource1]
318
+ ),
319
+ },
320
+ )()
321
+ mock_resource3 = type(
322
+ "MockResource",
323
+ (),
324
+ {
325
+ "name": "Resource3",
326
+ "get_dependencies": lambda self, fk_only=False: (
327
+ [] if fk_only else [mock_resource2]
328
+ ),
329
+ },
330
+ )()
331
+
332
+ # Create a dataset and add the mock resources
333
+ dataset = ExDataset()
334
+ dataset.resources.extend([mock_resource3, mock_resource2, mock_resource1])
335
+
336
+ # Call sorted_by_deps
337
+ sorted_resources = dataset.sorted_by_deps()
338
+
339
+ # Assert that the resources are sorted correctly
340
+ assert sorted_resources == [
341
+ mock_resource1,
342
+ mock_resource2,
343
+ mock_resource3,
344
+ ]
345
+
346
+ def test_sorted_by_deps_with_circular_dependency(self, capsys):
347
+ # Create mock resources with circular dependencies
348
+ mock_resource1 = type(
349
+ "MockResource",
350
+ (),
351
+ {
352
+ "name": "Resource1",
353
+ "get_dependencies": lambda self, fk_only=False: (
354
+ [] if fk_only else [mock_resource2]
355
+ ),
356
+ },
357
+ )()
358
+ mock_resource2 = type(
359
+ "MockResource",
360
+ (),
361
+ {
362
+ "name": "Resource2",
363
+ "get_dependencies": lambda self, fk_only=False: (
364
+ [] if fk_only else [mock_resource1]
365
+ ),
366
+ },
367
+ )()
368
+
369
+ # Create a dataset and add the mock resources
370
+ dataset = ExDataset()
371
+ dataset.resources.extend([mock_resource1, mock_resource2])
372
+
373
+ # Call sorted_by_deps; circular deps may print a warning.
374
+ sorted_resources = dataset.sorted_by_deps()
375
+
376
+ # Capture the output after calling sorted_by_deps
377
+ captured = capsys.readouterr()
378
+
379
+ # The circular dependency detection happens in recursive function
380
+ # and prints to stdout. Check if it was captured.
381
+ output = captured.out
382
+ if "Circular dependency detected" not in output:
383
+ # If not captured, function still works but warning may not print.
384
+ # depending on execution path. Just verify the function completes.
385
+ pass
386
+
387
+ # Assert that the sorted resources are returned (partial order)
388
+ # Even with circular deps, the function should return resources
389
+ assert len(sorted_resources) == 2
390
+ assert mock_resource1 in sorted_resources
391
+ assert mock_resource2 in sorted_resources
392
+
393
+ def test_sorted_by_deps_with_fk_only_dependencies(self):
394
+ # Create mock resources with foreign key-only dependencies
395
+ mock_resource1 = type(
396
+ "MockResource",
397
+ (),
398
+ {
399
+ "name": "Resource1",
400
+ "get_dependencies": lambda self, fk_only=False: [],
401
+ },
402
+ )()
403
+ mock_resource2 = type(
404
+ "MockResource",
405
+ (),
406
+ {
407
+ "name": "Resource2",
408
+ "get_dependencies": lambda self, fk_only=False: (
409
+ [mock_resource1] if fk_only else [mock_resource1]
410
+ ),
411
+ },
412
+ )()
413
+
414
+ # Create a dataset and add the mock resources
415
+ dataset = ExDataset()
416
+ dataset.resources.extend([mock_resource2, mock_resource1])
417
+
418
+ # Call sorted_by_deps
419
+ sorted_resources = dataset.sorted_by_deps()
420
+
421
+ # Assert that the resources are sorted correctly
422
+ assert sorted_resources == [mock_resource1, mock_resource2]
@@ -0,0 +1,109 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+
5
+ from exdrf.constants import (
6
+ FIELD_TYPE_REF_MANY_TO_MANY,
7
+ FIELD_TYPE_REF_MANY_TO_ONE,
8
+ FIELD_TYPE_REF_ONE_TO_MANY,
9
+ FIELD_TYPE_REF_ONE_TO_ONE,
10
+ )
11
+ from exdrf.field import ExField
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_resource():
16
+ resource = Mock()
17
+ resource.name = "test_resource"
18
+ return resource
19
+
20
+
21
+ @pytest.fixture
22
+ def ex_field(mock_resource):
23
+ return ExField(
24
+ name="test_field",
25
+ resource=mock_resource,
26
+ type_name=FIELD_TYPE_REF_ONE_TO_MANY,
27
+ is_list=True,
28
+ primary=True,
29
+ visible=True,
30
+ read_only=False,
31
+ nullable=False,
32
+ sortable=True,
33
+ filterable=True,
34
+ exportable=True,
35
+ qsearch=True,
36
+ resizable=True,
37
+ )
38
+
39
+
40
+ def test_exfield_initialization(ex_field):
41
+ assert ex_field.name == "test_field"
42
+ assert ex_field.resource.name == "test_resource"
43
+ assert ex_field.type_name == FIELD_TYPE_REF_ONE_TO_MANY
44
+ assert ex_field.is_list is True
45
+ assert ex_field.primary is True
46
+ assert ex_field.visible is True
47
+ assert ex_field.read_only is False
48
+ assert ex_field.nullable is False
49
+
50
+
51
+ def test_exfield_pascal_case_name(ex_field):
52
+ assert ex_field.pascal_case_name == "TestField"
53
+
54
+
55
+ def test_exfield_snake_case_name(ex_field):
56
+ assert ex_field.snake_case_name == "test_field"
57
+
58
+
59
+ def test_exfield_snake_case_name_plural(ex_field):
60
+ ex_field.name = "test_field"
61
+ assert ex_field.snake_case_name_plural == "test_fields"
62
+
63
+
64
+ def test_exfield_camel_case_name(ex_field):
65
+ assert ex_field.camel_case_name == "testField"
66
+
67
+
68
+ def test_exfield_text_name(ex_field):
69
+ assert ex_field.text_name == "Test field"
70
+
71
+
72
+ def test_exfield_doc_lines(ex_field):
73
+ ex_field.description = "This is a test field."
74
+ assert ex_field.doc_lines == ["This is a test field."]
75
+
76
+
77
+ def test_exfield_is_ref_type(ex_field):
78
+ assert ex_field.is_ref_type is True
79
+
80
+
81
+ def test_exfield_is_one_to_many_type(ex_field):
82
+ assert ex_field.is_one_to_many_type is True
83
+
84
+
85
+ def test_exfield_is_one_to_one_type(ex_field):
86
+ ex_field.type_name = FIELD_TYPE_REF_ONE_TO_ONE
87
+ assert ex_field.is_one_to_one_type is True
88
+
89
+
90
+ def test_exfield_is_many_to_many_type(ex_field):
91
+ ex_field.type_name = FIELD_TYPE_REF_MANY_TO_MANY
92
+ assert ex_field.is_many_to_many_type is True
93
+
94
+
95
+ def test_exfield_is_many_to_one_type(ex_field):
96
+ ex_field.type_name = FIELD_TYPE_REF_MANY_TO_ONE
97
+ assert ex_field.is_many_to_one_type is True
98
+
99
+
100
+ def test_exfield_visit(ex_field):
101
+ mock_visitor = Mock()
102
+ mock_visitor.visit_field.return_value = True
103
+ assert ex_field.visit(mock_visitor) is True
104
+ mock_visitor.visit_field.assert_called_once_with(ex_field)
105
+
106
+
107
+ def test_exfield_extra_ref(ex_field):
108
+ mock_dataset = Mock()
109
+ assert ex_field.extra_ref(mock_dataset) == []