dataframely 1.8.2__tar.gz → 1.9.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.8.2 → dataframely-1.9.0}/.gitignore +3 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/PKG-INFO +1 -1
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_base_collection.py +3 -3
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_base_schema.py +24 -7
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_polars.py +0 -22
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_rule.py +35 -9
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/collection.py +124 -48
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/failure.py +6 -2
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/schema.py +48 -54
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/factory.py +23 -3
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/rules.py +1 -1
- {dataframely-1.8.2 → dataframely-1.9.0}/pyproject.toml +1 -1
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_filter_validate.py +23 -0
- dataframely-1.9.0/tests/collection/test_join.py +92 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_array.py +1 -7
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_filter.py +36 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_validate.py +20 -0
- dataframely-1.9.0/tests/test_factory.py +89 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_failure_info.py +29 -1
- {dataframely-1.8.2 → dataframely-1.9.0}/.copier-answers.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.envrc +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.gitattributes +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/CODEOWNERS +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/dependabot.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/release-drafter.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/workflows/build.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/workflows/chore.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/workflows/ci.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/workflows/nightly.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.github/workflows/scorecard.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.pre-commit-config.yaml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.prettierignore +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.prettierrc +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/.readthedocs.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/Cargo.lock +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/Cargo.toml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/LICENSE +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/README.md +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/SECURITY.md +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_compat.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_deprecation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_extre.pyi +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_filter.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_serialization.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_typing.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/_validation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/_base.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/_registry.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/any.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/array.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/bool.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/categorical.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/datetime.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/decimal.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/enum.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/float.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/integer.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/list.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/object.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/string.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/columns/struct.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/config.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/exc.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/functional.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/mypy.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/py.typed +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/random.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/const.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/mask.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/dataframely/testing/typing.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docker-compose.yml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/Makefile +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.collection.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.any.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.bool.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.enum.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.float.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.integer.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.list.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.string.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.columns.struct.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.config.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.exc.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.failure.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.functional.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.mypy.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.random.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.schema.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.const.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.factory.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.mask.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.rules.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/dataframely.testing.typing.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_api/modules.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_static/custom.css +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/_static/favicon.ico +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/conf.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/index.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/make.bat +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/development.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/examples/real-world.ipynb +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/faq.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/installation.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/quickstart.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/docs/sites/versioning.rst +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/pixi.lock +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/pixi.toml +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/src/errdefs.rs +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/src/lib.rs +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/src/regex_repr.rs +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/benches/conftest.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/benches/test_collection.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/benches/test_failure.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/benches/test_schema.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_base.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_cast.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_collection_future_annotations.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_matches.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_read_write_parquet.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_repr.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_sample.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_serialization.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_any.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_datetime.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_float.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_list.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_object.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_string.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_alias.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_check.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_matches.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_pyarrow.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_rules.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_sample.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_sql_schema.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_str.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/columns/test_utils.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/core_validation/test_column_validation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/core_validation/test_dtype_validation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/functional/test_concat.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_base.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_cast.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_matches.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_read_write_parquet.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_repr.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_rule_implementation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_sample.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/schema/test_serialization.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_compat.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_config.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_deprecation.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_exc.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_extre.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_random.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_serialization.py +0 -0
- {dataframely-1.8.2 → dataframely-1.9.0}/tests/test_typing.py +0 -0
|
@@ -66,8 +66,8 @@ class CollectionMember:
|
|
|
66
66
|
# --------------------------------------- UTILS -------------------------------------- #
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def _common_primary_keys(
|
|
70
|
-
return set.intersection(*[set(schema.primary_keys()) for schema in
|
|
69
|
+
def _common_primary_keys(schemas: Iterable[type[Schema]]) -> set[str]:
|
|
70
|
+
return set.intersection(*[set(schema.primary_keys()) for schema in schemas])
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
# ------------------------------------------------------------------------------------ #
|
|
@@ -137,7 +137,7 @@ class CollectionMeta(ABCMeta):
|
|
|
137
137
|
for member in result.members.values()
|
|
138
138
|
),
|
|
139
139
|
*(
|
|
140
|
-
set(member.schema._validation_rules())
|
|
140
|
+
set(member.schema._validation_rules(with_cast=True))
|
|
141
141
|
for member in result.members.values()
|
|
142
142
|
),
|
|
143
143
|
)
|
|
@@ -12,7 +12,7 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
import polars as pl
|
|
14
14
|
|
|
15
|
-
from ._rule import GroupRule, Rule
|
|
15
|
+
from ._rule import DtypeCastRule, GroupRule, Rule
|
|
16
16
|
from .columns import Column
|
|
17
17
|
from .exc import ImplementationError
|
|
18
18
|
|
|
@@ -24,11 +24,13 @@ else:
|
|
|
24
24
|
_COLUMN_ATTR = "__dataframely_columns__"
|
|
25
25
|
_RULE_ATTR = "__dataframely_rules__"
|
|
26
26
|
|
|
27
|
+
ORIGINAL_COLUMN_PREFIX = "__DATAFRAMELY_ORIGINAL__"
|
|
28
|
+
|
|
27
29
|
# --------------------------------------- UTILS -------------------------------------- #
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def _build_rules(
|
|
31
|
-
custom: dict[str, Rule], columns: dict[str, Column]
|
|
33
|
+
custom: dict[str, Rule], columns: dict[str, Column], *, with_cast: bool
|
|
32
34
|
) -> dict[str, Rule]:
|
|
33
35
|
# NOTE: Copy here to prevent in-place modification of the custom rules
|
|
34
36
|
rules: dict[str, Rule] = copy(custom)
|
|
@@ -46,6 +48,21 @@ def _build_rules(
|
|
|
46
48
|
}
|
|
47
49
|
rules.update(column_rules)
|
|
48
50
|
|
|
51
|
+
# Add casting rules if requested. Here, we can simply check whether the nullability
|
|
52
|
+
# property of a column changes due to lenient dtype casting. Whenever casting fails,
|
|
53
|
+
# the value is set to `null`, mismatching the previous nullability.
|
|
54
|
+
# NOTE: This check assumes that both the original and cast column are present in the
|
|
55
|
+
# data frame.
|
|
56
|
+
if with_cast:
|
|
57
|
+
casting_rules = {
|
|
58
|
+
f"{col_name}|dtype": DtypeCastRule(
|
|
59
|
+
pl.col(col_name).is_null()
|
|
60
|
+
== pl.col(f"{ORIGINAL_COLUMN_PREFIX}{col_name}").is_null()
|
|
61
|
+
)
|
|
62
|
+
for col_name in columns
|
|
63
|
+
}
|
|
64
|
+
rules.update(casting_rules)
|
|
65
|
+
|
|
49
66
|
return rules
|
|
50
67
|
|
|
51
68
|
|
|
@@ -93,9 +110,7 @@ class SchemaMeta(ABCMeta):
|
|
|
93
110
|
# we assume that users cast dtypes, i.e. additional rules for dtype casting
|
|
94
111
|
# are also checked.
|
|
95
112
|
all_column_names = set(result.columns)
|
|
96
|
-
all_rule_names = set(_build_rules(result.rules, result.columns)
|
|
97
|
-
f"{col}|dtype" for col in result.columns
|
|
98
|
-
)
|
|
113
|
+
all_rule_names = set(_build_rules(result.rules, result.columns, with_cast=True))
|
|
99
114
|
common_names = all_column_names & all_rule_names
|
|
100
115
|
if len(common_names) > 0:
|
|
101
116
|
common_list = ", ".join(sorted(f"'{col}'" for col in common_names))
|
|
@@ -189,8 +204,10 @@ class BaseSchema(metaclass=SchemaMeta):
|
|
|
189
204
|
return _primary_keys(cls.columns())
|
|
190
205
|
|
|
191
206
|
@classmethod
|
|
192
|
-
def _validation_rules(cls) -> dict[str, Rule]:
|
|
193
|
-
return _build_rules(
|
|
207
|
+
def _validation_rules(cls, *, with_cast: bool) -> dict[str, Rule]:
|
|
208
|
+
return _build_rules(
|
|
209
|
+
cls._schema_validation_rules(), cls.columns(), with_cast=with_cast
|
|
210
|
+
)
|
|
194
211
|
|
|
195
212
|
@classmethod
|
|
196
213
|
def _schema_validation_rules(cls) -> dict[str, Rule]:
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
3
|
|
|
4
4
|
import datetime as dt
|
|
5
|
-
from collections.abc import Iterable
|
|
6
5
|
from typing import TypeVar
|
|
7
6
|
|
|
8
7
|
import polars as pl
|
|
@@ -14,27 +13,6 @@ FrameType = TypeVar("FrameType", pl.DataFrame, pl.LazyFrame)
|
|
|
14
13
|
EPOCH_DATETIME = dt.datetime(1970, 1, 1)
|
|
15
14
|
SECONDS_PER_DAY = 86400
|
|
16
15
|
|
|
17
|
-
# --------------------------------------- JOINS -------------------------------------- #
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def join_all_inner(dfs: Iterable[FrameType], on: str | list[str]) -> FrameType:
|
|
21
|
-
it = iter(dfs)
|
|
22
|
-
result = next(it)
|
|
23
|
-
while (df := next(it, None)) is not None:
|
|
24
|
-
result = result.join(df, on=on)
|
|
25
|
-
return result
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def join_all_outer(dfs: Iterable[FrameType], on: str | list[str]) -> FrameType:
|
|
29
|
-
it = iter(dfs)
|
|
30
|
-
result = next(it)
|
|
31
|
-
while (df := next(it, None)) is not None:
|
|
32
|
-
result = result.join(df, on=on, how="full", coalesce=True)
|
|
33
|
-
return result
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# ------------------------------------- DATETIMES ------------------------------------ #
|
|
37
|
-
|
|
38
16
|
|
|
39
17
|
def date_matches_resolution(t: dt.date, resolution: str) -> bool:
|
|
40
18
|
return pl.Series([t], dtype=pl.Date).dt.truncate(resolution).item() == t
|
|
@@ -59,6 +59,14 @@ class Rule:
|
|
|
59
59
|
return str(self.expr)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
class DtypeCastRule(Rule):
|
|
63
|
+
"""Rule that evaluates whether casting a column to another dtype is successful.
|
|
64
|
+
|
|
65
|
+
The only purpose of this rule is to provide a runtime type to distinguish it from
|
|
66
|
+
other rules.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
62
70
|
class GroupRule(Rule):
|
|
63
71
|
"""Rule that is evaluated on a group of columns."""
|
|
64
72
|
|
|
@@ -147,7 +155,8 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
|
|
|
147
155
|
while ``False`` indicates an issue.
|
|
148
156
|
"""
|
|
149
157
|
# Rules must be distinguished into two types of rules:
|
|
150
|
-
# 1. Simple rules can simply be selected on the data frame
|
|
158
|
+
# 1. Simple rules can simply be selected on the data frame (this includes rules
|
|
159
|
+
# that check whether dtype casts succeeded)
|
|
151
160
|
# 2. "Group" rules require a `group_by` and a subsequent join
|
|
152
161
|
simple_exprs = {
|
|
153
162
|
name: rule.expr
|
|
@@ -160,7 +169,7 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
|
|
|
160
169
|
|
|
161
170
|
# Before we can select all of the simple expressions, we need to turn the
|
|
162
171
|
# group rules into something to use in a `select` statement as well.
|
|
163
|
-
|
|
172
|
+
result = (
|
|
164
173
|
# NOTE: A value of `null` always validates successfully as nullability should
|
|
165
174
|
# already be checked via dedicated rules.
|
|
166
175
|
_with_group_rules(lf, group_rules).with_columns(
|
|
@@ -168,6 +177,27 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
|
|
|
168
177
|
)
|
|
169
178
|
)
|
|
170
179
|
|
|
180
|
+
# If there is at least one rule that checks for successful dtype casting, we need
|
|
181
|
+
# to take an extra step: rules other than the "dtype rules" might not be reliable
|
|
182
|
+
# if casting failed, i.e. if any of the "dtype rules" evaluated to `False`. For
|
|
183
|
+
# this reason, we set all other rule evaluations to `null` in the case of dtype
|
|
184
|
+
# casting failure.
|
|
185
|
+
dtype_rule_names = [
|
|
186
|
+
name for name, rule in rules.items() if isinstance(rule, DtypeCastRule)
|
|
187
|
+
]
|
|
188
|
+
if len(dtype_rule_names) > 0:
|
|
189
|
+
non_dtype_rule_names = [
|
|
190
|
+
name for name, rule in rules.items() if not isinstance(rule, DtypeCastRule)
|
|
191
|
+
]
|
|
192
|
+
all_dtype_casts_valid = pl.all_horizontal(dtype_rule_names)
|
|
193
|
+
return result.with_columns(
|
|
194
|
+
pl.when(all_dtype_casts_valid)
|
|
195
|
+
.then(pl.col(non_dtype_rule_names))
|
|
196
|
+
.otherwise(pl.lit(None, dtype=pl.Boolean))
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return result
|
|
200
|
+
|
|
171
201
|
|
|
172
202
|
def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyFrame:
|
|
173
203
|
# First, we partition the rules by group columns. This will minimize the number
|
|
@@ -184,16 +214,12 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
|
|
|
184
214
|
# We group by the group columns and apply all expressions
|
|
185
215
|
group_evaluations[group_columns] = lf.group_by(group_columns).agg(**group_rules)
|
|
186
216
|
|
|
187
|
-
# Eventually, we apply the rule evaluations onto the input data frame. For this,
|
|
188
|
-
#
|
|
189
|
-
# - We're essentially "broadcasting" the results within each group across rows
|
|
190
|
-
# in the same group.
|
|
191
|
-
# - While an inner-join would be semantically more accurate, the left-join
|
|
192
|
-
# preserves the order of the left data frame.
|
|
217
|
+
# Eventually, we apply the rule evaluations onto the input data frame. For this, we
|
|
218
|
+
# "broadcast" the results within each group across rows in the same group.
|
|
193
219
|
result = lf
|
|
194
220
|
for group_columns, frame in group_evaluations.items():
|
|
195
221
|
result = result.join(
|
|
196
|
-
frame, on=list(group_columns),
|
|
222
|
+
frame, on=list(group_columns), nulls_equal=True, maintain_order="left"
|
|
197
223
|
)
|
|
198
224
|
return result
|
|
199
225
|
|
|
@@ -10,14 +10,14 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
10
10
|
from dataclasses import asdict
|
|
11
11
|
from json import JSONDecodeError
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import IO, Annotated, Any, cast
|
|
13
|
+
from typing import IO, Annotated, Any, Literal, cast
|
|
14
14
|
|
|
15
15
|
import polars as pl
|
|
16
16
|
import polars.exceptions as plexc
|
|
17
17
|
|
|
18
18
|
from ._base_collection import BaseCollection, CollectionMember
|
|
19
19
|
from ._filter import Filter
|
|
20
|
-
from ._polars import FrameType
|
|
20
|
+
from ._polars import FrameType
|
|
21
21
|
from ._serialization import (
|
|
22
22
|
COLLECTION_METADATA_KEY,
|
|
23
23
|
SERIALIZATION_FORMAT_VERSION,
|
|
@@ -41,6 +41,8 @@ if sys.version_info >= (3, 11):
|
|
|
41
41
|
else:
|
|
42
42
|
from typing_extensions import Self
|
|
43
43
|
|
|
44
|
+
_FILTER_COLUMN_PREFIX = "__DATAFRAMELY_FILTER_COLUMN__"
|
|
45
|
+
|
|
44
46
|
|
|
45
47
|
class Collection(BaseCollection, ABC):
|
|
46
48
|
"""Base class for all collections of data frames with a predefined schema.
|
|
@@ -376,7 +378,8 @@ class Collection(BaseCollection, ABC):
|
|
|
376
378
|
Returns:
|
|
377
379
|
An instance of the collection. All members of the collection are guaranteed
|
|
378
380
|
to be valid with respect to their respective schemas and the filters on this
|
|
379
|
-
collection did not remove rows from any member.
|
|
381
|
+
collection did not remove rows from any member. The input order of each
|
|
382
|
+
member is maintained.
|
|
380
383
|
"""
|
|
381
384
|
out, failure = cls.filter(data, cast=cast)
|
|
382
385
|
if any(len(fail) > 0 for fail in failure.values()):
|
|
@@ -439,7 +442,9 @@ class Collection(BaseCollection, ABC):
|
|
|
439
442
|
filtered out by any of the collection's filters. While collection members
|
|
440
443
|
are always instances of :class:`~polars.LazyFrame`, the members of the
|
|
441
444
|
returned collection are essentially eager as they are constructed by
|
|
442
|
-
calling ``.lazy()`` on eager data frames.
|
|
445
|
+
calling ``.lazy()`` on eager data frames. Just like in polars' native
|
|
446
|
+
:meth:`~polars.DataFrame.filter`, the order of rows is maintained in all
|
|
447
|
+
returned data frames.
|
|
443
448
|
- A mapping from member name to a :class:`FailureInfo` object which provides
|
|
444
449
|
details on why individual rows had been removed. Optional members are only
|
|
445
450
|
included in this dictionary if they had been provided in the input.
|
|
@@ -465,7 +470,7 @@ class Collection(BaseCollection, ABC):
|
|
|
465
470
|
data[member_name], cast=cast
|
|
466
471
|
)
|
|
467
472
|
|
|
468
|
-
# Once we'
|
|
473
|
+
# Once we've done that, we can apply the filters on this collection. To this end,
|
|
469
474
|
# we iterate over all filters and store the filter results.
|
|
470
475
|
filters = cls._filters()
|
|
471
476
|
if len(filters) > 0:
|
|
@@ -476,41 +481,35 @@ class Collection(BaseCollection, ABC):
|
|
|
476
481
|
for name, filter in filters.items():
|
|
477
482
|
keep[name] = filter.logic(result_cls).select(primary_keys).collect()
|
|
478
483
|
|
|
479
|
-
#
|
|
480
|
-
# the
|
|
481
|
-
all_keep = join_all_inner(
|
|
482
|
-
[df.lazy() for df in keep.values()], on=primary_keys
|
|
483
|
-
).collect()
|
|
484
|
-
|
|
485
|
-
# Now we can iterate over the results where we do the following:
|
|
486
|
-
# - Join the current result onto `all_keep` to get rid of the rows we do not
|
|
487
|
-
# want to keep.
|
|
488
|
-
# - Anti-join onto each individual filter to extend the failure reasons
|
|
484
|
+
# Now we can iterate over the results and left-join onto each individual
|
|
485
|
+
# filter to obtain independent boolean indicators of whether to keep the row
|
|
489
486
|
for member_name, filtered in results.items():
|
|
490
|
-
|
|
487
|
+
member_info = cls.members()[member_name]
|
|
488
|
+
if member_info.ignored_in_filters:
|
|
491
489
|
continue
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
.with_columns(pl.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
# simply
|
|
490
|
+
|
|
491
|
+
lf_with_eval = filtered.lazy()
|
|
492
|
+
for name, filter_keep in keep.items():
|
|
493
|
+
lf_with_eval = lf_with_eval.join(
|
|
494
|
+
filter_keep.lazy().with_columns(pl.lit(True).alias(name)),
|
|
495
|
+
on=primary_keys,
|
|
496
|
+
how="left",
|
|
497
|
+
maintain_order="left",
|
|
498
|
+
).with_columns(pl.col(name).fill_null(False))
|
|
499
|
+
|
|
500
|
+
result_with_eval = lf_with_eval.collect()
|
|
501
|
+
|
|
502
|
+
# Filtering `result_with_eval` by the rows for which all joins
|
|
503
|
+
# "succeeded", we can identify the rows that pass all the filters. We
|
|
504
|
+
# keep these rows for the result.
|
|
505
|
+
results[member_name] = result_with_eval.filter(
|
|
506
|
+
pl.all_horizontal(keep.keys())
|
|
507
|
+
).drop(keep.keys())
|
|
508
|
+
|
|
509
|
+
# Filtering `result_with_eval` with the inverse condition, we find all
|
|
510
|
+
# the problematic rows. We can build a single failure info object by
|
|
511
|
+
# simply concatenating diagonally with the already existing failure. The
|
|
512
|
+
# resulting failure info looks as follows:
|
|
514
513
|
#
|
|
515
514
|
# | Source Data | Rule Columns (schema) | Filter Name Columns (collection) |
|
|
516
515
|
# | ----------- | --------------------- | -------------------------------- |
|
|
@@ -518,21 +517,98 @@ class Collection(BaseCollection, ABC):
|
|
|
518
517
|
# | ... | NULL | <filled> |
|
|
519
518
|
#
|
|
520
519
|
failure = failures[member_name]
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
520
|
+
filtered_failure = result_with_eval.filter(
|
|
521
|
+
~pl.all_horizontal(keep.keys())
|
|
522
|
+
).lazy()
|
|
523
|
+
|
|
524
|
+
# If we cast previously, `failure` and `filtered_failure` have different
|
|
525
|
+
# dtypes for the source data: `failure` keeps the original dtypes while
|
|
526
|
+
# `filtered_failure` has the target dtypes. Hence, we need to cast
|
|
527
|
+
# `filtered_failure` to the original dtypes. This is safe because any
|
|
528
|
+
# row in `filtered_failure` must have already been successfully cast and
|
|
529
|
+
# a "roundtrip cast" is always possible.
|
|
530
|
+
# Doing this in a fully lazy way is not trivial: we do a diagonal
|
|
531
|
+
# concatenation where we duplicate each column of the source data. We
|
|
532
|
+
# then coalesce the two versions into the original column dtype.
|
|
533
|
+
if cast:
|
|
534
|
+
filtered_failure = filtered_failure.rename(
|
|
535
|
+
{
|
|
536
|
+
name: f"{_FILTER_COLUMN_PREFIX}{name}"
|
|
537
|
+
for name in member_info.schema.column_names()
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
failure_lf = pl.concat([failure._lf, filtered_failure], how="diagonal")
|
|
542
|
+
if cast:
|
|
543
|
+
failure_lf = failure_lf.with_columns(
|
|
544
|
+
pl.coalesce(
|
|
545
|
+
name,
|
|
546
|
+
pl.col(f"{_FILTER_COLUMN_PREFIX}{name}").cast(
|
|
547
|
+
pl.dtype_of(name)
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
for name in member_info.schema.column_names()
|
|
551
|
+
).drop(
|
|
552
|
+
f"{_FILTER_COLUMN_PREFIX}{name}"
|
|
553
|
+
for name in member_info.schema.column_names()
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
failures[member_name] = FailureInfo(
|
|
557
|
+
lf=failure_lf,
|
|
558
|
+
rule_columns=failure._rule_columns + list(keep.keys()),
|
|
530
559
|
schema=failure.schema,
|
|
531
560
|
)
|
|
532
|
-
failures[member_name] = new_failure
|
|
533
561
|
|
|
534
562
|
return cls._init(results), failures
|
|
535
563
|
|
|
564
|
+
def join(
|
|
565
|
+
self,
|
|
566
|
+
primary_keys: pl.LazyFrame,
|
|
567
|
+
how: Literal["semi", "anti"] = "semi",
|
|
568
|
+
maintain_order: Literal["none", "left"] = "none",
|
|
569
|
+
) -> Self:
|
|
570
|
+
"""Filter the collection by joining onto a data frame containing entries for the
|
|
571
|
+
common primary key columns whose respective rows should be kept or removed in
|
|
572
|
+
the collection members.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
primary_keys: The data frame to join on. Must contain the common primary key
|
|
576
|
+
columns of the collection.
|
|
577
|
+
how: The join strategy to use. Like in polars, `semi` will keep all rows
|
|
578
|
+
that can be found in `primary_keys`, `anti` will remove them.
|
|
579
|
+
maintain_order: The `maintain_order` option to use for the polars join.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
The collection, with members potentially reduced in length.
|
|
583
|
+
|
|
584
|
+
Raises:
|
|
585
|
+
ValueError: If the collection contains any member that is annotated with
|
|
586
|
+
`ignored_in_filters=True`.
|
|
587
|
+
|
|
588
|
+
Attention:
|
|
589
|
+
This method does not validate the resulting collection. Ensure to only use
|
|
590
|
+
this if the resulting collection still satisfies the filters of the
|
|
591
|
+
collection. The joins are not evaluated eagerly. Therefore, a downstream
|
|
592
|
+
call to :meth:`collect` might fail, especially if `primary_keys` does not
|
|
593
|
+
contain all columns for all common primary keys.
|
|
594
|
+
"""
|
|
595
|
+
if any(member.ignored_in_filters for member in self.members().values()):
|
|
596
|
+
raise ValueError(
|
|
597
|
+
"The join operation is not supported for collections with members that are ignored in filters."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return self.cast(
|
|
601
|
+
{
|
|
602
|
+
key: lf.join(
|
|
603
|
+
primary_keys,
|
|
604
|
+
on=self.common_primary_keys(),
|
|
605
|
+
how=how,
|
|
606
|
+
maintain_order=maintain_order,
|
|
607
|
+
)
|
|
608
|
+
for key, lf in self.to_dict().items()
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
|
|
536
612
|
# ------------------------------------ CASTING ----------------------------------- #
|
|
537
613
|
|
|
538
614
|
@classmethod
|
|
@@ -19,6 +19,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
19
19
|
from .schema import Schema
|
|
20
20
|
|
|
21
21
|
RULE_METADATA_KEY = "dataframely_rule_columns"
|
|
22
|
+
UNKNOWN_SCHEMA_NAME = "__DATAFRAMELY_UNKNOWN__"
|
|
22
23
|
|
|
23
24
|
S = TypeVar("S", bound=BaseSchema)
|
|
24
25
|
|
|
@@ -176,7 +177,7 @@ class FailureInfo(Generic[S]):
|
|
|
176
177
|
def _from_parquet(
|
|
177
178
|
cls, source: str | Path | IO[bytes], scan: bool, **kwargs: Any
|
|
178
179
|
) -> FailureInfo[Schema]:
|
|
179
|
-
from .schema import deserialize_schema
|
|
180
|
+
from .schema import Schema, deserialize_schema
|
|
180
181
|
|
|
181
182
|
metadata = pl.read_parquet_metadata(source)
|
|
182
183
|
schema_metadata = metadata.get(SCHEMA_METADATA_KEY)
|
|
@@ -189,10 +190,13 @@ class FailureInfo(Generic[S]):
|
|
|
189
190
|
if scan
|
|
190
191
|
else pl.read_parquet(source, **kwargs).lazy()
|
|
191
192
|
)
|
|
193
|
+
failure_schema = deserialize_schema(schema_metadata, strict=False) or type(
|
|
194
|
+
UNKNOWN_SCHEMA_NAME, (Schema,), {}
|
|
195
|
+
)
|
|
192
196
|
return FailureInfo(
|
|
193
197
|
lf,
|
|
194
198
|
json.loads(rule_metadata),
|
|
195
|
-
schema=
|
|
199
|
+
schema=failure_schema,
|
|
196
200
|
)
|
|
197
201
|
|
|
198
202
|
|