dataframely 1.5.0__tar.gz → 1.6.0__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.
- {dataframely-1.5.0 → dataframely-1.6.0}/PKG-INFO +2 -2
- {dataframely-1.5.0 → dataframely-1.6.0}/README.md +1 -1
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/__init__.py +2 -1
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_base_collection.py +27 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_base_schema.py +23 -22
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_deprecation.py +11 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_filter.py +6 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_rule.py +92 -5
- dataframely-1.6.0/dataframely/_serialization.py +88 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/collection.py +60 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/__init__.py +2 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/_base.py +185 -13
- dataframely-1.6.0/dataframely/columns/_registry.py +32 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/any.py +4 -9
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/array.py +23 -9
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/bool.py +2 -2
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/datetime.py +22 -26
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/decimal.py +4 -8
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/enum.py +5 -8
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/float.py +6 -8
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/integer.py +13 -8
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/list.py +22 -9
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/object.py +4 -8
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/string.py +4 -10
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/struct.py +32 -9
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/exc.py +4 -25
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/schema.py +119 -1
- {dataframely-1.5.0 → dataframely-1.6.0}/pyproject.toml +1 -1
- dataframely-1.6.0/tests/collection/test_collection_future_annotations.py +24 -0
- dataframely-1.6.0/tests/collection/test_matches.py +119 -0
- dataframely-1.6.0/tests/collection/test_repr.py +38 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_alias.py +14 -0
- dataframely-1.6.0/tests/columns/test_matches.py +64 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_base.py +5 -8
- dataframely-1.6.0/tests/schema/test_matches.py +140 -0
- dataframely-1.6.0/tests/schema/test_repr.py +54 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_rule_implementation.py +1 -27
- dataframely-1.6.0/tests/schema/test_serialization.py +130 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_validate.py +25 -3
- dataframely-1.6.0/tests/test_deprecation.py +63 -0
- dataframely-1.5.0/tests/test_deprecation.py +0 -28
- {dataframely-1.5.0 → dataframely-1.6.0}/.copier-answers.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.envrc +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.gitattributes +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/CODEOWNERS +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/dependabot.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/release-drafter.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/workflows/build.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/workflows/chore.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/workflows/ci.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.github/workflows/scorecard.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.gitignore +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.pre-commit-config.yaml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.prettierignore +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.prettierrc +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/.readthedocs.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/Cargo.lock +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/Cargo.toml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/LICENSE +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/SECURITY.md +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_compat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_extre.pyi +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_polars.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_typing.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/config.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/failure.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/functional.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/mypy.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/py.typed +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/random.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/const.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/factory.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/mask.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/rules.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/dataframely/testing/typing.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docker-compose.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/Makefile +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.collection.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.any.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.bool.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.enum.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.float.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.integer.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.list.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.string.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.struct.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.config.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.exc.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.failure.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.functional.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.mypy.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.random.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.schema.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.const.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.factory.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.mask.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.rules.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.typing.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_api/modules.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_static/custom.css +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/_static/favicon.ico +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/conf.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/index.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/make.bat +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/development.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/examples/real-world.ipynb +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/faq.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/installation.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/quickstart.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/docs/sites/versioning.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/pixi.lock +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/pixi.toml +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/src/errdefs.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/src/lib.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/src/regex_repr.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_base.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_cast.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_sample.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_any.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_array.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_datetime.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_float.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_list.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_object.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_string.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_check.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_pyarrow.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_rules.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_sample.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_sql_schema.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_str.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/columns/test_utils.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/core_validation/test_column_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/core_validation/test_dtype_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/functional/test_concat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_cast.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_filter.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/schema/test_sample.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_compat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_config.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_exc.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_extre.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_failure_info.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_random.py +0 -0
- {dataframely-1.5.0 → dataframely-1.6.0}/tests/test_typing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframely
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -28,7 +28,7 @@ Project-URL: Repository, https://github.com/quantco/dataframely
|
|
|
28
28
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
29
29
|
[](https://pypi.org/project/dataframely)
|
|
30
30
|
[](https://pypi.org/project/dataframely)
|
|
31
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
31
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
32
32
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
12
12
|
[](https://pypi.org/project/dataframely)
|
|
13
13
|
[](https://pypi.org/project/dataframely)
|
|
14
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
14
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -51,7 +51,7 @@ from .functional import (
|
|
|
51
51
|
filter_relationship_one_to_at_least_one,
|
|
52
52
|
filter_relationship_one_to_one,
|
|
53
53
|
)
|
|
54
|
-
from .schema import Schema
|
|
54
|
+
from .schema import Schema, deserialize_schema
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"random",
|
|
@@ -67,6 +67,7 @@ __all__ = [
|
|
|
67
67
|
"filter_relationship_one_to_at_least_one",
|
|
68
68
|
"filter_relationship_one_to_one",
|
|
69
69
|
"Schema",
|
|
70
|
+
"deserialize_schema",
|
|
70
71
|
"Any",
|
|
71
72
|
"Bool",
|
|
72
73
|
"Column",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
import typing
|
|
7
8
|
from abc import ABCMeta
|
|
8
9
|
from collections.abc import Iterable
|
|
@@ -245,6 +246,32 @@ class CollectionMeta(ABCMeta):
|
|
|
245
246
|
# Some other unknown annotation
|
|
246
247
|
raise AnnotationImplementationError(attr, type_annotation)
|
|
247
248
|
|
|
249
|
+
def __repr__(cls) -> str:
|
|
250
|
+
parts = [f'[Collection "{cls.__class__.__name__}"]']
|
|
251
|
+
parts.append(textwrap.indent("Members:", prefix=" " * 2))
|
|
252
|
+
for name, member in cls.members().items(): # type: ignore
|
|
253
|
+
parts.append(
|
|
254
|
+
textwrap.indent(
|
|
255
|
+
f'- "{name}": {member.schema.__name__}'
|
|
256
|
+
f"(optional={member.is_optional}, "
|
|
257
|
+
f"ignored_in_filters={member.ignored_in_filters}, "
|
|
258
|
+
f"inline_for_sampling={member.inline_for_sampling})",
|
|
259
|
+
prefix=" " * 4,
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if filters := cls._filters(): # type: ignore
|
|
263
|
+
parts.append(textwrap.indent("Filters:", prefix=" " * 2))
|
|
264
|
+
for name, member in filters.items():
|
|
265
|
+
parts.append(textwrap.indent(f'- "{name}":', prefix=" " * 4))
|
|
266
|
+
parts.append(
|
|
267
|
+
textwrap.indent(
|
|
268
|
+
f"{member.logic(cls.create_empty()).explain()}", # type: ignore
|
|
269
|
+
prefix=" " * 8,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
parts.append("") # Add line break at the end
|
|
273
|
+
return "\n".join(parts)
|
|
274
|
+
|
|
248
275
|
|
|
249
276
|
class BaseCollection(metaclass=CollectionMeta):
|
|
250
277
|
"""Internal utility abstraction to reference collections without introducing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
from abc import ABCMeta
|
|
7
8
|
from copy import copy
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -10,9 +11,9 @@ from typing import Any, Self
|
|
|
10
11
|
|
|
11
12
|
import polars as pl
|
|
12
13
|
|
|
13
|
-
from ._rule import GroupRule, Rule
|
|
14
|
+
from ._rule import GroupRule, Rule
|
|
14
15
|
from .columns import Column
|
|
15
|
-
from .exc import ImplementationError
|
|
16
|
+
from .exc import ImplementationError
|
|
16
17
|
|
|
17
18
|
_COLUMN_ATTR = "__dataframely_columns__"
|
|
18
19
|
_RULE_ATTR = "__dataframely_rules__"
|
|
@@ -111,25 +112,15 @@ class SchemaMeta(ABCMeta):
|
|
|
111
112
|
f"which are not in the schema: {missing_list}."
|
|
112
113
|
)
|
|
113
114
|
|
|
114
|
-
# 3) Assuming that non-custom rules are implemented correctly, we check that all
|
|
115
|
-
# custom rules are _also_ implemented correctly by evaluating rules on an
|
|
116
|
-
# empty data frame and checking for the evaluated dtypes.
|
|
117
|
-
if len(result.rules) > 0:
|
|
118
|
-
lf_empty = pl.LazyFrame(
|
|
119
|
-
schema={col_name: col.dtype for col_name, col in result.columns.items()}
|
|
120
|
-
)
|
|
121
|
-
# NOTE: For some reason, `polars` does not yield correct dtypes when calling
|
|
122
|
-
# `collect_schema()`
|
|
123
|
-
schema = with_evaluation_rules(lf_empty, result.rules).collect().schema
|
|
124
|
-
for rule_name, rule in result.rules.items():
|
|
125
|
-
dtype = schema[rule_name]
|
|
126
|
-
if not isinstance(dtype, pl.Boolean):
|
|
127
|
-
raise RuleImplementationError(
|
|
128
|
-
rule_name, dtype, isinstance(rule, GroupRule)
|
|
129
|
-
)
|
|
130
|
-
|
|
131
115
|
return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
|
|
132
116
|
|
|
117
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
118
|
+
val = super().__getattribute__(name)
|
|
119
|
+
# Dynamically set the name of the column if it is a `Column` instance.
|
|
120
|
+
if isinstance(val, Column):
|
|
121
|
+
val._name = val.alias or name
|
|
122
|
+
return val
|
|
123
|
+
|
|
133
124
|
@staticmethod
|
|
134
125
|
def _get_metadata_recursively(kls: type[object]) -> Metadata:
|
|
135
126
|
result = Metadata()
|
|
@@ -145,9 +136,7 @@ class SchemaMeta(ABCMeta):
|
|
|
145
136
|
k: v for k, v in source.items() if not k.startswith("__")
|
|
146
137
|
}.items():
|
|
147
138
|
if isinstance(value, Column):
|
|
148
|
-
|
|
149
|
-
value.alias = attr
|
|
150
|
-
result.columns[value.alias] = value
|
|
139
|
+
result.columns[value.alias or attr] = value
|
|
151
140
|
if isinstance(value, Rule):
|
|
152
141
|
# We must ensure that custom rules do not clash with internal rules.
|
|
153
142
|
if attr == "primary_key":
|
|
@@ -157,6 +146,18 @@ class SchemaMeta(ABCMeta):
|
|
|
157
146
|
result.rules[attr] = value
|
|
158
147
|
return result
|
|
159
148
|
|
|
149
|
+
def __repr__(cls) -> str:
|
|
150
|
+
parts = [f'[Schema "{cls.__name__}"]']
|
|
151
|
+
parts.append(textwrap.indent("Columns:", prefix=" " * 2))
|
|
152
|
+
for name, col in cls.columns().items():
|
|
153
|
+
parts.append(textwrap.indent(f'- "{name}": {col!r}', prefix=" " * 4))
|
|
154
|
+
if validation_rules := cls._schema_validation_rules():
|
|
155
|
+
parts.append(textwrap.indent("Rules:", prefix=" " * 2))
|
|
156
|
+
for name, rule in validation_rules.items():
|
|
157
|
+
parts.append(textwrap.indent(f'- "{name}": {rule!r}', prefix=" " * 4))
|
|
158
|
+
parts.append("") # Add line break at the end
|
|
159
|
+
return "\n".join(parts)
|
|
160
|
+
|
|
160
161
|
|
|
161
162
|
class BaseSchema(metaclass=SchemaMeta):
|
|
162
163
|
"""Internal utility abstraction to reference schemas without introducing cyclical
|
|
@@ -37,3 +37,14 @@ def warn_nullable_default_change() -> None:
|
|
|
37
37
|
FutureWarning,
|
|
38
38
|
stacklevel=4,
|
|
39
39
|
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@skip_if(env="DATAFRAMELY_NO_FUTURE_WARNINGS")
|
|
43
|
+
def warn_no_nullable_primary_keys() -> None:
|
|
44
|
+
warnings.warn(
|
|
45
|
+
"Nullable primary keys are not supported. "
|
|
46
|
+
"Setting `nullable=True` on a primary key column is ignored "
|
|
47
|
+
"and will raise an error in a future release.",
|
|
48
|
+
FutureWarning,
|
|
49
|
+
stacklevel=4,
|
|
50
|
+
)
|
|
@@ -34,6 +34,12 @@ def filter() -> Callable[[Callable[[C], pl.LazyFrame]], Filter[C]]:
|
|
|
34
34
|
Attention:
|
|
35
35
|
Make sure to provide unique combinations of the primary keys or the filters
|
|
36
36
|
might introduce duplicate rows.
|
|
37
|
+
|
|
38
|
+
Attention:
|
|
39
|
+
The filter logic should return a lazy frame with a static computational graph.
|
|
40
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
41
|
+
validation, but may lead to wrong results in Collection comparisons
|
|
42
|
+
and (de-)serialization.
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
def decorator(validation_fn: Callable[[C], pl.LazyFrame]) -> Filter[C]:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Copyright (c) QuantCo 2025-2025
|
|
2
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
from collections import defaultdict
|
|
5
7
|
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Self
|
|
6
9
|
|
|
7
10
|
import polars as pl
|
|
8
11
|
|
|
@@ -12,17 +15,68 @@ ValidationFunction = Callable[[], pl.Expr]
|
|
|
12
15
|
class Rule:
|
|
13
16
|
"""Internal class representing validation rules."""
|
|
14
17
|
|
|
15
|
-
def __init__(self, expr: pl.Expr) -> None:
|
|
16
|
-
self.
|
|
18
|
+
def __init__(self, expr: pl.Expr | ValidationFunction) -> None:
|
|
19
|
+
self._expr = expr
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def expr(self) -> pl.Expr:
|
|
23
|
+
"""Get the expression of the rule."""
|
|
24
|
+
if callable(self._expr):
|
|
25
|
+
return self._expr()
|
|
26
|
+
return self._expr
|
|
27
|
+
|
|
28
|
+
def matches(self, other: Rule) -> bool:
|
|
29
|
+
"""Check whether this rule semantically matches another rule.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
other: The rule to compare with.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Whether the rules are semantically equal.
|
|
36
|
+
"""
|
|
37
|
+
return self.expr.meta.eq(other.expr)
|
|
38
|
+
|
|
39
|
+
def as_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Turn the rule into a dictionary."""
|
|
41
|
+
return {"rule_type": self.__class__.__name__, "expr": self.expr}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
45
|
+
"""Read the rule from a dictionary.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: The dictionary that was created via :meth:`asdict`.
|
|
49
|
+
"""
|
|
50
|
+
return cls(data["expr"])
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return str(self.expr)
|
|
17
54
|
|
|
18
55
|
|
|
19
56
|
class GroupRule(Rule):
|
|
20
57
|
"""Rule that is evaluated on a group of columns."""
|
|
21
58
|
|
|
22
|
-
def __init__(
|
|
59
|
+
def __init__(
|
|
60
|
+
self, expr: pl.Expr | ValidationFunction, group_columns: list[str]
|
|
61
|
+
) -> None:
|
|
23
62
|
super().__init__(expr)
|
|
24
63
|
self.group_columns = group_columns
|
|
25
64
|
|
|
65
|
+
def matches(self, other: Rule) -> bool:
|
|
66
|
+
if not isinstance(other, GroupRule):
|
|
67
|
+
return False
|
|
68
|
+
return super().matches(other) and self.group_columns == other.group_columns
|
|
69
|
+
|
|
70
|
+
def as_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {**super().as_dict(), "group_columns": self.group_columns}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
75
|
+
return cls(data["expr"], group_columns=data["group_columns"])
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"{super().__repr__()} grouped by {self.group_columns}"
|
|
79
|
+
|
|
26
80
|
|
|
27
81
|
def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction], Rule]:
|
|
28
82
|
"""Mark a function as a rule to evaluate during validation.
|
|
@@ -52,12 +106,18 @@ def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction],
|
|
|
52
106
|
rules. By default, any rule that evaluates to ``null`` because one of the
|
|
53
107
|
columns used in the rule is ``null`` is interpreted as ``true``, i.e. the row
|
|
54
108
|
is assumed to be valid.
|
|
109
|
+
|
|
110
|
+
Attention:
|
|
111
|
+
The rule logic should return a static result.
|
|
112
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
113
|
+
validation, but may lead to wrong results in Schema comparisons
|
|
114
|
+
and (de-)serialization.
|
|
55
115
|
"""
|
|
56
116
|
|
|
57
117
|
def decorator(validation_fn: ValidationFunction) -> Rule:
|
|
58
118
|
if group_by is not None:
|
|
59
|
-
return GroupRule(expr=validation_fn
|
|
60
|
-
return Rule(expr=validation_fn
|
|
119
|
+
return GroupRule(expr=validation_fn, group_columns=group_by)
|
|
120
|
+
return Rule(expr=validation_fn)
|
|
61
121
|
|
|
62
122
|
return decorator
|
|
63
123
|
|
|
@@ -130,3 +190,30 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
|
|
|
130
190
|
frame, on=list(group_columns), how="left", nulls_equal=True
|
|
131
191
|
)
|
|
132
192
|
return result
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------------------------ #
|
|
196
|
+
# FACTORY #
|
|
197
|
+
# ------------------------------------------------------------------------------------ #
|
|
198
|
+
|
|
199
|
+
_TYPE_MAPPING: dict[str, type[Rule]] = {
|
|
200
|
+
Rule.__name__: Rule,
|
|
201
|
+
GroupRule.__name__: GroupRule,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def rule_from_dict(data: dict[str, Any]) -> Rule:
|
|
206
|
+
"""Dynamically read a rule object from a dictionary.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data: The dictionary obtained by calling :meth:`~Rule.asdict` on a rule object.
|
|
210
|
+
The dictionary must contain a key ``"rule_type"`` that indicates which rule
|
|
211
|
+
type to instantiate.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The rule object as read from ``data``.
|
|
215
|
+
"""
|
|
216
|
+
name = data["rule_type"]
|
|
217
|
+
if name not in _TYPE_MAPPING:
|
|
218
|
+
raise ValueError(f"Unknown rule type: {name}")
|
|
219
|
+
return _TYPE_MAPPING[name].from_dict(data)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) QuantCo 2025-2025
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import decimal
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from json import JSONDecoder, JSONEncoder
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SchemaJSONEncoder(JSONEncoder):
|
|
14
|
+
"""Custom JSON encoder to properly serialize all types serialized by schemas."""
|
|
15
|
+
|
|
16
|
+
def encode(self, obj: Any) -> str:
|
|
17
|
+
def hint_tuples(item: Any) -> Any:
|
|
18
|
+
if isinstance(item, tuple):
|
|
19
|
+
return {"__type__": "tuple", "value": list(item)}
|
|
20
|
+
if isinstance(item, list):
|
|
21
|
+
return [hint_tuples(i) for i in item]
|
|
22
|
+
if isinstance(item, dict):
|
|
23
|
+
return {k: hint_tuples(v) for k, v in item.items()}
|
|
24
|
+
return item
|
|
25
|
+
|
|
26
|
+
return super().encode(hint_tuples(obj))
|
|
27
|
+
|
|
28
|
+
def default(self, obj: Any) -> Any:
|
|
29
|
+
match obj:
|
|
30
|
+
case pl.Expr():
|
|
31
|
+
return {
|
|
32
|
+
"__type__": "expression",
|
|
33
|
+
"value": obj.meta.serialize(format="json"),
|
|
34
|
+
}
|
|
35
|
+
case decimal.Decimal():
|
|
36
|
+
return {"__type__": "decimal", "value": str(obj)}
|
|
37
|
+
case dt.datetime():
|
|
38
|
+
return {"__type__": "datetime", "value": obj.isoformat()}
|
|
39
|
+
case dt.date():
|
|
40
|
+
return {"__type__": "date", "value": obj.isoformat()}
|
|
41
|
+
case dt.time():
|
|
42
|
+
return {"__type__": "time", "value": obj.isoformat()}
|
|
43
|
+
case dt.timedelta():
|
|
44
|
+
return {"__type__": "timedelta", "value": obj.total_seconds()}
|
|
45
|
+
case dt.tzinfo():
|
|
46
|
+
offset = obj.utcoffset(dt.datetime.now())
|
|
47
|
+
return {
|
|
48
|
+
"__type__": "tzinfo",
|
|
49
|
+
"value": offset.total_seconds() if offset is not None else None,
|
|
50
|
+
}
|
|
51
|
+
case _:
|
|
52
|
+
return super().default(obj)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SchemaJSONDecoder(JSONDecoder):
|
|
56
|
+
"""Custom JSON decoder to properly deserialize all types serialized by schemas."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
59
|
+
super().__init__(object_hook=self.object_hook, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
def object_hook(self, dct: dict[str, Any]) -> Any:
|
|
62
|
+
if "__type__" not in dct:
|
|
63
|
+
return dct
|
|
64
|
+
|
|
65
|
+
match dct["__type__"]:
|
|
66
|
+
case "tuple":
|
|
67
|
+
return tuple(dct["value"])
|
|
68
|
+
case "expression":
|
|
69
|
+
data = BytesIO(cast(str, dct["value"]).encode("utf-8"))
|
|
70
|
+
return pl.Expr.deserialize(data, format="json")
|
|
71
|
+
case "decimal":
|
|
72
|
+
return decimal.Decimal(dct["value"])
|
|
73
|
+
case "datetime":
|
|
74
|
+
return dt.datetime.fromisoformat(dct["value"])
|
|
75
|
+
case "date":
|
|
76
|
+
return dt.date.fromisoformat(dct["value"])
|
|
77
|
+
case "time":
|
|
78
|
+
return dt.time.fromisoformat(dct["value"])
|
|
79
|
+
case "timedelta":
|
|
80
|
+
return dt.timedelta(seconds=float(dct["value"]))
|
|
81
|
+
case "tzinfo":
|
|
82
|
+
return (
|
|
83
|
+
dt.timezone(dt.timedelta(seconds=float(dct["value"])))
|
|
84
|
+
if dct["value"] is not None
|
|
85
|
+
else dt.timezone(dt.timedelta(0))
|
|
86
|
+
)
|
|
87
|
+
case _:
|
|
88
|
+
raise TypeError(f"Unknown type '{dct['__type__']}' in JSON data.")
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import warnings
|
|
5
5
|
from abc import ABC
|
|
6
6
|
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import asdict
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Self, cast
|
|
9
10
|
|
|
@@ -225,6 +226,65 @@ class Collection(BaseCollection, ABC):
|
|
|
225
226
|
# 3) Eventually, we initialize the final collection and return
|
|
226
227
|
return cls.validate(members)
|
|
227
228
|
|
|
229
|
+
@classmethod
|
|
230
|
+
def matches(cls, other: type["Collection"]) -> bool:
|
|
231
|
+
"""Check whether this collection semantically matches another.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
other: The collection to compare with.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Whether the two collections are semantically equal.
|
|
238
|
+
|
|
239
|
+
Attention:
|
|
240
|
+
For custom filters, reliable comparison results are only guaranteed
|
|
241
|
+
if the filter always returns a static polars expression.
|
|
242
|
+
Otherwise, this function may falsely indicate a match.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def _members_match() -> bool:
|
|
246
|
+
members_lhs = cls.members()
|
|
247
|
+
members_rhs = other.members()
|
|
248
|
+
|
|
249
|
+
# Member names must match
|
|
250
|
+
if members_lhs.keys() != members_rhs.keys():
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
# Member attributes must match
|
|
254
|
+
for name in members_lhs:
|
|
255
|
+
lhs = asdict(members_lhs[name])
|
|
256
|
+
rhs = asdict(members_rhs[name])
|
|
257
|
+
for attr in lhs.keys() | rhs.keys():
|
|
258
|
+
if attr == "schema":
|
|
259
|
+
if not lhs[attr].matches(rhs[attr]):
|
|
260
|
+
return False
|
|
261
|
+
else:
|
|
262
|
+
if lhs[attr] != rhs[attr]:
|
|
263
|
+
return False
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
def _filters_match() -> bool:
|
|
267
|
+
filters_lhs = cls._filters()
|
|
268
|
+
filters_rhs = other._filters()
|
|
269
|
+
|
|
270
|
+
# Filter names must match
|
|
271
|
+
if filters_lhs.keys() != filters_rhs.keys():
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
# Computational graph of filter logic must match
|
|
275
|
+
# Evaluate on empty dataframes
|
|
276
|
+
empty_left = cls.create_empty()
|
|
277
|
+
empty_right = other.create_empty()
|
|
278
|
+
|
|
279
|
+
for name in filters_lhs:
|
|
280
|
+
lhs = filters_lhs[name].logic(empty_left)
|
|
281
|
+
rhs = filters_rhs[name].logic(empty_right)
|
|
282
|
+
if lhs.serialize(format="json") != rhs.serialize(format="json"):
|
|
283
|
+
return False
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
return _members_match() and _filters_match()
|
|
287
|
+
|
|
228
288
|
@classmethod
|
|
229
289
|
def _preprocess_sample(
|
|
230
290
|
cls, sample: dict[str, Any], index: int, generator: Generator
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
3
|
|
|
4
4
|
from ._base import Column
|
|
5
|
+
from ._registry import column_from_dict
|
|
5
6
|
from .any import Any
|
|
6
7
|
from .array import Array
|
|
7
8
|
from .bool import Bool
|
|
@@ -17,6 +18,7 @@ from .struct import Struct
|
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
19
20
|
"Column",
|
|
21
|
+
"column_from_dict",
|
|
20
22
|
"Any",
|
|
21
23
|
"Array",
|
|
22
24
|
"Bool",
|