dataframely 2.1.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.1.0 → dataframely-2.2.0}/.github/copilot-instructions.md +1 -1
- {dataframely-2.1.0 → dataframely-2.2.0}/PKG-INFO +1 -1
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/__init__.py +2 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/collection/collection.py +73 -41
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/exc.py +7 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/schema.py +20 -4
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/storage.py +103 -12
- {dataframely-2.1.0 → dataframely-2.2.0}/pixi.lock +6345 -5918
- {dataframely-2.1.0 → dataframely-2.2.0}/pyproject.toml +1 -1
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_serialization.py +35 -3
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_storage.py +57 -7
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_serialization.py +4 -4
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_storage.py +62 -1
- {dataframely-2.1.0 → dataframely-2.2.0}/.copier-answers.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.envrc +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.gitattributes +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/CODEOWNERS +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/dependabot.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/instructions/tests.instructions.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/release-drafter.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/build.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/chore.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/ci.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/copilot-setup-steps.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/nightly.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.github/workflows/scorecard.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.gitignore +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.pre-commit-config.yaml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.prettierignore +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.prettierrc +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/.readthedocs.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/Cargo.lock +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/Cargo.toml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/LICENSE +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/README.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/SECURITY.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_base_schema.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_compat.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_deprecation.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_filter.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_match_to_schema.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_native.pyi +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_plugin.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_polars.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_pydantic.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_rule.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_serialization.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/_exc.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/constants.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/delta.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_storage/parquet.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/_typing.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/collection/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/collection/_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/collection/filter_result.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/_registry.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/any.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/array.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/binary.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/bool.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/categorical.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/datetime.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/decimal.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/enum.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/float.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/integer.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/list.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/object.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/string.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/columns/struct.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/config.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/filter_result.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/functional.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/py.typed +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/random.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/const.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/factory.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/mask.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/dataframely/testing/rules.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docker-compose.yml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_static/custom.css +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_static/favicon.ico +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_templates/autosummary/class.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_templates/autosummary/method.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_templates/classes/column.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_templates/classes/error.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/_templates/classes/filter_result.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/generation.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/io.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/metadata.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/operations.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/collection/validation.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/columns/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/errors/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/filter_result/failure_info.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/filter_result/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/misc/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/conversion.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/generation.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/index.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/io.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/metadata.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/api/schema/validation.rst +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/conf.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/css/custom.css +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/development.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/examples/index.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/examples/real-world.ipynb +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/faq.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/column-metadata.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/data-generation.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/index.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/lazy-validation.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/primary-keys.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/serialization.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/features/sql-generation.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/index.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/migration/index.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/migration/v1-v2.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/guides/quickstart.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/docs/index.md +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/pixi.toml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/rust-toolchain.toml +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/lib.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/polars_plugin/mod.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/polars_plugin/rule_failure.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/polars_plugin/utils.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/polars_plugin/validation_error.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/regex/errdefs.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/regex/mod.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/src/regex/repr.rs +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/benches/conftest.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/benches/test_collection.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/benches/test_failure.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/benches/test_schema.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_cast.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_collection_future_annotations.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_join.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_matches.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_repr.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_sample.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_any.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_array.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_binary.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_datetime.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_float.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_list.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_object.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_string.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_alias.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_check.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_matches.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_pyarrow.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_rules.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_sample.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_sqlalchemy_columns.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_str.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/columns/test_utils.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/conftest.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/core_validation/test_match_to_schema.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/failure_info/test_storage.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/functional/test_concat.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_base.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_cast.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_filter.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_matches.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_read_write_parquet.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_repr.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_rule_implementation.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_sample.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/schema/test_validate.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/storage/test_delta.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_compat.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_config.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_deprecation.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_factory.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_native_regex.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_pydantic.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_random.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_serialization.py +0 -0
- {dataframely-2.1.0 → dataframely-2.2.0}/tests/test_typing.py +0 -0
|
@@ -202,7 +202,7 @@ validated_df: dy.DataFrame[MySchema] = MySchema.validate(df, cast=True)
|
|
|
202
202
|
|
|
203
203
|
1. **Python code**: Run `pixi run pre-commit run` before committing
|
|
204
204
|
2. **Rust code**: Run `pixi run postinstall` to rebuild, then run tests
|
|
205
|
-
3. **Tests**: Ensure `pixi run test` passes
|
|
205
|
+
3. **Tests**: Ensure `pixi run test` passes. If changes might affect storage backends, use `pixi run test -m s3`.
|
|
206
206
|
4. **Documentation**: Update docstrings
|
|
207
207
|
5. **API changes**: Ensure backward compatibility or document migration path
|
|
208
208
|
|
|
@@ -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
|
]
|
|
@@ -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
|
|
@@ -891,13 +895,13 @@ class Collection(BaseCollection, ABC):
|
|
|
891
895
|
- `"allow"`: The method tries to read the schema data from the parquet
|
|
892
896
|
files. If the stored collection schema matches this collection
|
|
893
897
|
schema, the collection is read without validation. If the stored
|
|
894
|
-
schema mismatches this schema no metadata can be found in
|
|
898
|
+
schema mismatches this schema, no valid metadata can be found in
|
|
895
899
|
the parquets, or the files have conflicting metadata,
|
|
896
900
|
this method automatically runs :meth:`validate` with `cast=True`.
|
|
897
901
|
- `"warn"`: The method behaves similarly to `"allow"`. However,
|
|
898
902
|
it prints a warning if validation is necessary.
|
|
899
903
|
- `"forbid"`: The method never runs validation automatically and only
|
|
900
|
-
returns if the metadata stores a collection schema that matches
|
|
904
|
+
returns if the metadata stores a valid collection schema that matches
|
|
901
905
|
this collection.
|
|
902
906
|
- `"skip"`: The method never runs validation and simply reads the
|
|
903
907
|
data, entrusting the user that the schema is valid. *Use this option
|
|
@@ -1184,7 +1188,12 @@ class Collection(BaseCollection, ABC):
|
|
|
1184
1188
|
members=cls.member_schemas().keys(), **kwargs
|
|
1185
1189
|
)
|
|
1186
1190
|
|
|
1187
|
-
|
|
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
|
+
)
|
|
1188
1197
|
collection_type = _reconcile_collection_types(collection_types)
|
|
1189
1198
|
|
|
1190
1199
|
if cls._requires_validation_for_reading_parquets(collection_type, validation):
|
|
@@ -1245,14 +1254,27 @@ def read_parquet_metadata_collection(
|
|
|
1245
1254
|
"""
|
|
1246
1255
|
metadata = pl.read_parquet_metadata(source)
|
|
1247
1256
|
if (schema_metadata := metadata.get(COLLECTION_METADATA_KEY)) is not None:
|
|
1248
|
-
|
|
1249
|
-
return deserialize_collection(schema_metadata)
|
|
1250
|
-
except (JSONDecodeError, plexc.ComputeError):
|
|
1251
|
-
return None
|
|
1257
|
+
return deserialize_collection(schema_metadata, strict=False)
|
|
1252
1258
|
return None
|
|
1253
1259
|
|
|
1254
1260
|
|
|
1255
|
-
|
|
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:
|
|
1256
1278
|
"""Deserialize a collection from a JSON string.
|
|
1257
1279
|
|
|
1258
1280
|
This method allows to dynamically load a collection from its serialization, without
|
|
@@ -1260,12 +1282,14 @@ def deserialize_collection(data: str) -> type[Collection]:
|
|
|
1260
1282
|
|
|
1261
1283
|
Args:
|
|
1262
1284
|
data: The JSON string created via :meth:`Collection.serialize`.
|
|
1285
|
+
strict: Whether to raise an exception if the collection cannot be deserialized.
|
|
1263
1286
|
|
|
1264
1287
|
Returns:
|
|
1265
1288
|
The collection loaded from the JSON data.
|
|
1266
1289
|
|
|
1267
1290
|
Raises:
|
|
1268
|
-
|
|
1291
|
+
DeserializationError: If the collection can not be deserialized
|
|
1292
|
+
and `strict=True`.
|
|
1269
1293
|
|
|
1270
1294
|
Attention:
|
|
1271
1295
|
The returned collection **cannot** be used to create instances of the
|
|
@@ -1280,34 +1304,41 @@ def deserialize_collection(data: str) -> type[Collection]:
|
|
|
1280
1304
|
See also:
|
|
1281
1305
|
:meth:`Collection.serialize` for additional information on serialization.
|
|
1282
1306
|
"""
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
+
},
|
|
1308
1334
|
},
|
|
1309
|
-
|
|
1310
|
-
)
|
|
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
|
|
1311
1342
|
|
|
1312
1343
|
|
|
1313
1344
|
# --------------------------------------- UTILS -------------------------------------- #
|
|
@@ -1333,14 +1364,15 @@ def _extract_keys_if_exist(
|
|
|
1333
1364
|
|
|
1334
1365
|
def _deserialize_types(
|
|
1335
1366
|
serialized_collection_types: Iterable[str | None],
|
|
1367
|
+
strict: bool = True,
|
|
1336
1368
|
) -> list[type[Collection]]:
|
|
1337
1369
|
collection_types = []
|
|
1338
|
-
collection_type: type[Collection] | None = None
|
|
1339
1370
|
for t in serialized_collection_types:
|
|
1340
1371
|
if t is None:
|
|
1341
1372
|
continue
|
|
1342
|
-
collection_type = deserialize_collection(t)
|
|
1343
|
-
|
|
1373
|
+
collection_type = deserialize_collection(t, strict=strict)
|
|
1374
|
+
if collection_type is not None:
|
|
1375
|
+
collection_types.append(collection_type)
|
|
1344
1376
|
|
|
1345
1377
|
return collection_types
|
|
1346
1378
|
|
|
@@ -41,3 +41,10 @@ class AnnotationImplementationError(ImplementationError):
|
|
|
41
41
|
|
|
42
42
|
class ValidationRequiredError(Exception):
|
|
43
43
|
"""Error raised when validation is required when reading a parquet file."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------- DESERIALIZATION --------------------------------- #
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DeserializationError(Exception):
|
|
50
|
+
"""Error raised when deserialization of a schema or collection fails."""
|
|
@@ -40,7 +40,12 @@ from ._storage.parquet import (
|
|
|
40
40
|
from ._typing import DataFrame, LazyFrame, Validation
|
|
41
41
|
from .columns import Column, column_from_dict
|
|
42
42
|
from .config import Config
|
|
43
|
-
from .exc import
|
|
43
|
+
from .exc import (
|
|
44
|
+
DeserializationError,
|
|
45
|
+
SchemaError,
|
|
46
|
+
ValidationError,
|
|
47
|
+
ValidationRequiredError,
|
|
48
|
+
)
|
|
44
49
|
from .filter_result import FailureInfo, FilterResult, LazyFilterResult
|
|
45
50
|
from .random import Generator
|
|
46
51
|
|
|
@@ -1238,8 +1243,13 @@ class Schema(BaseSchema, ABC):
|
|
|
1238
1243
|
validation: Validation,
|
|
1239
1244
|
source: str,
|
|
1240
1245
|
) -> DataFrame[Self] | LazyFrame[Self]:
|
|
1246
|
+
# Use strict=False when validation is "allow", "warn" or "skip" to tolerate
|
|
1247
|
+
# deserialization failures from old serialized formats.
|
|
1248
|
+
strict = validation == "forbid"
|
|
1241
1249
|
deserialized_schema = (
|
|
1242
|
-
deserialize_schema(serialized_schema)
|
|
1250
|
+
deserialize_schema(serialized_schema, strict=strict)
|
|
1251
|
+
if serialized_schema
|
|
1252
|
+
else None
|
|
1243
1253
|
)
|
|
1244
1254
|
|
|
1245
1255
|
# Smart validation
|
|
@@ -1347,6 +1357,10 @@ def deserialize_schema(data: str, strict: Literal[True] = True) -> type[Schema]:
|
|
|
1347
1357
|
def deserialize_schema(data: str, strict: Literal[False]) -> type[Schema] | None: ...
|
|
1348
1358
|
|
|
1349
1359
|
|
|
1360
|
+
@overload
|
|
1361
|
+
def deserialize_schema(data: str, strict: bool) -> type[Schema] | None: ...
|
|
1362
|
+
|
|
1363
|
+
|
|
1350
1364
|
def deserialize_schema(data: str, strict: bool = True) -> type[Schema] | None:
|
|
1351
1365
|
"""Deserialize a schema from a JSON string.
|
|
1352
1366
|
|
|
@@ -1375,9 +1389,11 @@ def deserialize_schema(data: str, strict: bool = True) -> type[Schema] | None:
|
|
|
1375
1389
|
if (format := decoded["versions"]["format"]) != SERIALIZATION_FORMAT_VERSION:
|
|
1376
1390
|
raise ValueError(f"Unsupported schema format version: {format}")
|
|
1377
1391
|
return _schema_from_dict(decoded)
|
|
1378
|
-
except (ValueError, JSONDecodeError, plexc.ComputeError) as e:
|
|
1392
|
+
except (ValueError, JSONDecodeError, plexc.ComputeError, TypeError) as e:
|
|
1379
1393
|
if strict:
|
|
1380
|
-
raise
|
|
1394
|
+
raise DeserializationError(
|
|
1395
|
+
"The Schema metadata could not be deserialized"
|
|
1396
|
+
) from e
|
|
1381
1397
|
return None
|
|
1382
1398
|
|
|
1383
1399
|
|
|
@@ -29,11 +29,11 @@ class SchemaStorageTester(ABC):
|
|
|
29
29
|
def write_typed(
|
|
30
30
|
self, schema: type[S], df: dy.DataFrame[S], path: str, lazy: bool
|
|
31
31
|
) -> None:
|
|
32
|
-
"""Write a schema to the backend
|
|
32
|
+
"""Write a schema to the backend and record schema information."""
|
|
33
33
|
|
|
34
34
|
@abstractmethod
|
|
35
35
|
def write_untyped(self, df: pl.DataFrame, path: str, lazy: bool) -> None:
|
|
36
|
-
"""Write a schema to the backend
|
|
36
|
+
"""Write a schema to the backend without recording schema information."""
|
|
37
37
|
|
|
38
38
|
@overload
|
|
39
39
|
def read(
|
|
@@ -45,12 +45,22 @@ class SchemaStorageTester(ABC):
|
|
|
45
45
|
self, schema: type[S], path: str, lazy: Literal[False], validation: Validation
|
|
46
46
|
) -> dy.DataFrame[S]: ...
|
|
47
47
|
|
|
48
|
+
@overload
|
|
49
|
+
def read(
|
|
50
|
+
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
51
|
+
) -> dy.LazyFrame[S] | dy.DataFrame[S]: ...
|
|
52
|
+
|
|
48
53
|
@abstractmethod
|
|
49
54
|
def read(
|
|
50
55
|
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
51
56
|
) -> dy.LazyFrame[S] | dy.DataFrame[S]:
|
|
52
57
|
"""Read from the backend, using schema information if available."""
|
|
53
58
|
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
61
|
+
"""Overwrite the metadata stored at the given path with the provided
|
|
62
|
+
metadata."""
|
|
63
|
+
|
|
54
64
|
|
|
55
65
|
class ParquetSchemaStorageTester(SchemaStorageTester):
|
|
56
66
|
"""Testing interface for the parquet storage functionality of Schema."""
|
|
@@ -83,6 +93,11 @@ class ParquetSchemaStorageTester(SchemaStorageTester):
|
|
|
83
93
|
self, schema: type[S], path: str, lazy: Literal[False], validation: Validation
|
|
84
94
|
) -> dy.DataFrame[S]: ...
|
|
85
95
|
|
|
96
|
+
@overload
|
|
97
|
+
def read(
|
|
98
|
+
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
99
|
+
) -> dy.LazyFrame[S] | dy.DataFrame[S]: ...
|
|
100
|
+
|
|
86
101
|
def read(
|
|
87
102
|
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
88
103
|
) -> dy.LazyFrame[S] | dy.DataFrame[S]:
|
|
@@ -93,6 +108,11 @@ class ParquetSchemaStorageTester(SchemaStorageTester):
|
|
|
93
108
|
else:
|
|
94
109
|
return schema.read_parquet(self._wrap_path(path), validation=validation)
|
|
95
110
|
|
|
111
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
112
|
+
target = self._wrap_path(path)
|
|
113
|
+
data = pl.read_parquet(target)
|
|
114
|
+
data.write_parquet(target, metadata=metadata)
|
|
115
|
+
|
|
96
116
|
|
|
97
117
|
class DeltaSchemaStorageTester(SchemaStorageTester):
|
|
98
118
|
"""Testing interface for the deltalake storage functionality of Schema."""
|
|
@@ -115,6 +135,11 @@ class DeltaSchemaStorageTester(SchemaStorageTester):
|
|
|
115
135
|
self, schema: type[S], path: str, lazy: Literal[False], validation: Validation
|
|
116
136
|
) -> dy.DataFrame[S]: ...
|
|
117
137
|
|
|
138
|
+
@overload
|
|
139
|
+
def read(
|
|
140
|
+
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
141
|
+
) -> dy.LazyFrame[S] | dy.DataFrame[S]: ...
|
|
142
|
+
|
|
118
143
|
def read(
|
|
119
144
|
self, schema: type[S], path: str, lazy: bool, validation: Validation
|
|
120
145
|
) -> dy.DataFrame[S] | dy.LazyFrame[S]:
|
|
@@ -122,6 +147,18 @@ class DeltaSchemaStorageTester(SchemaStorageTester):
|
|
|
122
147
|
return schema.scan_delta(path, validation=validation)
|
|
123
148
|
return schema.read_delta(path, validation=validation)
|
|
124
149
|
|
|
150
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
151
|
+
df = pl.read_delta(path)
|
|
152
|
+
df.head(0).write_delta(
|
|
153
|
+
path,
|
|
154
|
+
delta_write_options={
|
|
155
|
+
"commit_properties": deltalake.CommitProperties(
|
|
156
|
+
custom_metadata=metadata
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
mode="overwrite",
|
|
160
|
+
)
|
|
161
|
+
|
|
125
162
|
|
|
126
163
|
# ------------------------------- Collection -------------------------------------------
|
|
127
164
|
|
|
@@ -147,11 +184,36 @@ class CollectionStorageTester(ABC):
|
|
|
147
184
|
def read(self, collection: type[C], path: str, lazy: bool, **kwargs: Any) -> C:
|
|
148
185
|
"""Read from the backend, using collection information if available."""
|
|
149
186
|
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
189
|
+
"""Overwrite the metadata stored at the given path with the provided
|
|
190
|
+
metadata."""
|
|
191
|
+
|
|
192
|
+
def _prefix_path(self, path: str, fs: AbstractFileSystem) -> str:
|
|
193
|
+
return f"{self._get_prefix(fs)}{path}"
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _get_prefix(fs: AbstractFileSystem) -> str:
|
|
197
|
+
return (
|
|
198
|
+
""
|
|
199
|
+
if fs.protocol == "file"
|
|
200
|
+
else (
|
|
201
|
+
f"{fs.protocol}://"
|
|
202
|
+
if isinstance(fs.protocol, str)
|
|
203
|
+
else f"{fs.protocol[0]}://"
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
150
207
|
|
|
151
208
|
class ParquetCollectionStorageTester(CollectionStorageTester):
|
|
152
209
|
def write_typed(
|
|
153
210
|
self, collection: dy.Collection, path: str, lazy: bool, **kwargs: Any
|
|
154
211
|
) -> None:
|
|
212
|
+
if "metadata" in kwargs: # pragma: no cover
|
|
213
|
+
raise KeyError(
|
|
214
|
+
"`metadata` kwarg will be ignored in `write_typed`. Use `set_metadata`."
|
|
215
|
+
)
|
|
216
|
+
|
|
155
217
|
# Polars does not support partitioning via kwarg on sink_parquet
|
|
156
218
|
if lazy:
|
|
157
219
|
kwargs.pop("partition_by", None)
|
|
@@ -164,6 +226,11 @@ class ParquetCollectionStorageTester(CollectionStorageTester):
|
|
|
164
226
|
def write_untyped(
|
|
165
227
|
self, collection: dy.Collection, path: str, lazy: bool, **kwargs: Any
|
|
166
228
|
) -> None:
|
|
229
|
+
if "metadata" in kwargs: # pragma: no cover
|
|
230
|
+
raise KeyError(
|
|
231
|
+
"Cannot set metadata through `write_untyped`. Use `set_metadata`."
|
|
232
|
+
)
|
|
233
|
+
|
|
167
234
|
if lazy:
|
|
168
235
|
collection.sink_parquet(path, **kwargs)
|
|
169
236
|
else:
|
|
@@ -175,17 +242,8 @@ class ParquetCollectionStorageTester(CollectionStorageTester):
|
|
|
175
242
|
df.write_parquet(file)
|
|
176
243
|
|
|
177
244
|
fs: AbstractFileSystem = url_to_fs(path)[0]
|
|
178
|
-
prefix = (
|
|
179
|
-
""
|
|
180
|
-
if fs.protocol == "file"
|
|
181
|
-
else (
|
|
182
|
-
f"{fs.protocol}://"
|
|
183
|
-
if isinstance(fs.protocol, str)
|
|
184
|
-
else f"{fs.protocol[0]}://"
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
245
|
for file in fs.glob(fs.sep.join([path, "**", "*.parquet"])):
|
|
188
|
-
_delete_meta(
|
|
246
|
+
_delete_meta(self._prefix_path(file, fs))
|
|
189
247
|
|
|
190
248
|
def read(self, collection: type[C], path: str, lazy: bool, **kwargs: Any) -> C:
|
|
191
249
|
if lazy:
|
|
@@ -193,11 +251,23 @@ class ParquetCollectionStorageTester(CollectionStorageTester):
|
|
|
193
251
|
else:
|
|
194
252
|
return collection.read_parquet(path, **kwargs)
|
|
195
253
|
|
|
254
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
255
|
+
fs: AbstractFileSystem = url_to_fs(path)[0]
|
|
256
|
+
for file in fs.glob(fs.sep.join([path, "*.parquet"])):
|
|
257
|
+
file_path = self._prefix_path(file, fs)
|
|
258
|
+
df = pl.read_parquet(file_path)
|
|
259
|
+
df.write_parquet(file_path, metadata=metadata)
|
|
260
|
+
|
|
196
261
|
|
|
197
262
|
class DeltaCollectionStorageTester(CollectionStorageTester):
|
|
198
263
|
def write_typed(
|
|
199
264
|
self, collection: dy.Collection, path: str, lazy: bool, **kwargs: Any
|
|
200
265
|
) -> None:
|
|
266
|
+
if "metadata" in kwargs: # pragma: no cover
|
|
267
|
+
raise KeyError(
|
|
268
|
+
"`metadata` kwarg will be ignored in `write_typed`. Use `set_metadata`."
|
|
269
|
+
)
|
|
270
|
+
|
|
201
271
|
extra_kwargs = {}
|
|
202
272
|
if partition_by := kwargs.pop("partition_by", None):
|
|
203
273
|
extra_kwargs["delta_write_options"] = {"partition_by": partition_by}
|
|
@@ -207,6 +277,10 @@ class DeltaCollectionStorageTester(CollectionStorageTester):
|
|
|
207
277
|
def write_untyped(
|
|
208
278
|
self, collection: dy.Collection, path: str, lazy: bool, **kwargs: Any
|
|
209
279
|
) -> None:
|
|
280
|
+
if "metadata" in kwargs: # pragma: no cover
|
|
281
|
+
raise KeyError(
|
|
282
|
+
"Cannot set metadata through `write_untyped`. Use `set_metadata`."
|
|
283
|
+
)
|
|
210
284
|
collection.write_delta(path, **kwargs)
|
|
211
285
|
|
|
212
286
|
# For each member table, write an empty commit
|
|
@@ -222,6 +296,23 @@ class DeltaCollectionStorageTester(CollectionStorageTester):
|
|
|
222
296
|
return collection.scan_delta(source=path, **kwargs)
|
|
223
297
|
return collection.read_delta(source=path, **kwargs)
|
|
224
298
|
|
|
299
|
+
def set_metadata(self, path: str, metadata: dict[str, Any]) -> None:
|
|
300
|
+
fs: AbstractFileSystem = url_to_fs(path)[0]
|
|
301
|
+
# For delta, we need to update metadata on each member table
|
|
302
|
+
for entry in fs.ls(path):
|
|
303
|
+
member_path = self._prefix_path(entry, fs)
|
|
304
|
+
if fs.isdir(member_path):
|
|
305
|
+
df = pl.read_delta(member_path)
|
|
306
|
+
df.head(0).write_delta(
|
|
307
|
+
member_path,
|
|
308
|
+
delta_write_options={
|
|
309
|
+
"commit_properties": deltalake.CommitProperties(
|
|
310
|
+
custom_metadata=metadata
|
|
311
|
+
),
|
|
312
|
+
},
|
|
313
|
+
mode="overwrite",
|
|
314
|
+
)
|
|
315
|
+
|
|
225
316
|
|
|
226
317
|
# ------------------------------------ Failure info ------------------------------------
|
|
227
318
|
class FailureInfoStorageTester(ABC):
|