dataframely 1.4.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dataframely-1.4.0 → dataframely-1.6.0}/.copier-answers.yml +1 -1
- dataframely-1.6.0/.gitattributes +1 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/workflows/build.yml +2 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/workflows/chore.yml +4 -4
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/workflows/ci.yml +5 -2
- dataframely-1.6.0/.github/workflows/scorecard.yml +79 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/PKG-INFO +3 -3
- {dataframely-1.4.0 → dataframely-1.6.0}/README.md +1 -1
- dataframely-1.6.0/SECURITY.md +7 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/__init__.py +2 -1
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_base_collection.py +27 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_base_schema.py +23 -22
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_deprecation.py +11 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_filter.py +6 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_rule.py +92 -5
- dataframely-1.6.0/dataframely/_serialization.py +88 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/collection.py +68 -15
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/__init__.py +2 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/_base.py +185 -13
- dataframely-1.6.0/dataframely/columns/_registry.py +32 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/any.py +4 -9
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/array.py +23 -9
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/bool.py +2 -2
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/datetime.py +29 -28
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/decimal.py +4 -8
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/enum.py +5 -8
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/float.py +6 -8
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/integer.py +13 -8
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/list.py +22 -9
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/object.py +4 -8
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/string.py +4 -10
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/struct.py +32 -9
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/exc.py +4 -25
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/failure.py +20 -12
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/random.py +6 -2
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/schema.py +119 -1
- {dataframely-1.4.0 → dataframely-1.6.0}/pixi.lock +3591 -3676
- {dataframely-1.4.0 → dataframely-1.6.0}/pixi.toml +1 -1
- {dataframely-1.4.0 → dataframely-1.6.0}/pyproject.toml +2 -2
- dataframely-1.6.0/tests/collection/test_collection_future_annotations.py +24 -0
- dataframely-1.6.0/tests/collection/test_matches.py +119 -0
- dataframely-1.6.0/tests/collection/test_repr.py +38 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_datetime.py +7 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_alias.py +14 -0
- dataframely-1.6.0/tests/columns/test_matches.py +64 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_pyarrow.py +7 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_base.py +5 -8
- dataframely-1.6.0/tests/schema/test_matches.py +140 -0
- dataframely-1.6.0/tests/schema/test_repr.py +54 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_rule_implementation.py +1 -27
- dataframely-1.6.0/tests/schema/test_serialization.py +130 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_validate.py +25 -3
- dataframely-1.6.0/tests/test_deprecation.py +63 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_failure_info.py +33 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_typing.py +1 -1
- dataframely-1.4.0/.gitattributes +0 -1
- dataframely-1.4.0/tests/test_deprecation.py +0 -28
- {dataframely-1.4.0 → dataframely-1.6.0}/.envrc +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/CODEOWNERS +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/dependabot.yml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.github/release-drafter.yml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.gitignore +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.pre-commit-config.yaml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.prettierignore +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.prettierrc +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/.readthedocs.yml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/Cargo.lock +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/Cargo.toml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/LICENSE +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_compat.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_extre.pyi +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_polars.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_typing.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/_validation.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/_mixins.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/columns/_utils.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/config.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/functional.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/mypy.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/py.typed +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/__init__.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/const.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/factory.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/mask.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/rules.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/dataframely/testing/typing.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docker-compose.yml +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/Makefile +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.collection.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.any.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.bool.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.enum.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.float.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.integer.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.list.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.string.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.columns.struct.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.config.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.exc.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.failure.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.functional.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.mypy.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.random.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.schema.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.const.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.factory.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.mask.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.rules.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/dataframely.testing.typing.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_api/modules.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_static/custom.css +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/_static/favicon.ico +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/conf.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/index.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/make.bat +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/development.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/examples/real-world.ipynb +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/faq.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/installation.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/quickstart.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/docs/sites/versioning.rst +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/src/errdefs.rs +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/src/lib.rs +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/src/regex_repr.rs +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_base.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_cast.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_create_empty.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_implementation.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_optional_members.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_sample.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/collection/test_validate_input.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/__init__.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_any.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_array.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_decimal.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_enum.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_float.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_integer.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_list.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_object.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_string.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/column_types/test_struct.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/__init__.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_check.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_metadata.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_rules.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_sample.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_sql_schema.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_str.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/columns/test_utils.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/core_validation/__init__.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/core_validation/test_column_validation.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/core_validation/test_dtype_validation.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/functional/test_concat.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/functional/test_relationships.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_cast.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_create_empty.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_filter.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_inheritance.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/schema/test_sample.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_compat.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_config.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_exc.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_extre.py +0 -0
- {dataframely-1.4.0 → dataframely-1.6.0}/tests/test_random.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
|
|
@@ -21,14 +21,14 @@ jobs:
|
|
|
21
21
|
steps:
|
|
22
22
|
- name: Check valid conventional commit message
|
|
23
23
|
id: lint
|
|
24
|
-
uses: amannn/action-semantic-pull-request@v5
|
|
24
|
+
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
|
25
25
|
with:
|
|
26
26
|
subjectPattern: ^[A-Z].+[^. ]$ # subject must start with uppercase letter and may not end with a dot/space
|
|
27
27
|
env:
|
|
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@v2
|
|
31
|
+
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
|
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@v2
|
|
48
|
+
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
|
49
49
|
with:
|
|
50
50
|
header: conventional-commit-pr-title
|
|
51
51
|
delete: true
|
|
@@ -58,7 +58,7 @@ jobs:
|
|
|
58
58
|
pull-requests: write
|
|
59
59
|
steps:
|
|
60
60
|
- name: ${{ github.event_name == 'pull_request_target' && 'Assign labels' || 'Update release draft' }}
|
|
61
|
-
uses: release-drafter/release-drafter@v6
|
|
61
|
+
uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
|
|
62
62
|
with:
|
|
63
63
|
disable-releaser: ${{ github.event_name == 'pull_request_target' }}
|
|
64
64
|
disable-autolabeler: ${{ github.event_name == 'push' }}
|
|
@@ -9,6 +9,9 @@ concurrency:
|
|
|
9
9
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
10
|
cancel-in-progress: true
|
|
11
11
|
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
12
15
|
jobs:
|
|
13
16
|
pre-commit-checks:
|
|
14
17
|
name: Pre-commit Checks
|
|
@@ -40,7 +43,7 @@ jobs:
|
|
|
40
43
|
environment: [py311, py312, py313]
|
|
41
44
|
steps:
|
|
42
45
|
- name: Checkout branch
|
|
43
|
-
uses: actions/checkout@v4
|
|
46
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
44
47
|
- name: Set up pixi
|
|
45
48
|
uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
|
|
46
49
|
with:
|
|
@@ -50,7 +53,7 @@ jobs:
|
|
|
50
53
|
- name: Run pytest
|
|
51
54
|
run: pixi run -e ${{ matrix.environment }} test-coverage --color=yes
|
|
52
55
|
- name: Upload codecov
|
|
53
|
-
uses: codecov/codecov-action@v5
|
|
56
|
+
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
|
54
57
|
with:
|
|
55
58
|
files: ./coverage.xml
|
|
56
59
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub. They are provided
|
|
2
|
+
# by a third-party and are governed by separate terms of service, privacy
|
|
3
|
+
# policy, and support documentation.
|
|
4
|
+
|
|
5
|
+
name: Scorecard supply-chain security
|
|
6
|
+
on:
|
|
7
|
+
# For Branch-Protection check. Only the default branch is supported. See
|
|
8
|
+
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
|
9
|
+
branch_protection_rule:
|
|
10
|
+
# To guarantee Maintained check is occasionally updated. See
|
|
11
|
+
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
|
12
|
+
schedule:
|
|
13
|
+
- cron: "34 5 * * 0"
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
push:
|
|
16
|
+
branches: ["main"]
|
|
17
|
+
|
|
18
|
+
# Declare default permissions as read only.
|
|
19
|
+
permissions: read-all
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
analysis:
|
|
23
|
+
name: Scorecard analysis
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
|
|
26
|
+
if: (github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request') && github.repository == 'quantco/dataframely'
|
|
27
|
+
permissions:
|
|
28
|
+
# Needed to upload the results to code-scanning dashboard.
|
|
29
|
+
security-events: write
|
|
30
|
+
# Needed to publish results and get a badge (see publish_results below).
|
|
31
|
+
id-token: write
|
|
32
|
+
# Uncomment the permissions below if installing in a private repository.
|
|
33
|
+
# contents: read
|
|
34
|
+
# actions: read
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- name: "Checkout code"
|
|
38
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
39
|
+
with:
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
|
|
42
|
+
- name: "Run analysis"
|
|
43
|
+
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
|
44
|
+
with:
|
|
45
|
+
results_file: results.sarif
|
|
46
|
+
results_format: sarif
|
|
47
|
+
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
|
48
|
+
# - you want to enable the Branch-Protection check on a *public* repository, or
|
|
49
|
+
# - you are installing Scorecard on a *private* repository
|
|
50
|
+
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
|
|
51
|
+
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
|
52
|
+
|
|
53
|
+
# Public repositories:
|
|
54
|
+
# - Publish results to OpenSSF REST API for easy access by consumers
|
|
55
|
+
# - Allows the repository to include the Scorecard badge.
|
|
56
|
+
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
|
57
|
+
# For private repositories:
|
|
58
|
+
# - `publish_results` will always be set to `false`, regardless
|
|
59
|
+
# of the value entered here.
|
|
60
|
+
publish_results: true
|
|
61
|
+
|
|
62
|
+
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
|
|
63
|
+
# file_mode: git
|
|
64
|
+
|
|
65
|
+
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
|
66
|
+
# format to the repository Actions tab.
|
|
67
|
+
- name: "Upload artifact"
|
|
68
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
69
|
+
with:
|
|
70
|
+
name: SARIF file
|
|
71
|
+
path: results.sarif
|
|
72
|
+
retention-days: 5
|
|
73
|
+
|
|
74
|
+
# Upload the results to GitHub's code scanning dashboard (optional).
|
|
75
|
+
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
|
76
|
+
- name: "Upload to code-scanning"
|
|
77
|
+
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
|
78
|
+
with:
|
|
79
|
+
sarif_file: results.sarif
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframely
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.12
|
|
7
7
|
Classifier: Programming Language :: Python :: 3.13
|
|
8
8
|
Requires-Dist: numpy
|
|
9
|
-
Requires-Dist: polars>=1.
|
|
9
|
+
Requires-Dist: polars>=1.30
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Summary: A declarative, polars-native data frame validation library
|
|
12
12
|
Author-email: Andreas Albert <andreas.albert@quantco.com>, Daniel Elsner <daniel.elsner@quantco.com>, Oliver Borchert <oliver.borchert@quantco.com>
|
|
@@ -28,7 +28,7 @@ Project-URL: Repository, https://github.com/quantco/dataframely
|
|
|
28
28
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
29
29
|
[](https://pypi.org/project/dataframely)
|
|
30
30
|
[](https://pypi.org/project/dataframely)
|
|
31
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
31
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
32
32
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://prefix.dev/channels/conda-forge/packages/dataframely)
|
|
12
12
|
[](https://pypi.org/project/dataframely)
|
|
13
13
|
[](https://pypi.org/project/dataframely)
|
|
14
|
-
[](https://codecov.io/gh/Quantco/dataframely)
|
|
14
|
+
[](https://codecov.io/gh/Quantco/dataframely)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Reporting Security Issues
|
|
2
|
+
|
|
3
|
+
We take security bugs in our projects seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
|
4
|
+
|
|
5
|
+
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/quantco/dataframely/security/advisories/new) tab.
|
|
6
|
+
|
|
7
|
+
We will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
|
@@ -51,7 +51,7 @@ from .functional import (
|
|
|
51
51
|
filter_relationship_one_to_at_least_one,
|
|
52
52
|
filter_relationship_one_to_one,
|
|
53
53
|
)
|
|
54
|
-
from .schema import Schema
|
|
54
|
+
from .schema import Schema, deserialize_schema
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"random",
|
|
@@ -67,6 +67,7 @@ __all__ = [
|
|
|
67
67
|
"filter_relationship_one_to_at_least_one",
|
|
68
68
|
"filter_relationship_one_to_one",
|
|
69
69
|
"Schema",
|
|
70
|
+
"deserialize_schema",
|
|
70
71
|
"Any",
|
|
71
72
|
"Bool",
|
|
72
73
|
"Column",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
import typing
|
|
7
8
|
from abc import ABCMeta
|
|
8
9
|
from collections.abc import Iterable
|
|
@@ -245,6 +246,32 @@ class CollectionMeta(ABCMeta):
|
|
|
245
246
|
# Some other unknown annotation
|
|
246
247
|
raise AnnotationImplementationError(attr, type_annotation)
|
|
247
248
|
|
|
249
|
+
def __repr__(cls) -> str:
|
|
250
|
+
parts = [f'[Collection "{cls.__class__.__name__}"]']
|
|
251
|
+
parts.append(textwrap.indent("Members:", prefix=" " * 2))
|
|
252
|
+
for name, member in cls.members().items(): # type: ignore
|
|
253
|
+
parts.append(
|
|
254
|
+
textwrap.indent(
|
|
255
|
+
f'- "{name}": {member.schema.__name__}'
|
|
256
|
+
f"(optional={member.is_optional}, "
|
|
257
|
+
f"ignored_in_filters={member.ignored_in_filters}, "
|
|
258
|
+
f"inline_for_sampling={member.inline_for_sampling})",
|
|
259
|
+
prefix=" " * 4,
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if filters := cls._filters(): # type: ignore
|
|
263
|
+
parts.append(textwrap.indent("Filters:", prefix=" " * 2))
|
|
264
|
+
for name, member in filters.items():
|
|
265
|
+
parts.append(textwrap.indent(f'- "{name}":', prefix=" " * 4))
|
|
266
|
+
parts.append(
|
|
267
|
+
textwrap.indent(
|
|
268
|
+
f"{member.logic(cls.create_empty()).explain()}", # type: ignore
|
|
269
|
+
prefix=" " * 8,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
parts.append("") # Add line break at the end
|
|
273
|
+
return "\n".join(parts)
|
|
274
|
+
|
|
248
275
|
|
|
249
276
|
class BaseCollection(metaclass=CollectionMeta):
|
|
250
277
|
"""Internal utility abstraction to reference collections without introducing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import textwrap
|
|
6
7
|
from abc import ABCMeta
|
|
7
8
|
from copy import copy
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -10,9 +11,9 @@ from typing import Any, Self
|
|
|
10
11
|
|
|
11
12
|
import polars as pl
|
|
12
13
|
|
|
13
|
-
from ._rule import GroupRule, Rule
|
|
14
|
+
from ._rule import GroupRule, Rule
|
|
14
15
|
from .columns import Column
|
|
15
|
-
from .exc import ImplementationError
|
|
16
|
+
from .exc import ImplementationError
|
|
16
17
|
|
|
17
18
|
_COLUMN_ATTR = "__dataframely_columns__"
|
|
18
19
|
_RULE_ATTR = "__dataframely_rules__"
|
|
@@ -111,25 +112,15 @@ class SchemaMeta(ABCMeta):
|
|
|
111
112
|
f"which are not in the schema: {missing_list}."
|
|
112
113
|
)
|
|
113
114
|
|
|
114
|
-
# 3) Assuming that non-custom rules are implemented correctly, we check that all
|
|
115
|
-
# custom rules are _also_ implemented correctly by evaluating rules on an
|
|
116
|
-
# empty data frame and checking for the evaluated dtypes.
|
|
117
|
-
if len(result.rules) > 0:
|
|
118
|
-
lf_empty = pl.LazyFrame(
|
|
119
|
-
schema={col_name: col.dtype for col_name, col in result.columns.items()}
|
|
120
|
-
)
|
|
121
|
-
# NOTE: For some reason, `polars` does not yield correct dtypes when calling
|
|
122
|
-
# `collect_schema()`
|
|
123
|
-
schema = with_evaluation_rules(lf_empty, result.rules).collect().schema
|
|
124
|
-
for rule_name, rule in result.rules.items():
|
|
125
|
-
dtype = schema[rule_name]
|
|
126
|
-
if not isinstance(dtype, pl.Boolean):
|
|
127
|
-
raise RuleImplementationError(
|
|
128
|
-
rule_name, dtype, isinstance(rule, GroupRule)
|
|
129
|
-
)
|
|
130
|
-
|
|
131
115
|
return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
|
|
132
116
|
|
|
117
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
118
|
+
val = super().__getattribute__(name)
|
|
119
|
+
# Dynamically set the name of the column if it is a `Column` instance.
|
|
120
|
+
if isinstance(val, Column):
|
|
121
|
+
val._name = val.alias or name
|
|
122
|
+
return val
|
|
123
|
+
|
|
133
124
|
@staticmethod
|
|
134
125
|
def _get_metadata_recursively(kls: type[object]) -> Metadata:
|
|
135
126
|
result = Metadata()
|
|
@@ -145,9 +136,7 @@ class SchemaMeta(ABCMeta):
|
|
|
145
136
|
k: v for k, v in source.items() if not k.startswith("__")
|
|
146
137
|
}.items():
|
|
147
138
|
if isinstance(value, Column):
|
|
148
|
-
|
|
149
|
-
value.alias = attr
|
|
150
|
-
result.columns[value.alias] = value
|
|
139
|
+
result.columns[value.alias or attr] = value
|
|
151
140
|
if isinstance(value, Rule):
|
|
152
141
|
# We must ensure that custom rules do not clash with internal rules.
|
|
153
142
|
if attr == "primary_key":
|
|
@@ -157,6 +146,18 @@ class SchemaMeta(ABCMeta):
|
|
|
157
146
|
result.rules[attr] = value
|
|
158
147
|
return result
|
|
159
148
|
|
|
149
|
+
def __repr__(cls) -> str:
|
|
150
|
+
parts = [f'[Schema "{cls.__name__}"]']
|
|
151
|
+
parts.append(textwrap.indent("Columns:", prefix=" " * 2))
|
|
152
|
+
for name, col in cls.columns().items():
|
|
153
|
+
parts.append(textwrap.indent(f'- "{name}": {col!r}', prefix=" " * 4))
|
|
154
|
+
if validation_rules := cls._schema_validation_rules():
|
|
155
|
+
parts.append(textwrap.indent("Rules:", prefix=" " * 2))
|
|
156
|
+
for name, rule in validation_rules.items():
|
|
157
|
+
parts.append(textwrap.indent(f'- "{name}": {rule!r}', prefix=" " * 4))
|
|
158
|
+
parts.append("") # Add line break at the end
|
|
159
|
+
return "\n".join(parts)
|
|
160
|
+
|
|
160
161
|
|
|
161
162
|
class BaseSchema(metaclass=SchemaMeta):
|
|
162
163
|
"""Internal utility abstraction to reference schemas without introducing cyclical
|
|
@@ -37,3 +37,14 @@ def warn_nullable_default_change() -> None:
|
|
|
37
37
|
FutureWarning,
|
|
38
38
|
stacklevel=4,
|
|
39
39
|
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@skip_if(env="DATAFRAMELY_NO_FUTURE_WARNINGS")
|
|
43
|
+
def warn_no_nullable_primary_keys() -> None:
|
|
44
|
+
warnings.warn(
|
|
45
|
+
"Nullable primary keys are not supported. "
|
|
46
|
+
"Setting `nullable=True` on a primary key column is ignored "
|
|
47
|
+
"and will raise an error in a future release.",
|
|
48
|
+
FutureWarning,
|
|
49
|
+
stacklevel=4,
|
|
50
|
+
)
|
|
@@ -34,6 +34,12 @@ def filter() -> Callable[[Callable[[C], pl.LazyFrame]], Filter[C]]:
|
|
|
34
34
|
Attention:
|
|
35
35
|
Make sure to provide unique combinations of the primary keys or the filters
|
|
36
36
|
might introduce duplicate rows.
|
|
37
|
+
|
|
38
|
+
Attention:
|
|
39
|
+
The filter logic should return a lazy frame with a static computational graph.
|
|
40
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
41
|
+
validation, but may lead to wrong results in Collection comparisons
|
|
42
|
+
and (de-)serialization.
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
def decorator(validation_fn: Callable[[C], pl.LazyFrame]) -> Filter[C]:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Copyright (c) QuantCo 2025-2025
|
|
2
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
from collections import defaultdict
|
|
5
7
|
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Self
|
|
6
9
|
|
|
7
10
|
import polars as pl
|
|
8
11
|
|
|
@@ -12,17 +15,68 @@ ValidationFunction = Callable[[], pl.Expr]
|
|
|
12
15
|
class Rule:
|
|
13
16
|
"""Internal class representing validation rules."""
|
|
14
17
|
|
|
15
|
-
def __init__(self, expr: pl.Expr) -> None:
|
|
16
|
-
self.
|
|
18
|
+
def __init__(self, expr: pl.Expr | ValidationFunction) -> None:
|
|
19
|
+
self._expr = expr
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def expr(self) -> pl.Expr:
|
|
23
|
+
"""Get the expression of the rule."""
|
|
24
|
+
if callable(self._expr):
|
|
25
|
+
return self._expr()
|
|
26
|
+
return self._expr
|
|
27
|
+
|
|
28
|
+
def matches(self, other: Rule) -> bool:
|
|
29
|
+
"""Check whether this rule semantically matches another rule.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
other: The rule to compare with.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Whether the rules are semantically equal.
|
|
36
|
+
"""
|
|
37
|
+
return self.expr.meta.eq(other.expr)
|
|
38
|
+
|
|
39
|
+
def as_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Turn the rule into a dictionary."""
|
|
41
|
+
return {"rule_type": self.__class__.__name__, "expr": self.expr}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
45
|
+
"""Read the rule from a dictionary.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: The dictionary that was created via :meth:`asdict`.
|
|
49
|
+
"""
|
|
50
|
+
return cls(data["expr"])
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return str(self.expr)
|
|
17
54
|
|
|
18
55
|
|
|
19
56
|
class GroupRule(Rule):
|
|
20
57
|
"""Rule that is evaluated on a group of columns."""
|
|
21
58
|
|
|
22
|
-
def __init__(
|
|
59
|
+
def __init__(
|
|
60
|
+
self, expr: pl.Expr | ValidationFunction, group_columns: list[str]
|
|
61
|
+
) -> None:
|
|
23
62
|
super().__init__(expr)
|
|
24
63
|
self.group_columns = group_columns
|
|
25
64
|
|
|
65
|
+
def matches(self, other: Rule) -> bool:
|
|
66
|
+
if not isinstance(other, GroupRule):
|
|
67
|
+
return False
|
|
68
|
+
return super().matches(other) and self.group_columns == other.group_columns
|
|
69
|
+
|
|
70
|
+
def as_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {**super().as_dict(), "group_columns": self.group_columns}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
75
|
+
return cls(data["expr"], group_columns=data["group_columns"])
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"{super().__repr__()} grouped by {self.group_columns}"
|
|
79
|
+
|
|
26
80
|
|
|
27
81
|
def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction], Rule]:
|
|
28
82
|
"""Mark a function as a rule to evaluate during validation.
|
|
@@ -52,12 +106,18 @@ def rule(*, group_by: list[str] | None = None) -> Callable[[ValidationFunction],
|
|
|
52
106
|
rules. By default, any rule that evaluates to ``null`` because one of the
|
|
53
107
|
columns used in the rule is ``null`` is interpreted as ``true``, i.e. the row
|
|
54
108
|
is assumed to be valid.
|
|
109
|
+
|
|
110
|
+
Attention:
|
|
111
|
+
The rule logic should return a static result.
|
|
112
|
+
Other implementations using arbitrary python logic works for filtering and
|
|
113
|
+
validation, but may lead to wrong results in Schema comparisons
|
|
114
|
+
and (de-)serialization.
|
|
55
115
|
"""
|
|
56
116
|
|
|
57
117
|
def decorator(validation_fn: ValidationFunction) -> Rule:
|
|
58
118
|
if group_by is not None:
|
|
59
|
-
return GroupRule(expr=validation_fn
|
|
60
|
-
return Rule(expr=validation_fn
|
|
119
|
+
return GroupRule(expr=validation_fn, group_columns=group_by)
|
|
120
|
+
return Rule(expr=validation_fn)
|
|
61
121
|
|
|
62
122
|
return decorator
|
|
63
123
|
|
|
@@ -130,3 +190,30 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
|
|
|
130
190
|
frame, on=list(group_columns), how="left", nulls_equal=True
|
|
131
191
|
)
|
|
132
192
|
return result
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------------------------ #
|
|
196
|
+
# FACTORY #
|
|
197
|
+
# ------------------------------------------------------------------------------------ #
|
|
198
|
+
|
|
199
|
+
_TYPE_MAPPING: dict[str, type[Rule]] = {
|
|
200
|
+
Rule.__name__: Rule,
|
|
201
|
+
GroupRule.__name__: GroupRule,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def rule_from_dict(data: dict[str, Any]) -> Rule:
|
|
206
|
+
"""Dynamically read a rule object from a dictionary.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data: The dictionary obtained by calling :meth:`~Rule.asdict` on a rule object.
|
|
210
|
+
The dictionary must contain a key ``"rule_type"`` that indicates which rule
|
|
211
|
+
type to instantiate.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The rule object as read from ``data``.
|
|
215
|
+
"""
|
|
216
|
+
name = data["rule_type"]
|
|
217
|
+
if name not in _TYPE_MAPPING:
|
|
218
|
+
raise ValueError(f"Unknown rule type: {name}")
|
|
219
|
+
return _TYPE_MAPPING[name].from_dict(data)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) QuantCo 2025-2025
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import decimal
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from json import JSONDecoder, JSONEncoder
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SchemaJSONEncoder(JSONEncoder):
|
|
14
|
+
"""Custom JSON encoder to properly serialize all types serialized by schemas."""
|
|
15
|
+
|
|
16
|
+
def encode(self, obj: Any) -> str:
|
|
17
|
+
def hint_tuples(item: Any) -> Any:
|
|
18
|
+
if isinstance(item, tuple):
|
|
19
|
+
return {"__type__": "tuple", "value": list(item)}
|
|
20
|
+
if isinstance(item, list):
|
|
21
|
+
return [hint_tuples(i) for i in item]
|
|
22
|
+
if isinstance(item, dict):
|
|
23
|
+
return {k: hint_tuples(v) for k, v in item.items()}
|
|
24
|
+
return item
|
|
25
|
+
|
|
26
|
+
return super().encode(hint_tuples(obj))
|
|
27
|
+
|
|
28
|
+
def default(self, obj: Any) -> Any:
|
|
29
|
+
match obj:
|
|
30
|
+
case pl.Expr():
|
|
31
|
+
return {
|
|
32
|
+
"__type__": "expression",
|
|
33
|
+
"value": obj.meta.serialize(format="json"),
|
|
34
|
+
}
|
|
35
|
+
case decimal.Decimal():
|
|
36
|
+
return {"__type__": "decimal", "value": str(obj)}
|
|
37
|
+
case dt.datetime():
|
|
38
|
+
return {"__type__": "datetime", "value": obj.isoformat()}
|
|
39
|
+
case dt.date():
|
|
40
|
+
return {"__type__": "date", "value": obj.isoformat()}
|
|
41
|
+
case dt.time():
|
|
42
|
+
return {"__type__": "time", "value": obj.isoformat()}
|
|
43
|
+
case dt.timedelta():
|
|
44
|
+
return {"__type__": "timedelta", "value": obj.total_seconds()}
|
|
45
|
+
case dt.tzinfo():
|
|
46
|
+
offset = obj.utcoffset(dt.datetime.now())
|
|
47
|
+
return {
|
|
48
|
+
"__type__": "tzinfo",
|
|
49
|
+
"value": offset.total_seconds() if offset is not None else None,
|
|
50
|
+
}
|
|
51
|
+
case _:
|
|
52
|
+
return super().default(obj)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SchemaJSONDecoder(JSONDecoder):
|
|
56
|
+
"""Custom JSON decoder to properly deserialize all types serialized by schemas."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
59
|
+
super().__init__(object_hook=self.object_hook, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
def object_hook(self, dct: dict[str, Any]) -> Any:
|
|
62
|
+
if "__type__" not in dct:
|
|
63
|
+
return dct
|
|
64
|
+
|
|
65
|
+
match dct["__type__"]:
|
|
66
|
+
case "tuple":
|
|
67
|
+
return tuple(dct["value"])
|
|
68
|
+
case "expression":
|
|
69
|
+
data = BytesIO(cast(str, dct["value"]).encode("utf-8"))
|
|
70
|
+
return pl.Expr.deserialize(data, format="json")
|
|
71
|
+
case "decimal":
|
|
72
|
+
return decimal.Decimal(dct["value"])
|
|
73
|
+
case "datetime":
|
|
74
|
+
return dt.datetime.fromisoformat(dct["value"])
|
|
75
|
+
case "date":
|
|
76
|
+
return dt.date.fromisoformat(dct["value"])
|
|
77
|
+
case "time":
|
|
78
|
+
return dt.time.fromisoformat(dct["value"])
|
|
79
|
+
case "timedelta":
|
|
80
|
+
return dt.timedelta(seconds=float(dct["value"]))
|
|
81
|
+
case "tzinfo":
|
|
82
|
+
return (
|
|
83
|
+
dt.timezone(dt.timedelta(seconds=float(dct["value"])))
|
|
84
|
+
if dct["value"] is not None
|
|
85
|
+
else dt.timezone(dt.timedelta(0))
|
|
86
|
+
)
|
|
87
|
+
case _:
|
|
88
|
+
raise TypeError(f"Unknown type '{dct['__type__']}' in JSON data.")
|