dataframely 2.0.0__tar.gz → 2.2.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-2.2.0/.github/copilot-instructions.md +214 -0
- dataframely-2.2.0/.github/instructions/tests.instructions.md +9 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/workflows/build.yml +6 -6
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/workflows/ci.yml +2 -2
- dataframely-2.2.0/.github/workflows/copilot-setup-steps.yml +26 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/workflows/nightly.yml +1 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/workflows/scorecard.yml +2 -2
- {dataframely-2.0.0 → dataframely-2.2.0}/PKG-INFO +2 -2
- {dataframely-2.0.0 → dataframely-2.2.0}/README.md +1 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/__init__.py +2 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_base_schema.py +13 -9
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_pydantic.py +2 -2
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/collection/collection.py +92 -41
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/array.py +25 -23
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/list.py +37 -24
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/struct.py +1 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/exc.py +7 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/filter_result.py +16 -4
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/schema.py +36 -5
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/storage.py +103 -12
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/conf.py +1 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/faq.md +1 -14
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/migration/v1-v2.md +1 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/quickstart.md +3 -4
- {dataframely-2.0.0 → dataframely-2.2.0}/pixi.lock +9040 -8759
- {dataframely-2.0.0 → dataframely-2.2.0}/pixi.toml +1 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/pyproject.toml +2 -2
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_repr.py +2 -2
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_serialization.py +35 -3
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_storage.py +57 -7
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_array.py +23 -13
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_pyarrow.py +96 -7
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_serialization.py +4 -4
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_storage.py +62 -1
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_typing.py +13 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.copier-answers.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.envrc +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.gitattributes +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/CODEOWNERS +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/dependabot.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/release-drafter.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.github/workflows/chore.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.gitignore +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.pre-commit-config.yaml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.prettierignore +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.prettierrc +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/.readthedocs.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/Cargo.lock +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/Cargo.toml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/LICENSE +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/SECURITY.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_compat.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_deprecation.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_filter.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_match_to_schema.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_native.pyi +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_plugin.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_polars.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_rule.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_serialization.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/_exc.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/constants.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/delta.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_storage/parquet.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/_typing.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/collection/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/collection/_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/collection/filter_result.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/_registry.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/any.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/binary.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/bool.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/categorical.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/datetime.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/decimal.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/enum.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/float.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/integer.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/object.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/columns/string.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/config.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/functional.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/py.typed +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/random.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/const.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/factory.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/mask.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/dataframely/testing/rules.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docker-compose.yml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_static/custom.css +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_static/favicon.ico +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_templates/autosummary/class.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_templates/autosummary/method.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_templates/classes/column.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_templates/classes/error.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/_templates/classes/filter_result.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/generation.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/io.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/metadata.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/operations.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/collection/validation.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/columns/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/errors/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/filter_result/failure_info.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/filter_result/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/misc/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/conversion.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/generation.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/index.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/io.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/metadata.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/api/schema/validation.rst +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/css/custom.css +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/development.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/examples/index.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/examples/real-world.ipynb +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/column-metadata.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/data-generation.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/index.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/lazy-validation.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/primary-keys.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/serialization.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/features/sql-generation.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/index.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/guides/migration/index.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/docs/index.md +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/rust-toolchain.toml +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/lib.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/polars_plugin/mod.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/polars_plugin/rule_failure.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/polars_plugin/utils.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/polars_plugin/validation_error.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/regex/errdefs.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/regex/mod.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/src/regex/repr.rs +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/benches/conftest.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/benches/test_collection.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/benches/test_failure.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/benches/test_schema.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_cast.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_collection_future_annotations.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_join.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_matches.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_sample.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_any.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_binary.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_datetime.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_float.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_list.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_object.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_string.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_alias.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_check.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_matches.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_rules.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_sample.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_sqlalchemy_columns.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_str.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/columns/test_utils.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/conftest.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/core_validation/test_match_to_schema.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/failure_info/test_storage.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/functional/test_concat.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_base.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_cast.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_filter.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_matches.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_read_write_parquet.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_repr.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_rule_implementation.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_sample.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/schema/test_validate.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/storage/test_delta.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_compat.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_config.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_deprecation.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_factory.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_native_regex.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_pydantic.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_random.py +0 -0
- {dataframely-2.0.0 → dataframely-2.2.0}/tests/test_serialization.py +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Dataframely - Coding Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Dataframely is a declarative, polars-native data frame validation library. It validates schemas and data content in
|
|
6
|
+
polars DataFrames using native polars expressions and a custom Rust-based polars plugin for high performance. It
|
|
7
|
+
supports validating individual data frames via `Schema` classes and interconnected data frames via `Collection` classes.
|
|
8
|
+
|
|
9
|
+
## Tech Stack
|
|
10
|
+
|
|
11
|
+
### Core Technologies
|
|
12
|
+
|
|
13
|
+
- **Python**: Primary language for the public API
|
|
14
|
+
- **Rust**: Backend for polars plugin and custom regex operations
|
|
15
|
+
- **Polars**: Only supported data frame library
|
|
16
|
+
- **pyo3 & maturin**: Rust-Python bindings and build system
|
|
17
|
+
- **pixi**: Primary environment and task manager (NOT pip/conda directly)
|
|
18
|
+
|
|
19
|
+
### Build System
|
|
20
|
+
|
|
21
|
+
- **maturin**: Builds the Rust extension module `dataframely._native`
|
|
22
|
+
- **Cargo**: Rust dependency management
|
|
23
|
+
- Rust toolchain specified in `rust-toolchain.toml` with clippy and rustfmt components
|
|
24
|
+
|
|
25
|
+
## Environment Setup
|
|
26
|
+
|
|
27
|
+
**CRITICAL**: Always use `pixi` commands - never run `pip`, `conda`, `python`, or `cargo` directly unless specifically
|
|
28
|
+
required for Rust-only operations.
|
|
29
|
+
|
|
30
|
+
### Initial Setup
|
|
31
|
+
|
|
32
|
+
Unless already performed via external setup steps:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install Rust toolchain
|
|
36
|
+
rustup show
|
|
37
|
+
|
|
38
|
+
# Install pixi environment and dependencies
|
|
39
|
+
pixi install
|
|
40
|
+
|
|
41
|
+
# Build and install the package locally (REQUIRED after Rust changes)
|
|
42
|
+
pixi run postinstall
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### After Rust Code Changes
|
|
46
|
+
|
|
47
|
+
**Always run** `pixi run postinstall` after modifying any Rust code in `src/` to rebuild the native extension.
|
|
48
|
+
|
|
49
|
+
## Development Workflow
|
|
50
|
+
|
|
51
|
+
### Running Tests
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Run all tests (excludes S3 tests by default)
|
|
55
|
+
pixi run test
|
|
56
|
+
|
|
57
|
+
# Run tests with S3 backend (requires moto server)
|
|
58
|
+
pixi run test -m s3
|
|
59
|
+
|
|
60
|
+
# Run specific test file or directory
|
|
61
|
+
pixi run test tests/schema/
|
|
62
|
+
|
|
63
|
+
# Run with coverage
|
|
64
|
+
pixi run test-coverage
|
|
65
|
+
|
|
66
|
+
# Run benchmarks
|
|
67
|
+
pixi run test-bench
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Code Quality
|
|
71
|
+
|
|
72
|
+
**NEVER** run linters/formatters directly. Use pre-commit:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Run all pre-commit hooks
|
|
76
|
+
pixi run pre-commit run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Pre-commit handles:
|
|
80
|
+
|
|
81
|
+
- **Python**: ruff (lint & format), mypy (type checking), docformatter
|
|
82
|
+
- **Rust**: cargo fmt, cargo clippy
|
|
83
|
+
- **Other**: prettier (md/yml), taplo (toml), license headers, trailing whitespace
|
|
84
|
+
|
|
85
|
+
### Building Documentation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Build documentation
|
|
89
|
+
pixi run -e docs postinstall
|
|
90
|
+
pixi run docs
|
|
91
|
+
|
|
92
|
+
# Open in browser (macOS)
|
|
93
|
+
open docs/_build/html/index.html
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Project Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
dataframely/ # Python package
|
|
100
|
+
schema.py # Core Schema class for DataFrame validation
|
|
101
|
+
collection/ # Collection class for validating multiple interconnected DataFrames
|
|
102
|
+
columns/ # Column type definitions (String, Integer, Float, etc.)
|
|
103
|
+
testing/ # Testing utilities (factories, masks, storage mocks)
|
|
104
|
+
_storage/ # Storage backends (Parquet, Delta Lake)
|
|
105
|
+
_rule.py # Rule decorator for validation rules
|
|
106
|
+
_plugin.py # Polars plugin registration
|
|
107
|
+
_native.pyi # Type stubs for Rust extension
|
|
108
|
+
|
|
109
|
+
src/ # Rust source code
|
|
110
|
+
lib.rs # PyO3 module definition
|
|
111
|
+
polars_plugin/ # Custom polars plugin for validation
|
|
112
|
+
regex/ # Custom regex operations
|
|
113
|
+
|
|
114
|
+
tests/ # Unit tests (mirrors dataframely/ structure)
|
|
115
|
+
benches/ # Benchmark tests
|
|
116
|
+
conftest.py # Shared pytest fixtures (including s3_server)
|
|
117
|
+
|
|
118
|
+
docs/ # Sphinx documentation
|
|
119
|
+
guides/ # User guides and examples
|
|
120
|
+
api/ # Auto-generated API reference
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Pixi Environments
|
|
124
|
+
|
|
125
|
+
Multiple environments for different purposes:
|
|
126
|
+
|
|
127
|
+
- **default**: Base Python + core dependencies
|
|
128
|
+
- **dev**: Includes jupyter for notebooks
|
|
129
|
+
- **test**: Testing dependencies (pytest, moto, boto3, etc.)
|
|
130
|
+
- **docs**: Documentation building (sphinx, myst-parser, etc.)
|
|
131
|
+
- **lint**: Linting and formatting tools
|
|
132
|
+
- **optionals**: Optional dependencies (pydantic, deltalake, pyarrow, sqlalchemy)
|
|
133
|
+
- **py310-py314**: Python version-specific environments
|
|
134
|
+
|
|
135
|
+
Use `-e <env>` to run commands in specific environments:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pixi run -e test test
|
|
139
|
+
pixi run -e docs docs
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## API Design Principles
|
|
143
|
+
|
|
144
|
+
### Critical Guidelines
|
|
145
|
+
|
|
146
|
+
1. **NO BREAKING CHANGES**: Public API must remain backward compatible
|
|
147
|
+
2. **100% Test Coverage**: All new code requires tests
|
|
148
|
+
3. **Documentation Required**: All public features need docstrings + API docs
|
|
149
|
+
4. **Cautious API Extension**: Avoid adding to public API unless necessary
|
|
150
|
+
|
|
151
|
+
### Public API
|
|
152
|
+
|
|
153
|
+
Public exports are in `dataframely/__init__.py`. Main components:
|
|
154
|
+
|
|
155
|
+
- **Schema classes**: `Schema` for DataFrame validation
|
|
156
|
+
- **Collection classes**: `Collection`, `CollectionMember` for multi-DataFrame validation
|
|
157
|
+
- **Column types**: `String`, `Integer`, `Float`, `Bool`, `Date`, `Datetime`, etc.
|
|
158
|
+
- **Decorators**: `@rule()`, `@filter()`
|
|
159
|
+
- **Type hints**: `DataFrame[Schema]`, `LazyFrame[Schema]`, `Validation`
|
|
160
|
+
|
|
161
|
+
## Common Pitfalls & Solutions
|
|
162
|
+
|
|
163
|
+
### S3 Testing
|
|
164
|
+
|
|
165
|
+
The `s3_server` fixture in `tests/conftest.py` uses `subprocess.Popen` to start moto_server on port 9999. This is a **workaround** for a polars issue with ThreadedMotoServer. When the polars issue is fixed, it should be replaced with ThreadedMotoServer (code is commented in the file).
|
|
166
|
+
|
|
167
|
+
**Note**: CI skips S3 tests by default. Run with `pixi run test -m s3` when modifying storage backends.
|
|
168
|
+
|
|
169
|
+
## Testing Strategy
|
|
170
|
+
|
|
171
|
+
- Tests are organized by module, mirroring the `dataframely/` structure
|
|
172
|
+
- Use `dy.Schema.sample()` for generating test data
|
|
173
|
+
- Test both eager (`DataFrame`) and lazy (`LazyFrame`) execution
|
|
174
|
+
- S3 tests use moto server fixture from `conftest.py`
|
|
175
|
+
- Benchmark tests in `tests/benches/` use pytest-benchmark
|
|
176
|
+
|
|
177
|
+
## Validation Pattern
|
|
178
|
+
|
|
179
|
+
Typical usage pattern:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
class MySchema(dy.Schema):
|
|
183
|
+
col = dy.String(nullable=False)
|
|
184
|
+
|
|
185
|
+
@dy.rule()
|
|
186
|
+
def my_rule(cls) -> pl.Expr:
|
|
187
|
+
return pl.col("col").str.len_chars() > 0
|
|
188
|
+
|
|
189
|
+
# Validate and cast
|
|
190
|
+
validated_df: dy.DataFrame[MySchema] = MySchema.validate(df, cast=True)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Key Configuration Files
|
|
194
|
+
|
|
195
|
+
- `pixi.toml`: Environment and task definitions
|
|
196
|
+
- `pyproject.toml`: Python package metadata, tool configurations (ruff, mypy, pytest)
|
|
197
|
+
- `Cargo.toml`: Rust dependencies and build settings
|
|
198
|
+
- `.pre-commit-config.yaml`: All code quality checks
|
|
199
|
+
- `rust-toolchain.toml`: Rust nightly version specification
|
|
200
|
+
|
|
201
|
+
## When Making Changes
|
|
202
|
+
|
|
203
|
+
1. **Python code**: Run `pixi run pre-commit run` before committing
|
|
204
|
+
2. **Rust code**: Run `pixi run postinstall` to rebuild, then run tests
|
|
205
|
+
3. **Tests**: Ensure `pixi run test` passes. If changes might affect storage backends, use `pixi run test -m s3`.
|
|
206
|
+
4. **Documentation**: Update docstrings
|
|
207
|
+
5. **API changes**: Ensure backward compatibility or document migration path
|
|
208
|
+
|
|
209
|
+
## Performance Considerations
|
|
210
|
+
|
|
211
|
+
- Validation uses native polars expressions for performance
|
|
212
|
+
- Custom Rust plugin for advanced validation logic
|
|
213
|
+
- Lazy evaluation supported via `LazyFrame` for large datasets
|
|
214
|
+
- Avoid materializing data unnecessarily in validation rules
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: tests/**/*.py
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Testing Guidelines
|
|
6
|
+
|
|
7
|
+
- Instead of writing new tests, existing tests should ideally be parametrized using `@pytest.mark.parametrize`
|
|
8
|
+
unless the parametrization is impractical, e.g. by adding more parameters
|
|
9
|
+
- Tests should not use docstrings unless they are _very_ complex
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
with:
|
|
18
18
|
fetch-depth: 0
|
|
19
19
|
- name: Set up pixi
|
|
20
|
-
uses: prefix-dev/setup-pixi@
|
|
20
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
21
21
|
with:
|
|
22
22
|
environments: build
|
|
23
23
|
- name: Set version
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
- name: Build project
|
|
26
26
|
run: pixi run -e build build-sdist
|
|
27
27
|
- name: Upload package
|
|
28
|
-
uses: actions/upload-artifact@
|
|
28
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
29
29
|
with:
|
|
30
30
|
name: sdist
|
|
31
31
|
path: dist/*
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
- target-platform: linux-aarch64
|
|
43
43
|
os: ubuntu-24.04-arm
|
|
44
44
|
- target-platform: osx-64
|
|
45
|
-
os: macos-
|
|
45
|
+
os: macos-15-intel
|
|
46
46
|
- target-platform: osx-arm64
|
|
47
47
|
os: macos-latest
|
|
48
48
|
- target-platform: win-64
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
with:
|
|
53
53
|
fetch-depth: 0
|
|
54
54
|
- name: Set up pixi
|
|
55
|
-
uses: prefix-dev/setup-pixi@
|
|
55
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
56
56
|
with:
|
|
57
57
|
environments: build
|
|
58
58
|
- name: Set version
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
|
70
70
|
- name: Check package
|
|
71
71
|
run: pixi run -e build check-wheel
|
|
72
72
|
- name: Upload package
|
|
73
|
-
uses: actions/upload-artifact@
|
|
73
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
74
74
|
with:
|
|
75
75
|
name: wheel-${{ matrix.target-platform }}
|
|
76
76
|
path: dist/*
|
|
@@ -84,7 +84,7 @@ jobs:
|
|
|
84
84
|
id-token: write
|
|
85
85
|
environment: pypi
|
|
86
86
|
steps:
|
|
87
|
-
- uses: actions/download-artifact@
|
|
87
|
+
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
88
88
|
with:
|
|
89
89
|
path: dist
|
|
90
90
|
merge-multiple: true
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
# needed for 'pre-commit-mirrors-insert-license'
|
|
25
25
|
fetch-depth: 0
|
|
26
26
|
- name: Set up pixi
|
|
27
|
-
uses: prefix-dev/setup-pixi@
|
|
27
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
28
28
|
with:
|
|
29
29
|
environments: default lint
|
|
30
30
|
- name: Install Rust
|
|
@@ -58,7 +58,7 @@ jobs:
|
|
|
58
58
|
- name: Checkout branch
|
|
59
59
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
60
60
|
- name: Set up pixi
|
|
61
|
-
uses: prefix-dev/setup-pixi@
|
|
61
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
62
62
|
with:
|
|
63
63
|
environments: ${{ matrix.environment }}
|
|
64
64
|
# FIXME: Remove when `s3_server` fixture does not start a process anymore
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Copilot Setup Steps
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
paths:
|
|
5
|
+
- .github/workflows/copilot-setup-steps.yml
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
copilot-setup-steps:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout branch
|
|
16
|
+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
17
|
+
- name: Set up pixi
|
|
18
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
19
|
+
with:
|
|
20
|
+
environments: default
|
|
21
|
+
- name: Install Rust
|
|
22
|
+
run: rustup show
|
|
23
|
+
- name: Cache Rust dependencies
|
|
24
|
+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
|
25
|
+
- name: Install repository
|
|
26
|
+
run: pixi run postinstall
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
- name: Checkout branch
|
|
26
26
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
27
27
|
- name: Set up pixi
|
|
28
|
-
uses: prefix-dev/setup-pixi@
|
|
28
|
+
uses: prefix-dev/setup-pixi@28eb668aafebd9dede9d97c4ba1cd9989a4d0004 # v0.9.2
|
|
29
29
|
with:
|
|
30
30
|
environments: nightly
|
|
31
31
|
- name: Install polars nightly
|
|
@@ -65,7 +65,7 @@ jobs:
|
|
|
65
65
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
|
66
66
|
# format to the repository Actions tab.
|
|
67
67
|
- name: "Upload artifact"
|
|
68
|
-
uses: actions/upload-artifact@
|
|
68
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
69
69
|
with:
|
|
70
70
|
name: SARIF file
|
|
71
71
|
path: results.sarif
|
|
@@ -74,6 +74,6 @@ jobs:
|
|
|
74
74
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
|
75
75
|
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
|
76
76
|
- name: "Upload to code-scanning"
|
|
77
|
-
uses: github/codeql-action/upload-sarif@
|
|
77
|
+
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5
|
|
78
78
|
with:
|
|
79
79
|
sarif_file: results.sarif
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframely
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.10
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -109,5 +109,5 @@ df = pl.DataFrame({
|
|
|
109
109
|
validated_df: dy.DataFrame[HouseSchema] = HouseSchema.validate(df, cast=True)
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
See more advanced usage examples in the [documentation](https://dataframely.readthedocs.io/
|
|
112
|
+
See more advanced usage examples in the [documentation](https://dataframely.readthedocs.io/stable/).
|
|
113
113
|
|
|
@@ -78,4 +78,4 @@ df = pl.DataFrame({
|
|
|
78
78
|
validated_df: dy.DataFrame[HouseSchema] = HouseSchema.validate(df, cast=True)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
See more advanced usage examples in the [documentation](https://dataframely.readthedocs.io/
|
|
81
|
+
See more advanced usage examples in the [documentation](https://dataframely.readthedocs.io/stable/).
|
|
@@ -51,6 +51,7 @@ from .columns import (
|
|
|
51
51
|
UInt64,
|
|
52
52
|
)
|
|
53
53
|
from .config import Config
|
|
54
|
+
from .exc import DeserializationError
|
|
54
55
|
from .filter_result import FailureInfo
|
|
55
56
|
from .functional import (
|
|
56
57
|
concat_collection_members,
|
|
@@ -106,4 +107,5 @@ __all__ = [
|
|
|
106
107
|
"Array",
|
|
107
108
|
"Object",
|
|
108
109
|
"Validation",
|
|
110
|
+
"DeserializationError",
|
|
109
111
|
]
|
|
@@ -8,7 +8,7 @@ import textwrap
|
|
|
8
8
|
from abc import ABCMeta
|
|
9
9
|
from copy import copy
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
13
|
import polars as pl
|
|
14
14
|
|
|
@@ -164,12 +164,16 @@ class SchemaMeta(ABCMeta):
|
|
|
164
164
|
|
|
165
165
|
return cls
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
if not TYPE_CHECKING:
|
|
168
|
+
# Only define __getattribute__ at runtime to allow type checkers to properly
|
|
169
|
+
# validate attribute access. When TYPE_CHECKING is True, type checkers will use
|
|
170
|
+
# the default metaclass behavior which correctly identifies non-existent attributes.
|
|
171
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
172
|
+
val = super().__getattribute__(name)
|
|
173
|
+
# Dynamically set the name of the column if it is a `Column` instance.
|
|
174
|
+
if isinstance(val, Column):
|
|
175
|
+
val._name = val.alias or name
|
|
176
|
+
return val
|
|
173
177
|
|
|
174
178
|
@staticmethod
|
|
175
179
|
def _get_metadata_recursively(kls: type[object]) -> Metadata:
|
|
@@ -199,9 +203,9 @@ class SchemaMeta(ABCMeta):
|
|
|
199
203
|
def __repr__(cls) -> str:
|
|
200
204
|
parts = [f'[Schema "{cls.__name__}"]']
|
|
201
205
|
parts.append(textwrap.indent("Columns:", prefix=" " * 2))
|
|
202
|
-
for name, col in cls.columns().items():
|
|
206
|
+
for name, col in cls.columns().items(): # type: ignore[attr-defined]
|
|
203
207
|
parts.append(textwrap.indent(f'- "{name}": {col!r}', prefix=" " * 4))
|
|
204
|
-
if validation_rules := cls._schema_validation_rules():
|
|
208
|
+
if validation_rules := cls._schema_validation_rules(): # type: ignore[attr-defined]
|
|
205
209
|
parts.append(textwrap.indent("Rules:", prefix=" " * 2))
|
|
206
210
|
for name, rule in validation_rules.items():
|
|
207
211
|
parts.append(textwrap.indent(f'- "{name}": {rule!r}', prefix=" " * 4))
|
|
@@ -20,12 +20,12 @@ _S = TypeVar("_S", bound=BaseSchema)
|
|
|
20
20
|
def _dict_to_df(schema_type: type[BaseSchema], data: dict) -> pl.DataFrame:
|
|
21
21
|
return pl.from_dict(
|
|
22
22
|
data,
|
|
23
|
-
schema=schema_type.to_polars_schema(),
|
|
23
|
+
schema=schema_type.to_polars_schema(), # type: ignore[attr-defined]
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _validate_df_schema(schema_type: type[_S], df: pl.DataFrame) -> DataFrame[_S]:
|
|
28
|
-
if not schema_type.is_valid(df):
|
|
28
|
+
if not schema_type.is_valid(df): # type: ignore[attr-defined]
|
|
29
29
|
raise ValueError("DataFrame violates schema")
|
|
30
30
|
return df # type: ignore
|
|
31
31
|
|
|
@@ -12,7 +12,7 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
12
12
|
from dataclasses import asdict
|
|
13
13
|
from json import JSONDecodeError
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import IO, Annotated, Any, Literal, cast
|
|
15
|
+
from typing import IO, Annotated, Any, Literal, cast, overload
|
|
16
16
|
|
|
17
17
|
import polars as pl
|
|
18
18
|
import polars.exceptions as plexc
|
|
@@ -33,7 +33,11 @@ from dataframely._storage.constants import COLLECTION_METADATA_KEY
|
|
|
33
33
|
from dataframely._storage.delta import DeltaStorageBackend
|
|
34
34
|
from dataframely._storage.parquet import ParquetStorageBackend
|
|
35
35
|
from dataframely._typing import LazyFrame, Validation
|
|
36
|
-
from dataframely.exc import
|
|
36
|
+
from dataframely.exc import (
|
|
37
|
+
DeserializationError,
|
|
38
|
+
ValidationError,
|
|
39
|
+
ValidationRequiredError,
|
|
40
|
+
)
|
|
37
41
|
from dataframely.filter_result import FailureInfo
|
|
38
42
|
from dataframely.random import Generator
|
|
39
43
|
from dataframely.schema import _schema_from_dict
|
|
@@ -531,6 +535,25 @@ class Collection(BaseCollection, ABC):
|
|
|
531
535
|
Raises:
|
|
532
536
|
ValueError: If an insufficient set of input data frames is provided, i.e. if
|
|
533
537
|
any required member of this collection is missing in the input.
|
|
538
|
+
|
|
539
|
+
Example:
|
|
540
|
+
|
|
541
|
+
.. code-block:: python
|
|
542
|
+
|
|
543
|
+
# Define collection
|
|
544
|
+
class HospitalInvoiceData(dy.Collection):
|
|
545
|
+
invoice: dy.LazyFrame[InvoiceSchema]
|
|
546
|
+
...
|
|
547
|
+
|
|
548
|
+
# Filter the data and cast columns to expected types
|
|
549
|
+
good, failure = HospitalInvoiceData.filter(df, cast=True)
|
|
550
|
+
|
|
551
|
+
# Inspect the reasons for the failed rows for member `invoice`
|
|
552
|
+
print(failure.invoice.counts())
|
|
553
|
+
|
|
554
|
+
# Inspect the failed rows
|
|
555
|
+
failed_df = failure.invoice.invalid()
|
|
556
|
+
print(failed_df)
|
|
534
557
|
"""
|
|
535
558
|
cls._validate_input_keys(data)
|
|
536
559
|
|
|
@@ -872,13 +895,13 @@ class Collection(BaseCollection, ABC):
|
|
|
872
895
|
- `"allow"`: The method tries to read the schema data from the parquet
|
|
873
896
|
files. If the stored collection schema matches this collection
|
|
874
897
|
schema, the collection is read without validation. If the stored
|
|
875
|
-
schema mismatches this schema no metadata can be found in
|
|
898
|
+
schema mismatches this schema, no valid metadata can be found in
|
|
876
899
|
the parquets, or the files have conflicting metadata,
|
|
877
900
|
this method automatically runs :meth:`validate` with `cast=True`.
|
|
878
901
|
- `"warn"`: The method behaves similarly to `"allow"`. However,
|
|
879
902
|
it prints a warning if validation is necessary.
|
|
880
903
|
- `"forbid"`: The method never runs validation automatically and only
|
|
881
|
-
returns if the metadata stores a collection schema that matches
|
|
904
|
+
returns if the metadata stores a valid collection schema that matches
|
|
882
905
|
this collection.
|
|
883
906
|
- `"skip"`: The method never runs validation and simply reads the
|
|
884
907
|
data, entrusting the user that the schema is valid. *Use this option
|
|
@@ -1165,7 +1188,12 @@ class Collection(BaseCollection, ABC):
|
|
|
1165
1188
|
members=cls.member_schemas().keys(), **kwargs
|
|
1166
1189
|
)
|
|
1167
1190
|
|
|
1168
|
-
|
|
1191
|
+
# Use strict=False when validation is "allow", "warn" or "skip" to tolerate
|
|
1192
|
+
# missing or broken collection metadata.
|
|
1193
|
+
strict = validation == "forbid"
|
|
1194
|
+
collection_types = _deserialize_types(
|
|
1195
|
+
serialized_collection_types, strict=strict
|
|
1196
|
+
)
|
|
1169
1197
|
collection_type = _reconcile_collection_types(collection_types)
|
|
1170
1198
|
|
|
1171
1199
|
if cls._requires_validation_for_reading_parquets(collection_type, validation):
|
|
@@ -1226,14 +1254,27 @@ def read_parquet_metadata_collection(
|
|
|
1226
1254
|
"""
|
|
1227
1255
|
metadata = pl.read_parquet_metadata(source)
|
|
1228
1256
|
if (schema_metadata := metadata.get(COLLECTION_METADATA_KEY)) is not None:
|
|
1229
|
-
|
|
1230
|
-
return deserialize_collection(schema_metadata)
|
|
1231
|
-
except (JSONDecodeError, plexc.ComputeError):
|
|
1232
|
-
return None
|
|
1257
|
+
return deserialize_collection(schema_metadata, strict=False)
|
|
1233
1258
|
return None
|
|
1234
1259
|
|
|
1235
1260
|
|
|
1236
|
-
|
|
1261
|
+
@overload
|
|
1262
|
+
def deserialize_collection(
|
|
1263
|
+
data: str, strict: Literal[True] = True
|
|
1264
|
+
) -> type[Collection]: ...
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
@overload
|
|
1268
|
+
def deserialize_collection(
|
|
1269
|
+
data: str, strict: Literal[False]
|
|
1270
|
+
) -> type[Collection] | None: ...
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
@overload
|
|
1274
|
+
def deserialize_collection(data: str, strict: bool) -> type[Collection] | None: ...
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def deserialize_collection(data: str, strict: bool = True) -> type[Collection] | None:
|
|
1237
1278
|
"""Deserialize a collection from a JSON string.
|
|
1238
1279
|
|
|
1239
1280
|
This method allows to dynamically load a collection from its serialization, without
|
|
@@ -1241,12 +1282,14 @@ def deserialize_collection(data: str) -> type[Collection]:
|
|
|
1241
1282
|
|
|
1242
1283
|
Args:
|
|
1243
1284
|
data: The JSON string created via :meth:`Collection.serialize`.
|
|
1285
|
+
strict: Whether to raise an exception if the collection cannot be deserialized.
|
|
1244
1286
|
|
|
1245
1287
|
Returns:
|
|
1246
1288
|
The collection loaded from the JSON data.
|
|
1247
1289
|
|
|
1248
1290
|
Raises:
|
|
1249
|
-
|
|
1291
|
+
DeserializationError: If the collection can not be deserialized
|
|
1292
|
+
and `strict=True`.
|
|
1250
1293
|
|
|
1251
1294
|
Attention:
|
|
1252
1295
|
The returned collection **cannot** be used to create instances of the
|
|
@@ -1261,34 +1304,41 @@ def deserialize_collection(data: str) -> type[Collection]:
|
|
|
1261
1304
|
See also:
|
|
1262
1305
|
:meth:`Collection.serialize` for additional information on serialization.
|
|
1263
1306
|
"""
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1307
|
+
try:
|
|
1308
|
+
decoded = json.loads(data, cls=SchemaJSONDecoder)
|
|
1309
|
+
if (format := decoded["versions"]["format"]) != SERIALIZATION_FORMAT_VERSION:
|
|
1310
|
+
raise ValueError(f"Unsupported schema format version: {format}")
|
|
1311
|
+
|
|
1312
|
+
annotations: dict[str, Any] = {}
|
|
1313
|
+
for name, info in decoded["members"].items():
|
|
1314
|
+
lf_type = LazyFrame[_schema_from_dict(info["schema"])] # type: ignore
|
|
1315
|
+
if info["is_optional"]:
|
|
1316
|
+
lf_type = lf_type | None # type: ignore
|
|
1317
|
+
annotations[name] = Annotated[
|
|
1318
|
+
lf_type,
|
|
1319
|
+
CollectionMember(
|
|
1320
|
+
ignored_in_filters=info["ignored_in_filters"],
|
|
1321
|
+
inline_for_sampling=info["inline_for_sampling"],
|
|
1322
|
+
),
|
|
1323
|
+
]
|
|
1324
|
+
|
|
1325
|
+
return type(
|
|
1326
|
+
f"{decoded['name']}_dynamic",
|
|
1327
|
+
(Collection,),
|
|
1328
|
+
{
|
|
1329
|
+
"__annotations__": annotations,
|
|
1330
|
+
**{
|
|
1331
|
+
name: Filter(logic=lambda _, logic=logic: logic) # type: ignore
|
|
1332
|
+
for name, logic in decoded["filters"].items()
|
|
1333
|
+
},
|
|
1289
1334
|
},
|
|
1290
|
-
|
|
1291
|
-
)
|
|
1335
|
+
)
|
|
1336
|
+
except (ValueError, TypeError, JSONDecodeError, plexc.ComputeError) as e:
|
|
1337
|
+
if strict:
|
|
1338
|
+
raise DeserializationError(
|
|
1339
|
+
"The Collection metadata could not be deserialized"
|
|
1340
|
+
) from e
|
|
1341
|
+
return None
|
|
1292
1342
|
|
|
1293
1343
|
|
|
1294
1344
|
# --------------------------------------- UTILS -------------------------------------- #
|
|
@@ -1314,14 +1364,15 @@ def _extract_keys_if_exist(
|
|
|
1314
1364
|
|
|
1315
1365
|
def _deserialize_types(
|
|
1316
1366
|
serialized_collection_types: Iterable[str | None],
|
|
1367
|
+
strict: bool = True,
|
|
1317
1368
|
) -> list[type[Collection]]:
|
|
1318
1369
|
collection_types = []
|
|
1319
|
-
collection_type: type[Collection] | None = None
|
|
1320
1370
|
for t in serialized_collection_types:
|
|
1321
1371
|
if t is None:
|
|
1322
1372
|
continue
|
|
1323
|
-
collection_type = deserialize_collection(t)
|
|
1324
|
-
|
|
1373
|
+
collection_type = deserialize_collection(t, strict=strict)
|
|
1374
|
+
if collection_type is not None:
|
|
1375
|
+
collection_types.append(collection_type)
|
|
1325
1376
|
|
|
1326
1377
|
return collection_types
|
|
1327
1378
|
|