dataframely 1.5.0__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/workflows/build.yml +3 -3
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/workflows/chore.yml +2 -2
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/workflows/ci.yml +2 -2
- dataframely-1.7.0/.github/workflows/nightly.yml +36 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/workflows/scorecard.yml +1 -1
- {dataframely-1.5.0 → dataframely-1.7.0}/PKG-INFO +4 -3
- {dataframely-1.5.0 → dataframely-1.7.0}/README.md +3 -2
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/__init__.py +5 -2
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_base_collection.py +27 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_base_schema.py +28 -23
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_deprecation.py +11 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_filter.py +6 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_rule.py +92 -5
- dataframely-1.7.0/dataframely/_serialization.py +113 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_typing.py +3 -1
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/collection.py +389 -34
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/__init__.py +2 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/_base.py +185 -13
- dataframely-1.7.0/dataframely/columns/_registry.py +32 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/any.py +4 -9
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/array.py +23 -9
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/bool.py +2 -2
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/datetime.py +22 -26
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/decimal.py +4 -8
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/enum.py +5 -8
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/float.py +6 -8
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/integer.py +13 -8
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/list.py +22 -9
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/object.py +4 -8
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/string.py +4 -10
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/struct.py +32 -9
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/exc.py +7 -24
- dataframely-1.7.0/dataframely/failure.py +227 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/schema.py +411 -9
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/rules.py +1 -1
- {dataframely-1.5.0 → dataframely-1.7.0}/pixi.lock +12345 -6929
- {dataframely-1.5.0 → dataframely-1.7.0}/pixi.toml +10 -2
- {dataframely-1.5.0 → dataframely-1.7.0}/pyproject.toml +3 -3
- dataframely-1.7.0/tests/benches/conftest.py +25 -0
- dataframely-1.7.0/tests/benches/test_collection.py +112 -0
- dataframely-1.7.0/tests/benches/test_failure.py +35 -0
- dataframely-1.7.0/tests/benches/test_schema.py +168 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_base.py +0 -40
- dataframely-1.7.0/tests/collection/test_collection_future_annotations.py +24 -0
- dataframely-1.7.0/tests/collection/test_matches.py +119 -0
- dataframely-1.7.0/tests/collection/test_read_write_parquet.py +285 -0
- dataframely-1.7.0/tests/collection/test_repr.py +38 -0
- dataframely-1.7.0/tests/collection/test_serialization.py +77 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_alias.py +14 -0
- dataframely-1.7.0/tests/columns/test_matches.py +64 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_base.py +14 -8
- dataframely-1.7.0/tests/schema/test_matches.py +140 -0
- dataframely-1.7.0/tests/schema/test_read_write_parquet.py +218 -0
- dataframely-1.7.0/tests/schema/test_repr.py +54 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_rule_implementation.py +1 -27
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_sample.py +71 -0
- dataframely-1.7.0/tests/schema/test_serialization.py +130 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_validate.py +25 -3
- dataframely-1.7.0/tests/test_deprecation.py +63 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_failure_info.py +25 -22
- dataframely-1.5.0/dataframely/failure.py +0 -154
- dataframely-1.5.0/tests/test_deprecation.py +0 -28
- {dataframely-1.5.0 → dataframely-1.7.0}/.copier-answers.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.envrc +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.gitattributes +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/CODEOWNERS +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/dependabot.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.github/release-drafter.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.gitignore +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.pre-commit-config.yaml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.prettierignore +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.prettierrc +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/.readthedocs.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/Cargo.lock +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/Cargo.toml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/LICENSE +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/SECURITY.md +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_compat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_extre.pyi +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_polars.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/config.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/functional.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/mypy.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/py.typed +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/random.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/const.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/factory.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/mask.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/dataframely/testing/typing.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docker-compose.yml +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/Makefile +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.collection.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.any.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.bool.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.enum.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.float.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.integer.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.list.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.string.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.columns.struct.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.config.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.exc.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.failure.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.functional.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.mypy.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.random.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.schema.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.const.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.factory.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.mask.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.rules.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/dataframely.testing.typing.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_api/modules.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_static/custom.css +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/_static/favicon.ico +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/conf.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/index.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/make.bat +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/development.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/examples/real-world.ipynb +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/faq.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/installation.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/quickstart.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/docs/sites/versioning.rst +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/src/errdefs.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/src/lib.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/src/regex_repr.rs +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_cast.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_sample.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_any.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_array.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_datetime.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_float.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_list.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_object.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_string.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_check.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_pyarrow.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_rules.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_sample.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_sql_schema.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_str.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/columns/test_utils.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/core_validation/test_column_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/core_validation/test_dtype_validation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/functional/test_concat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_cast.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_filter.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_compat.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_config.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_exc.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_extre.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_random.py +0 -0
- {dataframely-1.5.0 → dataframely-1.7.0}/tests/test_typing.py +0 -0
|
@@ -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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
|
|
21
21
|
with:
|
|
22
22
|
environments: build
|
|
23
23
|
- name: Set version
|
|
@@ -52,13 +52,13 @@ 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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
|
|
56
56
|
with:
|
|
57
57
|
environments: build
|
|
58
58
|
- name: Set version
|
|
59
59
|
run: pixi run -e build set-version
|
|
60
60
|
- name: Build wheel
|
|
61
|
-
uses: PyO3/maturin-action@
|
|
61
|
+
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
|
62
62
|
with:
|
|
63
63
|
command: build
|
|
64
64
|
args: --out dist -i python3.11
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
GITHUB_TOKEN: ${{ github.token }}
|
|
29
29
|
- name: Post comment about invalid PR title
|
|
30
30
|
if: failure()
|
|
31
|
-
uses: marocchino/sticky-pull-request-comment@
|
|
31
|
+
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
|
|
32
32
|
with:
|
|
33
33
|
header: conventional-commit-pr-title
|
|
34
34
|
message: |
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
</details>
|
|
46
46
|
- name: Delete comment about invalid PR title
|
|
47
47
|
if: success()
|
|
48
|
-
uses: marocchino/sticky-pull-request-comment@
|
|
48
|
+
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
|
|
49
49
|
with:
|
|
50
50
|
header: conventional-commit-pr-title
|
|
51
51
|
delete: 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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
|
|
28
28
|
with:
|
|
29
29
|
environments: default lint
|
|
30
30
|
- name: Install repository
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
- name: Checkout branch
|
|
46
46
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
47
47
|
- name: Set up pixi
|
|
48
|
-
uses: prefix-dev/setup-pixi@
|
|
48
|
+
uses: prefix-dev/setup-pixi@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
|
|
49
49
|
with:
|
|
50
50
|
environments: ${{ matrix.environment }}
|
|
51
51
|
- name: Install repository
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Nightly CI
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: "0 0 * * *" # Runs every day at midnight UTC
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
# Automatically stop old builds on the same branch/PR
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
unit-tests:
|
|
17
|
+
name: Unit Tests (${{ matrix.os == 'ubuntu-latest' && 'Linux' || 'Windows' }})
|
|
18
|
+
timeout-minutes: 30
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: true
|
|
22
|
+
matrix:
|
|
23
|
+
os: [ubuntu-latest, windows-latest]
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout branch
|
|
26
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
27
|
+
- name: Set up pixi
|
|
28
|
+
uses: prefix-dev/setup-pixi@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
|
|
29
|
+
with:
|
|
30
|
+
environments: nightly
|
|
31
|
+
- name: Install polars nightly
|
|
32
|
+
run: pixi run -e nightly install-polars-nightly
|
|
33
|
+
- name: Install repository
|
|
34
|
+
run: pixi run -e nightly postinstall
|
|
35
|
+
- name: Run pytest
|
|
36
|
+
run: pixi run -e nightly test-coverage --color=yes
|
|
@@ -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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
|
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: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -25,10 +25,11 @@ Project-URL: Repository, https://github.com/quantco/dataframely
|
|
|
25
25
|
</h3>
|
|
26
26
|
|
|
27
27
|
[](https://github.com/quantco/dataframely/actions/workflows/ci.yml)
|
|
28
|
+
[](https://github.com/quantco/dataframely/actions/workflows/nightly.yml)
|
|
28
29
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
29
30
|
[](https://pypi.org/project/dataframely)
|
|
30
31
|
[](https://pypi.org/project/dataframely)
|
|
31
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
32
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
32
33
|
|
|
33
34
|
</div>
|
|
34
35
|
|
|
@@ -68,7 +69,7 @@ class HouseSchema(dy.Schema):
|
|
|
68
69
|
price = dy.Float64(nullable=False)
|
|
69
70
|
|
|
70
71
|
@dy.rule()
|
|
71
|
-
def
|
|
72
|
+
def reasonable_bathroom_to_bedroom_ratio() -> pl.Expr:
|
|
72
73
|
ratio = pl.col("num_bathrooms") / pl.col("num_bedrooms")
|
|
73
74
|
return (ratio >= 1 / 3) & (ratio <= 3)
|
|
74
75
|
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
</h3>
|
|
9
9
|
|
|
10
10
|
[](https://github.com/quantco/dataframely/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/quantco/dataframely/actions/workflows/nightly.yml)
|
|
11
12
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
12
13
|
[](https://pypi.org/project/dataframely)
|
|
13
14
|
[](https://pypi.org/project/dataframely)
|
|
14
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
15
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
15
16
|
|
|
16
17
|
</div>
|
|
17
18
|
|
|
@@ -51,7 +52,7 @@ class HouseSchema(dy.Schema):
|
|
|
51
52
|
price = dy.Float64(nullable=False)
|
|
52
53
|
|
|
53
54
|
@dy.rule()
|
|
54
|
-
def
|
|
55
|
+
def reasonable_bathroom_to_bedroom_ratio() -> pl.Expr:
|
|
55
56
|
ratio = pl.col("num_bathrooms") / pl.col("num_bedrooms")
|
|
56
57
|
return (ratio >= 1 / 3) & (ratio <= 3)
|
|
57
58
|
|
|
@@ -15,7 +15,7 @@ from ._base_collection import CollectionMember
|
|
|
15
15
|
from ._filter import filter
|
|
16
16
|
from ._rule import rule
|
|
17
17
|
from ._typing import DataFrame, LazyFrame
|
|
18
|
-
from .collection import Collection
|
|
18
|
+
from .collection import Collection, deserialize_collection
|
|
19
19
|
from .columns import (
|
|
20
20
|
Any,
|
|
21
21
|
Array,
|
|
@@ -51,7 +51,7 @@ from .functional import (
|
|
|
51
51
|
filter_relationship_one_to_at_least_one,
|
|
52
52
|
filter_relationship_one_to_one,
|
|
53
53
|
)
|
|
54
|
-
from .schema import Schema
|
|
54
|
+
from .schema import Schema, deserialize_schema, read_parquet_metadata_schema
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"random",
|
|
@@ -61,12 +61,15 @@ __all__ = [
|
|
|
61
61
|
"LazyFrame",
|
|
62
62
|
"Collection",
|
|
63
63
|
"CollectionMember",
|
|
64
|
+
"deserialize_collection",
|
|
64
65
|
"Config",
|
|
65
66
|
"FailureInfo",
|
|
66
67
|
"concat_collection_members",
|
|
67
68
|
"filter_relationship_one_to_at_least_one",
|
|
68
69
|
"filter_relationship_one_to_one",
|
|
69
70
|
"Schema",
|
|
71
|
+
"deserialize_schema",
|
|
72
|
+
"read_parquet_metadata_schema",
|
|
70
73
|
"Any",
|
|
71
74
|
"Bool",
|
|
72
75
|
"Column",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
import typing
|
|
7
8
|
from abc import ABCMeta
|
|
8
9
|
from collections.abc import Iterable
|
|
@@ -245,6 +246,32 @@ class CollectionMeta(ABCMeta):
|
|
|
245
246
|
# Some other unknown annotation
|
|
246
247
|
raise AnnotationImplementationError(attr, type_annotation)
|
|
247
248
|
|
|
249
|
+
def __repr__(cls) -> str:
|
|
250
|
+
parts = [f'[Collection "{cls.__class__.__name__}"]']
|
|
251
|
+
parts.append(textwrap.indent("Members:", prefix=" " * 2))
|
|
252
|
+
for name, member in cls.members().items(): # type: ignore
|
|
253
|
+
parts.append(
|
|
254
|
+
textwrap.indent(
|
|
255
|
+
f'- "{name}": {member.schema.__name__}'
|
|
256
|
+
f"(optional={member.is_optional}, "
|
|
257
|
+
f"ignored_in_filters={member.ignored_in_filters}, "
|
|
258
|
+
f"inline_for_sampling={member.inline_for_sampling})",
|
|
259
|
+
prefix=" " * 4,
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if filters := cls._filters(): # type: ignore
|
|
263
|
+
parts.append(textwrap.indent("Filters:", prefix=" " * 2))
|
|
264
|
+
for name, member in filters.items():
|
|
265
|
+
parts.append(textwrap.indent(f'- "{name}":', prefix=" " * 4))
|
|
266
|
+
parts.append(
|
|
267
|
+
textwrap.indent(
|
|
268
|
+
f"{member.logic(cls.create_empty()).explain()}", # type: ignore
|
|
269
|
+
prefix=" " * 8,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
parts.append("") # Add line break at the end
|
|
273
|
+
return "\n".join(parts)
|
|
274
|
+
|
|
248
275
|
|
|
249
276
|
class BaseCollection(metaclass=CollectionMeta):
|
|
250
277
|
"""Internal utility abstraction to reference collections without introducing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
from abc import ABCMeta
|
|
7
8
|
from copy import copy
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -10,9 +11,9 @@ from typing import Any, Self
|
|
|
10
11
|
|
|
11
12
|
import polars as pl
|
|
12
13
|
|
|
13
|
-
from ._rule import GroupRule, Rule
|
|
14
|
+
from ._rule import GroupRule, Rule
|
|
14
15
|
from .columns import Column
|
|
15
|
-
from .exc import ImplementationError
|
|
16
|
+
from .exc import ImplementationError
|
|
16
17
|
|
|
17
18
|
_COLUMN_ATTR = "__dataframely_columns__"
|
|
18
19
|
_RULE_ATTR = "__dataframely_rules__"
|
|
@@ -111,25 +112,15 @@ class SchemaMeta(ABCMeta):
|
|
|
111
112
|
f"which are not in the schema: {missing_list}."
|
|
112
113
|
)
|
|
113
114
|
|
|
114
|
-
# 3) Assuming that non-custom rules are implemented correctly, we check that all
|
|
115
|
-
# custom rules are _also_ implemented correctly by evaluating rules on an
|
|
116
|
-
# empty data frame and checking for the evaluated dtypes.
|
|
117
|
-
if len(result.rules) > 0:
|
|
118
|
-
lf_empty = pl.LazyFrame(
|
|
119
|
-
schema={col_name: col.dtype for col_name, col in result.columns.items()}
|
|
120
|
-
)
|
|
121
|
-
# NOTE: For some reason, `polars` does not yield correct dtypes when calling
|
|
122
|
-
# `collect_schema()`
|
|
123
|
-
schema = with_evaluation_rules(lf_empty, result.rules).collect().schema
|
|
124
|
-
for rule_name, rule in result.rules.items():
|
|
125
|
-
dtype = schema[rule_name]
|
|
126
|
-
if not isinstance(dtype, pl.Boolean):
|
|
127
|
-
raise RuleImplementationError(
|
|
128
|
-
rule_name, dtype, isinstance(rule, GroupRule)
|
|
129
|
-
)
|
|
130
|
-
|
|
131
115
|
return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
|
|
132
116
|
|
|
117
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
118
|
+
val = super().__getattribute__(name)
|
|
119
|
+
# Dynamically set the name of the column if it is a `Column` instance.
|
|
120
|
+
if isinstance(val, Column):
|
|
121
|
+
val._name = val.alias or name
|
|
122
|
+
return val
|
|
123
|
+
|
|
133
124
|
@staticmethod
|
|
134
125
|
def _get_metadata_recursively(kls: type[object]) -> Metadata:
|
|
135
126
|
result = Metadata()
|
|
@@ -145,9 +136,7 @@ class SchemaMeta(ABCMeta):
|
|
|
145
136
|
k: v for k, v in source.items() if not k.startswith("__")
|
|
146
137
|
}.items():
|
|
147
138
|
if isinstance(value, Column):
|
|
148
|
-
|
|
149
|
-
value.alias = attr
|
|
150
|
-
result.columns[value.alias] = value
|
|
139
|
+
result.columns[value.alias or attr] = value
|
|
151
140
|
if isinstance(value, Rule):
|
|
152
141
|
# We must ensure that custom rules do not clash with internal rules.
|
|
153
142
|
if attr == "primary_key":
|
|
@@ -157,6 +146,18 @@ class SchemaMeta(ABCMeta):
|
|
|
157
146
|
result.rules[attr] = value
|
|
158
147
|
return result
|
|
159
148
|
|
|
149
|
+
def __repr__(cls) -> str:
|
|
150
|
+
parts = [f'[Schema "{cls.__name__}"]']
|
|
151
|
+
parts.append(textwrap.indent("Columns:", prefix=" " * 2))
|
|
152
|
+
for name, col in cls.columns().items():
|
|
153
|
+
parts.append(textwrap.indent(f'- "{name}": {col!r}', prefix=" " * 4))
|
|
154
|
+
if validation_rules := cls._schema_validation_rules():
|
|
155
|
+
parts.append(textwrap.indent("Rules:", prefix=" " * 2))
|
|
156
|
+
for name, rule in validation_rules.items():
|
|
157
|
+
parts.append(textwrap.indent(f'- "{name}": {rule!r}', prefix=" " * 4))
|
|
158
|
+
parts.append("") # Add line break at the end
|
|
159
|
+
return "\n".join(parts)
|
|
160
|
+
|
|
160
161
|
|
|
161
162
|
class BaseSchema(metaclass=SchemaMeta):
|
|
162
163
|
"""Internal utility abstraction to reference schemas without introducing cyclical
|
|
@@ -170,7 +171,11 @@ class BaseSchema(metaclass=SchemaMeta):
|
|
|
170
171
|
@classmethod
|
|
171
172
|
def columns(cls) -> dict[str, Column]:
|
|
172
173
|
"""The column definitions of this schema."""
|
|
173
|
-
|
|
174
|
+
columns: dict[str, Column] = getattr(cls, _COLUMN_ATTR)
|
|
175
|
+
for name in columns.keys():
|
|
176
|
+
# Dynamically set the name of the columns.
|
|
177
|
+
columns[name]._name = name
|
|
178
|
+
return columns
|
|
174
179
|
|
|
175
180
|
@classmethod
|
|
176
181
|
def primary_keys(cls) -> list[str]:
|
|
@@ -37,3 +37,14 @@ def warn_nullable_default_change() -> None:
|
|
|
37
37
|
FutureWarning,
|
|
38
38
|
stacklevel=4,
|
|
39
39
|
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@skip_if(env="DATAFRAMELY_NO_FUTURE_WARNINGS")
|
|
43
|
+
def warn_no_nullable_primary_keys() -> None:
|
|
44
|
+
warnings.warn(
|
|
45
|
+
"Nullable primary keys are not supported. "
|
|
46
|
+
"Setting `nullable=True` on a primary key column is ignored "
|
|
47
|
+
"and will raise an error in a future release.",
|
|
48
|
+
FutureWarning,
|
|
49
|
+
stacklevel=4,
|
|
50
|
+
)
|
|
@@ -34,6 +34,12 @@ def filter() -> Callable[[Callable[[C], pl.LazyFrame]], Filter[C]]:
|
|
|
34
34
|
Attention:
|
|
35
35
|
Make sure to provide unique combinations of the primary keys or the filters
|
|
36
36
|
might introduce duplicate rows.
|
|
37
|
+
|
|
38
|
+
Attention:
|
|
39
|
+
The filter logic should return a lazy frame with a static computational graph.
|
|
40
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
41
|
+
validation, but may lead to wrong results in Collection comparisons
|
|
42
|
+
and (de-)serialization.
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
def decorator(validation_fn: Callable[[C], pl.LazyFrame]) -> Filter[C]:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Copyright (c) QuantCo 2025-2025
|
|
2
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
from collections import defaultdict
|
|
5
7
|
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Self
|
|
6
9
|
|
|
7
10
|
import polars as pl
|
|
8
11
|
|
|
@@ -12,17 +15,68 @@ ValidationFunction = Callable[[], pl.Expr]
|
|
|
12
15
|
class Rule:
|
|
13
16
|
"""Internal class representing validation rules."""
|
|
14
17
|
|
|
15
|
-
def __init__(self, expr: pl.Expr) -> None:
|
|
16
|
-
self.
|
|
18
|
+
def __init__(self, expr: pl.Expr | ValidationFunction) -> None:
|
|
19
|
+
self._expr = expr
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def expr(self) -> pl.Expr:
|
|
23
|
+
"""Get the expression of the rule."""
|
|
24
|
+
if callable(self._expr):
|
|
25
|
+
return self._expr()
|
|
26
|
+
return self._expr
|
|
27
|
+
|
|
28
|
+
def matches(self, other: Rule) -> bool:
|
|
29
|
+
"""Check whether this rule semantically matches another rule.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
other: The rule to compare with.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Whether the rules are semantically equal.
|
|
36
|
+
"""
|
|
37
|
+
return self.expr.meta.eq(other.expr)
|
|
38
|
+
|
|
39
|
+
def as_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Turn the rule into a dictionary."""
|
|
41
|
+
return {"rule_type": self.__class__.__name__, "expr": self.expr}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
45
|
+
"""Read the rule from a dictionary.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: The dictionary that was created via :meth:`asdict`.
|
|
49
|
+
"""
|
|
50
|
+
return cls(data["expr"])
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return str(self.expr)
|
|
17
54
|
|
|
18
55
|
|
|
19
56
|
class GroupRule(Rule):
|
|
20
57
|
"""Rule that is evaluated on a group of columns."""
|
|
21
58
|
|
|
22
|
-
def __init__(
|
|
59
|
+
def __init__(
|
|
60
|
+
self, expr: pl.Expr | ValidationFunction, group_columns: list[str]
|
|
61
|
+
) -> None:
|
|
23
62
|
super().__init__(expr)
|
|
24
63
|
self.group_columns = group_columns
|
|
25
64
|
|
|
65
|
+
def matches(self, other: Rule) -> bool:
|
|
66
|
+
if not isinstance(other, GroupRule):
|
|
67
|
+
return False
|
|
68
|
+
return super().matches(other) and self.group_columns == other.group_columns
|
|
69
|
+
|
|
70
|
+
def as_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {**super().as_dict(), "group_columns": self.group_columns}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
75
|
+
return cls(data["expr"], group_columns=data["group_columns"])
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"{super().__repr__()} grouped by {self.group_columns}"
|
|
79
|
+
|
|
26
80
|
|
|
27
81
|
def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction], Rule]:
|
|
28
82
|
"""Mark a function as a rule to evaluate during validation.
|
|
@@ -52,12 +106,18 @@ def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction],
|
|
|
52
106
|
rules. By default, any rule that evaluates to ``null`` because one of the
|
|
53
107
|
columns used in the rule is ``null`` is interpreted as ``true``, i.e. the row
|
|
54
108
|
is assumed to be valid.
|
|
109
|
+
|
|
110
|
+
Attention:
|
|
111
|
+
The rule logic should return a static result.
|
|
112
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
113
|
+
validation, but may lead to wrong results in Schema comparisons
|
|
114
|
+
and (de-)serialization.
|
|
55
115
|
"""
|
|
56
116
|
|
|
57
117
|
def decorator(validation_fn: ValidationFunction) -> Rule:
|
|
58
118
|
if group_by is not None:
|
|
59
|
-
return GroupRule(expr=validation_fn
|
|
60
|
-
return Rule(expr=validation_fn
|
|
119
|
+
return GroupRule(expr=validation_fn, group_columns=group_by)
|
|
120
|
+
return Rule(expr=validation_fn)
|
|
61
121
|
|
|
62
122
|
return decorator
|
|
63
123
|
|
|
@@ -130,3 +190,30 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
|
|
|
130
190
|
frame, on=list(group_columns), how="left", nulls_equal=True
|
|
131
191
|
)
|
|
132
192
|
return result
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------------------------ #
|
|
196
|
+
# FACTORY #
|
|
197
|
+
# ------------------------------------------------------------------------------------ #
|
|
198
|
+
|
|
199
|
+
_TYPE_MAPPING: dict[str, type[Rule]] = {
|
|
200
|
+
Rule.__name__: Rule,
|
|
201
|
+
GroupRule.__name__: GroupRule,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def rule_from_dict(data: dict[str, Any]) -> Rule:
|
|
206
|
+
"""Dynamically read a rule object from a dictionary.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data: The dictionary obtained by calling :meth:`~Rule.asdict` on a rule object.
|
|
210
|
+
The dictionary must contain a key ``"rule_type"`` that indicates which rule
|
|
211
|
+
type to instantiate.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The rule object as read from ``data``.
|
|
215
|
+
"""
|
|
216
|
+
name = data["rule_type"]
|
|
217
|
+
if name not in _TYPE_MAPPING:
|
|
218
|
+
raise ValueError(f"Unknown rule type: {name}")
|
|
219
|
+
return _TYPE_MAPPING[name].from_dict(data)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright (c) QuantCo 2025-2025
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import decimal
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from json import JSONDecoder, JSONEncoder
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
import polars as pl
|
|
12
|
+
|
|
13
|
+
SCHEMA_METADATA_KEY = "dataframely_schema"
|
|
14
|
+
SERIALIZATION_FORMAT_VERSION = "1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def serialization_versions() -> dict[str, str]:
|
|
18
|
+
"""Return the versions of the serialization format and the libraries used."""
|
|
19
|
+
from dataframely import __version__
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"format": SERIALIZATION_FORMAT_VERSION,
|
|
23
|
+
"dataframely": __version__,
|
|
24
|
+
"polars": pl.__version__,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SchemaJSONEncoder(JSONEncoder):
|
|
29
|
+
"""Custom JSON encoder to properly serialize all types serialized by schemas."""
|
|
30
|
+
|
|
31
|
+
def encode(self, obj: Any) -> str:
|
|
32
|
+
def hint_tuples(item: Any) -> Any:
|
|
33
|
+
if isinstance(item, tuple):
|
|
34
|
+
return {"__type__": "tuple", "value": list(item)}
|
|
35
|
+
if isinstance(item, list):
|
|
36
|
+
return [hint_tuples(i) for i in item]
|
|
37
|
+
if isinstance(item, dict):
|
|
38
|
+
return {k: hint_tuples(v) for k, v in item.items()}
|
|
39
|
+
return item
|
|
40
|
+
|
|
41
|
+
return super().encode(hint_tuples(obj))
|
|
42
|
+
|
|
43
|
+
def default(self, obj: Any) -> Any:
|
|
44
|
+
match obj:
|
|
45
|
+
case pl.Expr():
|
|
46
|
+
return {
|
|
47
|
+
"__type__": "expression",
|
|
48
|
+
"value": obj.meta.serialize(format="json"),
|
|
49
|
+
}
|
|
50
|
+
case pl.LazyFrame():
|
|
51
|
+
return {
|
|
52
|
+
"__type__": "lazyframe",
|
|
53
|
+
"value": base64.b64encode(obj.serialize()).decode("utf-8"),
|
|
54
|
+
}
|
|
55
|
+
case decimal.Decimal():
|
|
56
|
+
return {"__type__": "decimal", "value": str(obj)}
|
|
57
|
+
case dt.datetime():
|
|
58
|
+
return {"__type__": "datetime", "value": obj.isoformat()}
|
|
59
|
+
case dt.date():
|
|
60
|
+
return {"__type__": "date", "value": obj.isoformat()}
|
|
61
|
+
case dt.time():
|
|
62
|
+
return {"__type__": "time", "value": obj.isoformat()}
|
|
63
|
+
case dt.timedelta():
|
|
64
|
+
return {"__type__": "timedelta", "value": obj.total_seconds()}
|
|
65
|
+
case dt.tzinfo():
|
|
66
|
+
offset = obj.utcoffset(dt.datetime.now())
|
|
67
|
+
return {
|
|
68
|
+
"__type__": "tzinfo",
|
|
69
|
+
"value": offset.total_seconds() if offset is not None else None,
|
|
70
|
+
}
|
|
71
|
+
case _:
|
|
72
|
+
return super().default(obj)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SchemaJSONDecoder(JSONDecoder):
|
|
76
|
+
"""Custom JSON decoder to properly deserialize all types serialized by schemas."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
79
|
+
super().__init__(object_hook=self.object_hook, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
def object_hook(self, dct: dict[str, Any]) -> Any:
|
|
82
|
+
if "__type__" not in dct:
|
|
83
|
+
return dct
|
|
84
|
+
|
|
85
|
+
match dct["__type__"]:
|
|
86
|
+
case "tuple":
|
|
87
|
+
return tuple(dct["value"])
|
|
88
|
+
case "expression":
|
|
89
|
+
data = BytesIO(cast(str, dct["value"]).encode("utf-8"))
|
|
90
|
+
return pl.Expr.deserialize(data, format="json")
|
|
91
|
+
case "lazyframe":
|
|
92
|
+
data = BytesIO(
|
|
93
|
+
base64.b64decode(cast(str, dct["value"]).encode("utf-8"))
|
|
94
|
+
)
|
|
95
|
+
return pl.LazyFrame.deserialize(data)
|
|
96
|
+
case "decimal":
|
|
97
|
+
return decimal.Decimal(dct["value"])
|
|
98
|
+
case "datetime":
|
|
99
|
+
return dt.datetime.fromisoformat(dct["value"])
|
|
100
|
+
case "date":
|
|
101
|
+
return dt.date.fromisoformat(dct["value"])
|
|
102
|
+
case "time":
|
|
103
|
+
return dt.time.fromisoformat(dct["value"])
|
|
104
|
+
case "timedelta":
|
|
105
|
+
return dt.timedelta(seconds=float(dct["value"]))
|
|
106
|
+
case "tzinfo":
|
|
107
|
+
return (
|
|
108
|
+
dt.timezone(dt.timedelta(seconds=float(dct["value"])))
|
|
109
|
+
if dct["value"] is not None
|
|
110
|
+
else dt.timezone(dt.timedelta(0))
|
|
111
|
+
)
|
|
112
|
+
case _:
|
|
113
|
+
raise TypeError(f"Unknown type '{dct['__type__']}' in JSON data.")
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Concatenate, Generic, Literal, ParamSpec, TypeVar
|
|
8
8
|
|
|
9
9
|
import polars as pl
|
|
10
10
|
|
|
@@ -15,6 +15,8 @@ S = TypeVar("S", bound=BaseSchema, covariant=True)
|
|
|
15
15
|
P = ParamSpec("P")
|
|
16
16
|
R = TypeVar("R")
|
|
17
17
|
|
|
18
|
+
Validation = Literal["allow", "forbid", "warn", "skip"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def inherit_signature( # pragma: no cover
|
|
20
22
|
target_fn: Callable[P, Any],
|