exdrf 0.1.8__tar.gz

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 (63) hide show
  1. exdrf-0.1.8/PKG-INFO +41 -0
  2. exdrf-0.1.8/README.md +7 -0
  3. exdrf-0.1.8/exdrf/__init__.py +0 -0
  4. exdrf-0.1.8/exdrf/__version__.py +24 -0
  5. exdrf-0.1.8/exdrf/api.py +51 -0
  6. exdrf-0.1.8/exdrf/constants.py +30 -0
  7. exdrf-0.1.8/exdrf/dataset.py +197 -0
  8. exdrf-0.1.8/exdrf/field.py +554 -0
  9. exdrf-0.1.8/exdrf/field_types/__init__.py +0 -0
  10. exdrf-0.1.8/exdrf/field_types/api.py +78 -0
  11. exdrf-0.1.8/exdrf/field_types/blob_field.py +44 -0
  12. exdrf-0.1.8/exdrf/field_types/bool_field.py +47 -0
  13. exdrf-0.1.8/exdrf/field_types/date_field.py +49 -0
  14. exdrf-0.1.8/exdrf/field_types/date_time.py +52 -0
  15. exdrf-0.1.8/exdrf/field_types/dur_field.py +44 -0
  16. exdrf-0.1.8/exdrf/field_types/enum_field.py +41 -0
  17. exdrf-0.1.8/exdrf/field_types/filter_field.py +11 -0
  18. exdrf-0.1.8/exdrf/field_types/float_field.py +85 -0
  19. exdrf-0.1.8/exdrf/field_types/float_list.py +18 -0
  20. exdrf-0.1.8/exdrf/field_types/formatted.py +39 -0
  21. exdrf-0.1.8/exdrf/field_types/int_field.py +70 -0
  22. exdrf-0.1.8/exdrf/field_types/int_list.py +18 -0
  23. exdrf-0.1.8/exdrf/field_types/ref_base.py +105 -0
  24. exdrf-0.1.8/exdrf/field_types/ref_m2m.py +39 -0
  25. exdrf-0.1.8/exdrf/field_types/ref_m2o.py +23 -0
  26. exdrf-0.1.8/exdrf/field_types/ref_o2m.py +36 -0
  27. exdrf-0.1.8/exdrf/field_types/ref_o2o.py +32 -0
  28. exdrf-0.1.8/exdrf/field_types/sort_field.py +18 -0
  29. exdrf-0.1.8/exdrf/field_types/str_field.py +77 -0
  30. exdrf-0.1.8/exdrf/field_types/str_list.py +18 -0
  31. exdrf-0.1.8/exdrf/field_types/time_field.py +49 -0
  32. exdrf-0.1.8/exdrf/filter.py +653 -0
  33. exdrf-0.1.8/exdrf/filter_dsl.py +950 -0
  34. exdrf-0.1.8/exdrf/filter_op_catalog.py +222 -0
  35. exdrf-0.1.8/exdrf/label_dsl.py +691 -0
  36. exdrf-0.1.8/exdrf/moment.py +496 -0
  37. exdrf-0.1.8/exdrf/py.typed +0 -0
  38. exdrf-0.1.8/exdrf/py_support.py +21 -0
  39. exdrf-0.1.8/exdrf/resource.py +901 -0
  40. exdrf-0.1.8/exdrf/sa_fi_item.py +69 -0
  41. exdrf-0.1.8/exdrf/sa_filter_op.py +324 -0
  42. exdrf-0.1.8/exdrf/utils.py +17 -0
  43. exdrf-0.1.8/exdrf/validator.py +45 -0
  44. exdrf-0.1.8/exdrf/var_bag.py +328 -0
  45. exdrf-0.1.8/exdrf/visitor.py +58 -0
  46. exdrf-0.1.8/exdrf.egg-info/PKG-INFO +41 -0
  47. exdrf-0.1.8/exdrf.egg-info/SOURCES.txt +61 -0
  48. exdrf-0.1.8/exdrf.egg-info/dependency_links.txt +1 -0
  49. exdrf-0.1.8/exdrf.egg-info/requires.txt +21 -0
  50. exdrf-0.1.8/exdrf.egg-info/top_level.txt +3 -0
  51. exdrf-0.1.8/exdrf_tests/__init__.py +0 -0
  52. exdrf-0.1.8/exdrf_tests/test_dataset.py +422 -0
  53. exdrf-0.1.8/exdrf_tests/test_field.py +109 -0
  54. exdrf-0.1.8/exdrf_tests/test_filter.py +425 -0
  55. exdrf-0.1.8/exdrf_tests/test_filter_dsl.py +556 -0
  56. exdrf-0.1.8/exdrf_tests/test_label_dsl.py +234 -0
  57. exdrf-0.1.8/exdrf_tests/test_resource.py +107 -0
  58. exdrf-0.1.8/exdrf_tests/test_utils.py +43 -0
  59. exdrf-0.1.8/exdrf_tests/test_visitor.py +31 -0
  60. exdrf-0.1.8/exdrf_tests/var_bag_test.py +502 -0
  61. exdrf-0.1.8/pyproject.toml +72 -0
  62. exdrf-0.1.8/setup.cfg +4 -0
  63. exdrf-0.1.8/setup.py +6 -0
exdrf-0.1.8/PKG-INFO ADDED
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: exdrf
3
+ Version: 0.1.8
4
+ Summary: Describe datasets, resources and their fields.
5
+ Author-email: Nicu Tofan <nicu.tofan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12.2
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: attrs>=25.1.0
14
+ Requires-Dist: inflect>=7.5.0
15
+ Requires-Dist: SQLAlchemy>=2.0.38
16
+ Requires-Dist: Unidecode>=1.4.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: autoflake; extra == "dev"
19
+ Requires-Dist: black; extra == "dev"
20
+ Requires-Dist: build; extra == "dev"
21
+ Requires-Dist: flake8; extra == "dev"
22
+ Requires-Dist: isort; extra == "dev"
23
+ Requires-Dist: mypy; extra == "dev"
24
+ Requires-Dist: ruff; extra == "dev"
25
+ Requires-Dist: pre-commit; extra == "dev"
26
+ Requires-Dist: pyproject-flake8; extra == "dev"
27
+ Requires-Dist: pytest-cov; extra == "dev"
28
+ Requires-Dist: pytest-mock; extra == "dev"
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: twine; extra == "dev"
31
+ Requires-Dist: wheel; extra == "dev"
32
+ Requires-Dist: pre-commit; extra == "dev"
33
+ Requires-Dist: click<8.2.0,>=8.1.8; extra == "dev"
34
+
35
+ # Datasets-Resources-Fields
36
+
37
+ The library allows the user to define fields that make up a resource that is
38
+ part of a dataset. The purpose is to construct an unified interface for
39
+ describing these concepts, with supporting libraries for deriving the
40
+ tree from various sources (e.g. SqlAlchemy, Pydantic, etc.) and for
41
+ generating content from the tree.
exdrf-0.1.8/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Datasets-Resources-Fields
2
+
3
+ The library allows the user to define fields that make up a resource that is
4
+ part of a dataset. The purpose is to construct an unified interface for
5
+ describing these concepts, with supporting libraries for deriving the
6
+ tree from various sources (e.g. SqlAlchemy, Pydantic, etc.) and for
7
+ generating content from the tree.
File without changes
@@ -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.1.8'
22
+ __version_tuple__ = version_tuple = (0, 1, 8)
23
+
24
+ __commit_id__ = commit_id = 'g450ddaecd'
@@ -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
@@ -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]]
@@ -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())