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
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())
|