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/__init__.py ADDED
File without changes
exdrf/__version__.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.1-dev'
22
+ __version_tuple__ = version_tuple = (0, 0, 1, 'dev0')
23
+
24
+ __commit_id__ = commit_id = None
exdrf/api.py ADDED
@@ -0,0 +1,51 @@
1
+ from exdrf.dataset import ExDataset # noqa: F401
2
+ from exdrf.field import ExField # noqa: F401
3
+ from exdrf.field_types.api import ( # noqa: F401
4
+ BlobField,
5
+ BlobInfo,
6
+ BoolField,
7
+ BoolInfo,
8
+ DateField,
9
+ DateInfo,
10
+ DateTimeField,
11
+ DateTimeInfo,
12
+ DurationField,
13
+ DurationInfo,
14
+ EnumField,
15
+ EnumInfo,
16
+ FilterField,
17
+ FloatField,
18
+ FloatInfo,
19
+ FloatListField,
20
+ FloatListInfo,
21
+ FormattedField,
22
+ FormattedInfo,
23
+ IntField,
24
+ IntInfo,
25
+ IntListField,
26
+ IntListInfo,
27
+ RefBaseField,
28
+ RefManyToManyField,
29
+ RefManyToOneField,
30
+ RefOneToManyField,
31
+ RefOneToOneField,
32
+ RelExtraInfo,
33
+ SortField,
34
+ StrField,
35
+ StrInfo,
36
+ StrListField,
37
+ StrListInfo,
38
+ TimeField,
39
+ TimeInfo,
40
+ )
41
+ from exdrf.label_dsl import ( # noqa: F401
42
+ evaluate,
43
+ get_used_fields,
44
+ parse_expr,
45
+ )
46
+ from exdrf.resource import ExResource # noqa: F401
47
+ from exdrf.utils import ( # noqa: F401
48
+ doc_lines,
49
+ inflect_e,
50
+ )
51
+ from exdrf.visitor import ExVisitor # noqa: F401
exdrf/constants.py ADDED
@@ -0,0 +1,30 @@
1
+ # Constants for field types
2
+ from typing import Any, Final, Iterable, Literal, Union
3
+
4
+ FIELD_TYPE_BLOB: Final[Literal["blob"]] = "blob"
5
+ FIELD_TYPE_BOOL: Final[Literal["bool"]] = "bool"
6
+ FIELD_TYPE_DT: Final[Literal["date-time"]] = "date-time"
7
+ FIELD_TYPE_DATE: Final[Literal["date"]] = "date"
8
+ FIELD_TYPE_TIME: Final[Literal["time"]] = "time"
9
+ FIELD_TYPE_DURATION: Final[Literal["duration"]] = "duration"
10
+ FIELD_TYPE_ENUM: Final[Literal["enum"]] = "enum"
11
+ FIELD_TYPE_FLOAT: Final[Literal["float"]] = "float"
12
+ FIELD_TYPE_INTEGER: Final[Literal["integer"]] = "integer"
13
+ FIELD_TYPE_STRING: Final[Literal["string"]] = "string"
14
+ FIELD_TYPE_STRING_LIST: Final[Literal["string-list"]] = "string-list"
15
+ FIELD_TYPE_INT_LIST: Final[Literal["int-list"]] = "int-list"
16
+ FIELD_TYPE_FLOAT_LIST: Final[Literal["float-list"]] = "float-list"
17
+ FIELD_TYPE_FORMATTED: Final[Literal["formatted"]] = "formatted"
18
+ FIELD_TYPE_FILTER: Final[Literal["filter"]] = "filter"
19
+ FIELD_TYPE_SORT: Final[Literal["sort"]] = "sort"
20
+ FIELD_TYPE_REF_ONE_TO_MANY: Final[Literal["one-to-many"]] = "one-to-many"
21
+ FIELD_TYPE_REF_ONE_TO_ONE: Final[Literal["one-to-one"]] = "one-to-one"
22
+ FIELD_TYPE_REF_MANY_TO_MANY: Final[Literal["many-to-many"]] = "many-to-many"
23
+ FIELD_TYPE_REF_MANY_TO_ONE: Final[Literal["many-to-one"]] = "many-to-one"
24
+
25
+ # This are the types of relations that we know of.
26
+ RelType = Literal["OneToMany", "ManyToOne", "OneToOne", "ManyToMany"]
27
+
28
+ # A record ID can be an int in the simple case or a list of various types
29
+ # when there are multiple primary keys.
30
+ RecIdType = Union[int, Iterable[Any]]
exdrf/dataset.py ADDED
@@ -0,0 +1,197 @@
1
+ from collections import OrderedDict
2
+ from typing import (
3
+ TYPE_CHECKING,
4
+ Any,
5
+ Dict,
6
+ List,
7
+ Optional,
8
+ Tuple,
9
+ Type,
10
+ Union,
11
+ cast,
12
+ )
13
+
14
+ from attrs import define, field
15
+
16
+ from exdrf.resource import ExResource
17
+
18
+ if TYPE_CHECKING:
19
+ from exdrf.visitor import ExVisitor
20
+
21
+
22
+ @define
23
+ class ExDataset:
24
+ """A set of resources.
25
+
26
+ The resources are stored in a list to ensure a consistent enumeration
27
+ order. To access a resource in the dataset, you can use the `dataset[key]`
28
+ syntax, where `key` can be either the index of the resource in the list
29
+ of models or the name of the resource.
30
+
31
+ Attributes:
32
+ name: The name of the dataset.
33
+ resources: A list of resources in the dataset.
34
+ category_map: A tree of categories, where each key is a category and
35
+ the value is a dictionary of subcategories or resources.
36
+ """
37
+
38
+ name: str = field(default="Dataset")
39
+ resources: List["ExResource"] = field(factory=list, repr=False)
40
+ category_map: dict = field(factory=OrderedDict, repr=False)
41
+ res_class: Type["ExResource"] = field(default=ExResource, repr=False, kw_only=True)
42
+
43
+ def __hash__(self):
44
+ return hash(self.name)
45
+
46
+ def __getitem__(self, key: Union[int, str]) -> "ExResource":
47
+ # Attempt to use the key as an index first.
48
+ if isinstance(key, int):
49
+ return self.resources[key]
50
+
51
+ # If the key is not an index, treat it as a name.
52
+ for m in self.resources:
53
+ if m.name == key:
54
+ return m
55
+
56
+ raise KeyError(
57
+ f"No resource found for key: {key}; valid indices are "
58
+ f"from 0 to {len(self.resources) - 1}. "
59
+ f"Valid names are: {[m.name for m in self.resources]}"
60
+ )
61
+
62
+ def __contains__(self, key: str) -> bool:
63
+ """Check if a resource is in the dataset."""
64
+ for m in self.resources:
65
+ if m.name == key:
66
+ return True
67
+ return False
68
+
69
+ def add_resource(self, resource: "ExResource") -> None:
70
+ """Add a resource to the dataset.
71
+
72
+ Args:
73
+ resource: The resource to add.
74
+ """
75
+ if not isinstance(resource, self.res_class):
76
+ raise TypeError(
77
+ f"Expected resource of type {self.res_class}, but got {type(resource)}."
78
+ )
79
+ self.resources.append(resource)
80
+ resource.dataset = self # type: ignore
81
+
82
+ # Place the resource in the category map.
83
+ crt = self.category_map
84
+ for part in resource.categories:
85
+ next_crt = crt.get(part)
86
+ if next_crt is None:
87
+ next_crt = OrderedDict()
88
+ crt[part] = next_crt
89
+ crt = next_crt
90
+ crt[resource.name] = resource
91
+
92
+ def visit(
93
+ self,
94
+ visitor: "ExVisitor",
95
+ omit_fields: Optional[bool] = False,
96
+ omit_categories: Optional[bool] = False,
97
+ ) -> bool:
98
+ """Visit the dataset and its resources.
99
+
100
+ Args:
101
+ visitor: The visitor to use.
102
+ omit_fields: If True, resource fields will not be visited.
103
+ omit_categories: If True, categories will not be visited.
104
+ This means that the visitor will only visit the resources in
105
+ the dataset, not the categories, making the process a bit more
106
+ efficient.
107
+
108
+ Returns:
109
+ bool: True if the visit should continue, False otherwise.
110
+ """
111
+ if not visitor.visit_dataset(self): # type: ignore
112
+ return False
113
+
114
+ if omit_categories:
115
+ for res in self.resources:
116
+ if not res.visit(visitor, omit_fields=omit_fields):
117
+ return False
118
+ return True
119
+
120
+ def do_category_map(crt_map: dict, level: int = 0) -> bool:
121
+ for k, v in crt_map.items():
122
+ if isinstance(v, dict):
123
+ visitor.visit_category(k, level, v)
124
+ if not do_category_map(v, level + 1):
125
+ return False
126
+ else:
127
+ resource = cast("ExResource", v)
128
+ if not resource.visit(visitor, omit_fields=omit_fields):
129
+ return False
130
+ return True
131
+
132
+ return do_category_map(self.category_map)
133
+
134
+ def zero_categories(self) -> List[Tuple[str, List["ExResource"]]]:
135
+ """Get a list of top level categories and their resources."""
136
+ result = []
137
+ for ctg, ctg_data in self.category_map.items():
138
+ models = []
139
+
140
+ def do_data(crt_data: Any) -> None:
141
+ if isinstance(crt_data, dict):
142
+ for subset in crt_data.values():
143
+ do_data(subset)
144
+ else:
145
+ models.append(crt_data)
146
+
147
+ do_data(ctg_data)
148
+ result.append((ctg, models))
149
+ return result
150
+
151
+ def sorted_by_deps(self) -> List["ExResource"]:
152
+ # Build a dependency map where key is the resource name and value is a
153
+ # set of names of resources it depends on.
154
+ deps: Dict[str, List["ExResource"]] = {}
155
+ short_deps: Dict[str, List["ExResource"]] = {}
156
+ name_to_resource: Dict[str, "ExResource"] = {}
157
+ for resource in self.resources:
158
+ deps[resource.name] = list(resource.get_dependencies())
159
+ short_deps[resource.name] = list(resource.get_dependencies(fk_only=True))
160
+ name_to_resource[resource.name] = resource
161
+
162
+ # Start with those that have no dependencies.
163
+ result = OrderedDict(
164
+ (name, name_to_resource[name])
165
+ for name, deps in sorted(deps.items(), key=lambda x: x[0])
166
+ if len(deps) == 0
167
+ )
168
+
169
+ def recursive(name: str, visited: List[str], fk_only: bool = False):
170
+ """Examine the dependency chain of a resource."""
171
+ if name in visited:
172
+ print(
173
+ f"Circular dependency detected: {name} -> " + " -> ".join(visited)
174
+ )
175
+ return
176
+
177
+ if name in result:
178
+ return
179
+
180
+ if fk_only:
181
+ my_deps = short_deps[name]
182
+ else:
183
+ my_deps = deps[name]
184
+ for dep in my_deps:
185
+ recursive(dep.name, visited + [name], fk_only=fk_only)
186
+
187
+ # Add the resource to the sorted list.
188
+ result[name] = name_to_resource[name]
189
+
190
+ for name in sorted(short_deps.keys()):
191
+ recursive(name, [], fk_only=True)
192
+
193
+ if len(result) != len(deps):
194
+ for name in sorted(deps.keys()):
195
+ recursive(name, [], fk_only=False)
196
+
197
+ return list(result.values())