dataframely 1.0.0__tar.gz → 1.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.
Files changed (162) hide show
  1. {dataframely-1.0.0 → dataframely-1.2.0}/.github/workflows/build.yml +5 -5
  2. {dataframely-1.0.0 → dataframely-1.2.0}/.github/workflows/chore.yml +5 -5
  3. {dataframely-1.0.0 → dataframely-1.2.0}/.github/workflows/ci.yml +3 -2
  4. {dataframely-1.0.0 → dataframely-1.2.0}/.github/workflows/update-lockfiles.yml +2 -2
  5. {dataframely-1.0.0 → dataframely-1.2.0}/PKG-INFO +3 -2
  6. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/__init__.py +2 -0
  7. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_base_collection.py +85 -55
  8. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_base_schema.py +2 -2
  9. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_compat.py +1 -1
  10. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_filter.py +1 -1
  11. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_rule.py +2 -2
  12. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/collection.py +14 -7
  13. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/__init__.py +2 -0
  14. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/_mixins.py +1 -1
  15. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/decimal.py +3 -1
  16. dataframely-1.2.0/dataframely/columns/object.py +63 -0
  17. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/config.py +5 -5
  18. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/exc.py +11 -7
  19. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/failure.py +4 -2
  20. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/functional.py +7 -3
  21. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/mypy.py +7 -6
  22. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/random.py +1 -1
  23. {dataframely-1.0.0 → dataframely-1.2.0}/docs/conf.py +1 -1
  24. {dataframely-1.0.0 → dataframely-1.2.0}/docs/sites/quickstart.rst +1 -1
  25. {dataframely-1.0.0 → dataframely-1.2.0}/pyproject.toml +4 -2
  26. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_base.py +13 -11
  27. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_cast.py +5 -5
  28. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_create_empty.py +1 -1
  29. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_filter_one_to_n.py +1 -1
  30. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_filter_validate.py +8 -8
  31. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_ignore_in_filter.py +3 -3
  32. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_implementation.py +32 -14
  33. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_optional_members.py +3 -3
  34. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_sample.py +93 -7
  35. {dataframely-1.0.0 → dataframely-1.2.0}/tests/collection/test_validate_input.py +2 -2
  36. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_any.py +1 -1
  37. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_datetime.py +12 -6
  38. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_decimal.py +7 -7
  39. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_enum.py +2 -2
  40. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_float.py +19 -17
  41. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_integer.py +14 -12
  42. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_list.py +11 -11
  43. dataframely-1.2.0/tests/column_types/test_object.py +60 -0
  44. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_string.py +4 -4
  45. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/test_struct.py +8 -8
  46. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_alias.py +3 -3
  47. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_check.py +1 -1
  48. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_default_dtypes.py +1 -1
  49. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_metadata.py +1 -1
  50. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_pyarrow.py +9 -9
  51. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_rules.py +4 -4
  52. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_sample.py +20 -16
  53. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_sql_schema.py +15 -7
  54. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_str.py +4 -4
  55. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_utils.py +5 -5
  56. {dataframely-1.0.0 → dataframely-1.2.0}/tests/core_validation/test_column_validation.py +2 -2
  57. {dataframely-1.0.0 → dataframely-1.2.0}/tests/core_validation/test_dtype_validation.py +4 -4
  58. {dataframely-1.0.0 → dataframely-1.2.0}/tests/core_validation/test_rule_evaluation.py +7 -7
  59. {dataframely-1.0.0 → dataframely-1.2.0}/tests/functional/test_concat.py +2 -2
  60. {dataframely-1.0.0 → dataframely-1.2.0}/tests/functional/test_relationships.py +2 -2
  61. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_base.py +8 -8
  62. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_cast.py +3 -3
  63. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_create_empty.py +2 -2
  64. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_create_empty_if_none.py +2 -2
  65. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_filter.py +11 -7
  66. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_inheritance.py +1 -1
  67. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_rule_implementation.py +4 -4
  68. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_sample.py +11 -11
  69. {dataframely-1.0.0 → dataframely-1.2.0}/tests/schema/test_validate.py +9 -7
  70. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_compat.py +1 -1
  71. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_config.py +4 -4
  72. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_exc.py +3 -3
  73. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_extre.py +9 -9
  74. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_failure_info.py +1 -1
  75. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_random.py +14 -12
  76. {dataframely-1.0.0 → dataframely-1.2.0}/tests/test_typing.py +36 -14
  77. dataframely-1.0.0/tests/schema/__init__.py +0 -2
  78. {dataframely-1.0.0 → dataframely-1.2.0}/.copier-answers.yml +0 -0
  79. {dataframely-1.0.0 → dataframely-1.2.0}/.envrc +0 -0
  80. {dataframely-1.0.0 → dataframely-1.2.0}/.gitattributes +0 -0
  81. {dataframely-1.0.0 → dataframely-1.2.0}/.github/CODEOWNERS +0 -0
  82. {dataframely-1.0.0 → dataframely-1.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  83. {dataframely-1.0.0 → dataframely-1.2.0}/.github/dependabot.yml +0 -0
  84. {dataframely-1.0.0 → dataframely-1.2.0}/.github/release-drafter.yml +0 -0
  85. {dataframely-1.0.0 → dataframely-1.2.0}/.gitignore +0 -0
  86. {dataframely-1.0.0 → dataframely-1.2.0}/.pre-commit-config.yaml +0 -0
  87. {dataframely-1.0.0 → dataframely-1.2.0}/.prettierignore +0 -0
  88. {dataframely-1.0.0 → dataframely-1.2.0}/.prettierrc +0 -0
  89. {dataframely-1.0.0 → dataframely-1.2.0}/.readthedocs.yml +0 -0
  90. {dataframely-1.0.0 → dataframely-1.2.0}/Cargo.lock +0 -0
  91. {dataframely-1.0.0 → dataframely-1.2.0}/Cargo.toml +0 -0
  92. {dataframely-1.0.0 → dataframely-1.2.0}/LICENSE +0 -0
  93. {dataframely-1.0.0 → dataframely-1.2.0}/README.md +0 -0
  94. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_extre.pyi +0 -0
  95. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_polars.py +0 -0
  96. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_typing.py +0 -0
  97. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/_validation.py +0 -0
  98. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/_base.py +0 -0
  99. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/_utils.py +0 -0
  100. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/any.py +0 -0
  101. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/bool.py +0 -0
  102. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/datetime.py +0 -0
  103. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/enum.py +0 -0
  104. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/float.py +0 -0
  105. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/integer.py +0 -0
  106. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/list.py +0 -0
  107. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/string.py +0 -0
  108. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/columns/struct.py +0 -0
  109. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/py.typed +0 -0
  110. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/schema.py +0 -0
  111. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/__init__.py +0 -0
  112. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/const.py +0 -0
  113. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/factory.py +0 -0
  114. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/mask.py +0 -0
  115. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/rules.py +0 -0
  116. {dataframely-1.0.0 → dataframely-1.2.0}/dataframely/testing/typing.py +0 -0
  117. {dataframely-1.0.0 → dataframely-1.2.0}/docker-compose.yml +0 -0
  118. {dataframely-1.0.0 → dataframely-1.2.0}/docs/Makefile +0 -0
  119. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.collection.rst +0 -0
  120. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.any.rst +0 -0
  121. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.bool.rst +0 -0
  122. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
  123. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
  124. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.enum.rst +0 -0
  125. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.float.rst +0 -0
  126. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.integer.rst +0 -0
  127. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.list.rst +0 -0
  128. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.rst +0 -0
  129. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.string.rst +0 -0
  130. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.columns.struct.rst +0 -0
  131. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.config.rst +0 -0
  132. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.exc.rst +0 -0
  133. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.failure.rst +0 -0
  134. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.functional.rst +0 -0
  135. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.mypy.rst +0 -0
  136. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.random.rst +0 -0
  137. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.rst +0 -0
  138. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.schema.rst +0 -0
  139. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.const.rst +0 -0
  140. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.factory.rst +0 -0
  141. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.mask.rst +0 -0
  142. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.rst +0 -0
  143. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.rules.rst +0 -0
  144. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/dataframely.testing.typing.rst +0 -0
  145. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_api/modules.rst +0 -0
  146. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_static/custom.css +0 -0
  147. {dataframely-1.0.0 → dataframely-1.2.0}/docs/_static/favicon.ico +0 -0
  148. {dataframely-1.0.0 → dataframely-1.2.0}/docs/index.rst +0 -0
  149. {dataframely-1.0.0 → dataframely-1.2.0}/docs/make.bat +0 -0
  150. {dataframely-1.0.0 → dataframely-1.2.0}/docs/sites/development.rst +0 -0
  151. {dataframely-1.0.0 → dataframely-1.2.0}/docs/sites/examples/real-world.ipynb +0 -0
  152. {dataframely-1.0.0 → dataframely-1.2.0}/docs/sites/faq.rst +0 -0
  153. {dataframely-1.0.0 → dataframely-1.2.0}/docs/sites/installation.rst +0 -0
  154. {dataframely-1.0.0 → dataframely-1.2.0}/pixi.lock +0 -0
  155. {dataframely-1.0.0 → dataframely-1.2.0}/pixi.toml +0 -0
  156. {dataframely-1.0.0 → dataframely-1.2.0}/src/errdefs.rs +0 -0
  157. {dataframely-1.0.0 → dataframely-1.2.0}/src/lib.rs +0 -0
  158. {dataframely-1.0.0 → dataframely-1.2.0}/src/regex_repr.rs +0 -0
  159. {dataframely-1.0.0 → dataframely-1.2.0}/tests/column_types/__init__.py +0 -0
  160. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/__init__.py +0 -0
  161. {dataframely-1.0.0 → dataframely-1.2.0}/tests/columns/test_polars_schema.py +0 -0
  162. {dataframely-1.0.0 → dataframely-1.2.0}/tests/core_validation/__init__.py +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  with:
16
16
  fetch-depth: 0
17
17
  - name: Set up pixi
18
- uses: prefix-dev/setup-pixi@0f64e482e3d251f735019b1bc7fb0413ead75b2c # v0.8.2
18
+ uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
19
19
  with:
20
20
  environments: build
21
21
  - name: Set version
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Build project
24
24
  run: pixi run -e build build-sdist
25
25
  - name: Upload package
26
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
26
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
27
27
  with:
28
28
  name: sdist
29
29
  path: dist/*
@@ -50,7 +50,7 @@ jobs:
50
50
  with:
51
51
  fetch-depth: 0
52
52
  - name: Set up pixi
53
- uses: prefix-dev/setup-pixi@0f64e482e3d251f735019b1bc7fb0413ead75b2c # v0.8.2
53
+ uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
54
54
  with:
55
55
  environments: build
56
56
  - name: Set version
@@ -64,7 +64,7 @@ jobs:
64
64
  - name: Check package
65
65
  run: pixi run -e build check-wheel
66
66
  - name: Upload package
67
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
67
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
68
68
  with:
69
69
  name: wheel-${{ matrix.target-platform }}
70
70
  path: dist/*
@@ -78,7 +78,7 @@ jobs:
78
78
  id-token: write
79
79
  environment: pypi
80
80
  steps:
81
- - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
81
+ - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
82
82
  with:
83
83
  path: dist
84
84
  merge-multiple: true
@@ -1,6 +1,6 @@
1
1
  name: Chore
2
2
  on:
3
- pull_request:
3
+ pull_request_target:
4
4
  branches: [main]
5
5
  types: [opened, reopened, edited, synchronize]
6
6
  push:
@@ -13,7 +13,7 @@ concurrency:
13
13
  jobs:
14
14
  check-pr-title:
15
15
  name: Check PR Title
16
- if: github.event_name == 'pull_request'
16
+ if: github.event_name == 'pull_request_target'
17
17
  runs-on: ubuntu-latest
18
18
  permissions:
19
19
  contents: read
@@ -51,16 +51,16 @@ jobs:
51
51
  delete: true
52
52
 
53
53
  release-drafter:
54
- name: ${{ github.event_name == 'pull_request' && 'Assign Labels' || 'Draft Release' }}
54
+ name: ${{ github.event_name == 'pull_request_target' && 'Assign Labels' || 'Draft Release' }}
55
55
  runs-on: ubuntu-latest
56
56
  permissions:
57
57
  contents: write
58
58
  pull-requests: write
59
59
  steps:
60
- - name: ${{ github.event_name == 'pull_request' && 'Assign labels' || 'Update release draft' }}
60
+ - name: ${{ github.event_name == 'pull_request_target' && 'Assign labels' || 'Update release draft' }}
61
61
  uses: release-drafter/release-drafter@v6
62
62
  with:
63
- disable-releaser: ${{ github.event_name == 'pull_request' }}
63
+ disable-releaser: ${{ github.event_name == 'pull_request_target' }}
64
64
  disable-autolabeler: ${{ github.event_name == 'push' }}
65
65
  env:
66
66
  GITHUB_TOKEN: ${{ github.token }}
@@ -21,7 +21,7 @@ jobs:
21
21
  # needed for 'pre-commit-mirrors-insert-license'
22
22
  fetch-depth: 0
23
23
  - name: Set up pixi
24
- uses: prefix-dev/setup-pixi@0f64e482e3d251f735019b1bc7fb0413ead75b2c # v0.8.2
24
+ uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
25
25
  with:
26
26
  environments: default lint
27
27
  - name: Install repository
@@ -42,7 +42,7 @@ jobs:
42
42
  - name: Checkout branch
43
43
  uses: actions/checkout@v4
44
44
  - name: Set up pixi
45
- uses: prefix-dev/setup-pixi@8eaba7c61d661f73d558b0b477156b7b62667fa4
45
+ uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
46
46
  with:
47
47
  environments: ${{ matrix.environment }}
48
48
  - name: Install repository
@@ -53,3 +53,4 @@ jobs:
53
53
  uses: codecov/codecov-action@v5
54
54
  with:
55
55
  files: ./coverage.xml
56
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -15,13 +15,13 @@ jobs:
15
15
  steps:
16
16
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17
17
  - name: Set up pixi
18
- uses: prefix-dev/setup-pixi@0f64e482e3d251f735019b1bc7fb0413ead75b2c # v0.8.2
18
+ uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8
19
19
  with:
20
20
  run-install: false
21
21
  - name: Update lockfiles
22
22
  run: pixi update --json --no-install | pixi exec pixi-diff-to-markdown >> diff.md
23
23
  - name: Create pull request
24
- uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
24
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
25
25
  with:
26
26
  token: ${{ github.token }}
27
27
  commit-message: Update pixi lockfile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframely
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -12,7 +12,8 @@ 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>
13
13
  Requires-Python: >=3.11
14
14
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
- Project-URL: repository, https://github.com/quantco/dataframely
15
+ Project-URL: Repository, https://github.com/quantco/dataframely
16
+ Project-URL: Documentation, https://dataframely.readthedocs.io/
16
17
 
17
18
  <!-- LOGO -->
18
19
  <br />
@@ -34,6 +34,7 @@ from .columns import (
34
34
  Int64,
35
35
  Integer,
36
36
  List,
37
+ Object,
37
38
  String,
38
39
  Struct,
39
40
  Time,
@@ -89,4 +90,5 @@ __all__ = [
89
90
  "String",
90
91
  "Struct",
91
92
  "List",
93
+ "Object",
92
94
  ]
@@ -7,7 +7,7 @@ import typing
7
7
  from abc import ABCMeta
8
8
  from collections.abc import Iterable
9
9
  from dataclasses import dataclass, field
10
- from typing import Annotated, Any, Self, get_args, get_origin
10
+ from typing import Annotated, Any, Self, cast, get_args, get_origin
11
11
 
12
12
  import polars as pl
13
13
 
@@ -48,6 +48,12 @@ class CollectionMember:
48
48
 
49
49
  #: Whether the member should be ignored in the filter method.
50
50
  ignored_in_filters: bool = False
51
+ #: Whether the member's non-primary key columns should be inlined for sampling.
52
+ #: This means that value overrides are supplied on the top-level rather than in
53
+ #: a subkey with the member's name. Only valid if the member's primary key matches
54
+ #: the collection's common primary key. Two members that share common column names
55
+ #: may not both be inlined for sampling.
56
+ inline_for_sampling: bool = False
51
57
 
52
58
 
53
59
  # --------------------------------------- UTILS -------------------------------------- #
@@ -79,7 +85,7 @@ class Metadata:
79
85
  members: dict[str, MemberInfo] = field(default_factory=dict)
80
86
  filters: dict[str, Filter] = field(default_factory=dict)
81
87
 
82
- def update(self, other: Self):
88
+ def update(self, other: Self) -> None:
83
89
  self.members.update(other.members)
84
90
  self.filters.update(other.filters)
85
91
 
@@ -92,7 +98,7 @@ class CollectionMeta(ABCMeta):
92
98
  namespace: dict[str, Any],
93
99
  *args: Any,
94
100
  **kwargs: Any,
95
- ):
101
+ ) -> CollectionMeta:
96
102
  result = Metadata()
97
103
  for base in bases:
98
104
  result.update(mcs._get_metadata_recursively(base))
@@ -136,6 +142,30 @@ class CollectionMeta(ABCMeta):
136
142
  f"{len(intersection)} such filters: {sorted(intersection)}."
137
143
  )
138
144
 
145
+ # 3) Check that inlining for sampling is configured correctly.
146
+ if len(non_ignored_member_schemas) > 0:
147
+ common_primary_keys = _common_primary_keys(non_ignored_member_schemas)
148
+ inlined_columns: set[str] = set()
149
+ for member, info in result.members.items():
150
+ if info.inline_for_sampling:
151
+ if set(info.schema.primary_keys()) != common_primary_keys:
152
+ raise ImplementationError(
153
+ f"Member '{member}' is inlined for sampling but its primary "
154
+ "key is a superset of the common primary key. Such a member "
155
+ "must not be inlined to be able to provide multiple values "
156
+ "for a single combination of the common primary key."
157
+ )
158
+ non_primary_key_columns = (
159
+ set(info.schema.column_names()) - common_primary_keys
160
+ )
161
+ if len(inlined_columns & non_primary_key_columns):
162
+ raise ImplementationError(
163
+ f"At least one column name of member '{member}' clashes "
164
+ "with a column name of another member that is inlined for "
165
+ "sampling."
166
+ )
167
+ inlined_columns.update(non_primary_key_columns)
168
+
139
169
  return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
140
170
 
141
171
  @staticmethod
@@ -153,58 +183,9 @@ class CollectionMeta(ABCMeta):
153
183
  # Get all members via the annotations
154
184
  if "__annotations__" in source:
155
185
  for attr, kls in source["__annotations__"].items():
156
- origin = get_origin(kls)
157
-
158
- # optional annotation
159
- collection_member = CollectionMember()
160
-
161
- if origin is Annotated:
162
- annotation_args = get_args(kls)
163
- origin_arg0 = get_origin(annotation_args[0])
164
- if not origin_arg0 or not issubclass(origin_arg0, TypedLazyFrame):
165
- raise AnnotationImplementationError(attr, kls)
166
- if len(annotation_args) > 2:
167
- raise AnnotationImplementationError(attr, kls)
168
- if not isinstance(annotation_args[1], CollectionMember):
169
- raise AnnotationImplementationError(attr, kls)
170
-
171
- # Continue with wrapped FrameType
172
- collection_member = annotation_args[1]
173
- kls = annotation_args[0]
174
- origin = origin_arg0
175
-
176
- if origin is None:
177
- # `None` annotation is not allowed
178
- raise AnnotationImplementationError(attr, kls)
179
- elif origin == typing.Union:
180
- # Happy path: optional member
181
- union_args = get_args(kls)
182
- if len(union_args) != 2:
183
- raise AnnotationImplementationError(attr, kls)
184
- if not any(get_origin(arg) is None for arg in union_args):
185
- raise AnnotationImplementationError(attr, kls)
186
-
187
- [not_none_arg] = [
188
- arg for arg in union_args if get_origin(arg) is not None
189
- ]
190
- if not issubclass(get_origin(not_none_arg), TypedLazyFrame):
191
- raise AnnotationImplementationError(attr, kls)
192
-
193
- result.members[attr] = MemberInfo(
194
- schema=get_args(not_none_arg)[0],
195
- is_optional=True,
196
- ignored_in_filters=collection_member.ignored_in_filters,
197
- )
198
- elif issubclass(origin, TypedLazyFrame):
199
- # Happy path: required member
200
- result.members[attr] = MemberInfo(
201
- schema=get_args(kls)[0],
202
- is_optional=False,
203
- ignored_in_filters=collection_member.ignored_in_filters,
204
- )
205
- else:
206
- # Some other unknown annotation
207
- raise AnnotationImplementationError(attr, kls)
186
+ result.members[attr] = CollectionMeta._derive_member_info(
187
+ attr, kls, CollectionMember()
188
+ )
208
189
 
209
190
  # Get all filters by traversing the source
210
191
  for attr, value in {
@@ -215,6 +196,55 @@ class CollectionMeta(ABCMeta):
215
196
 
216
197
  return result
217
198
 
199
+ @staticmethod
200
+ def _derive_member_info(
201
+ attr: str, type_annotation: Any, collection_member: CollectionMember
202
+ ) -> MemberInfo:
203
+ origin = get_origin(type_annotation)
204
+
205
+ if origin is None:
206
+ # `None` annotation is not allowed
207
+ raise AnnotationImplementationError(attr, type_annotation)
208
+ elif origin == Annotated:
209
+ # Maybe happy path: annotated member, dispatch recursively
210
+ annotation_args = cast(list[Any], get_args(type_annotation))
211
+ if len(annotation_args) > 2:
212
+ raise AnnotationImplementationError(attr, type_annotation)
213
+ if not isinstance(annotation_args[1], CollectionMember):
214
+ raise AnnotationImplementationError(attr, type_annotation)
215
+ return CollectionMeta._derive_member_info(
216
+ attr, annotation_args[0], annotation_args[1]
217
+ )
218
+ elif origin == typing.Union:
219
+ # Happy path: optional member
220
+ union_args = get_args(type_annotation)
221
+ if len(union_args) != 2:
222
+ raise AnnotationImplementationError(attr, type_annotation)
223
+ if not any(get_origin(arg) is None for arg in union_args):
224
+ raise AnnotationImplementationError(attr, type_annotation)
225
+
226
+ [not_none_arg] = [arg for arg in union_args if get_origin(arg) is not None]
227
+ if not issubclass(get_origin(not_none_arg), TypedLazyFrame):
228
+ raise AnnotationImplementationError(attr, type_annotation)
229
+
230
+ return MemberInfo(
231
+ schema=get_args(not_none_arg)[0],
232
+ is_optional=True,
233
+ ignored_in_filters=collection_member.ignored_in_filters,
234
+ inline_for_sampling=collection_member.inline_for_sampling,
235
+ )
236
+ elif issubclass(origin, TypedLazyFrame):
237
+ # Happy path: required member
238
+ return MemberInfo(
239
+ schema=get_args(type_annotation)[0],
240
+ is_optional=False,
241
+ ignored_in_filters=collection_member.ignored_in_filters,
242
+ inline_for_sampling=collection_member.inline_for_sampling,
243
+ )
244
+ else:
245
+ # Some other unknown annotation
246
+ raise AnnotationImplementationError(attr, type_annotation)
247
+
218
248
 
219
249
  class BaseCollection(metaclass=CollectionMeta):
220
250
  """Internal utility abstraction to reference collections without introducing
@@ -58,7 +58,7 @@ class Metadata:
58
58
  columns: dict[str, Column] = field(default_factory=dict)
59
59
  rules: dict[str, Rule] = field(default_factory=dict)
60
60
 
61
- def update(self, other: Self):
61
+ def update(self, other: Self) -> None:
62
62
  self.columns.update(other.columns)
63
63
  self.rules.update(other.rules)
64
64
 
@@ -71,7 +71,7 @@ class SchemaMeta(ABCMeta):
71
71
  namespace: dict[str, Any],
72
72
  *args: Any,
73
73
  **kwargs: Any,
74
- ):
74
+ ) -> SchemaMeta:
75
75
  result = Metadata()
76
76
  for base in bases:
77
77
  result.update(mcs._get_metadata_recursively(base))
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
 
8
8
  class _DummyModule: # pragma: no cover
9
- def __init__(self, module: str):
9
+ def __init__(self, module: str) -> None:
10
10
  self.module = module
11
11
 
12
12
  def __getattr__(self, name: str) -> Any:
@@ -12,7 +12,7 @@ C = TypeVar("C")
12
12
  class Filter(Generic[C]):
13
13
  """Internal class representing logic for filtering members of a collection."""
14
14
 
15
- def __init__(self, logic: Callable[[C], pl.LazyFrame]):
15
+ def __init__(self, logic: Callable[[C], pl.LazyFrame]) -> None:
16
16
  self.logic = logic
17
17
 
18
18
 
@@ -12,14 +12,14 @@ ValidationFunction = Callable[[], pl.Expr]
12
12
  class Rule:
13
13
  """Internal class representing validation rules."""
14
14
 
15
- def __init__(self, expr: pl.Expr):
15
+ def __init__(self, expr: pl.Expr) -> None:
16
16
  self.expr = expr
17
17
 
18
18
 
19
19
  class GroupRule(Rule):
20
20
  """Rule that is evaluated on a group of columns."""
21
21
 
22
- def __init__(self, expr: pl.Expr, group_columns: list[str]):
22
+ def __init__(self, expr: pl.Expr, group_columns: list[str]) -> None:
23
23
  super().__init__(expr)
24
24
  self.group_columns = group_columns
25
25
 
@@ -4,7 +4,7 @@
4
4
  import sys
5
5
  import warnings
6
6
  from abc import ABC
7
- from collections.abc import Mapping, MutableMapping, Sequence
7
+ from collections.abc import Mapping, Sequence
8
8
  from pathlib import Path
9
9
  from typing import Any, Generic, Self, TypeVar, cast
10
10
 
@@ -20,10 +20,10 @@ from .random import Generator
20
20
 
21
21
  if sys.version_info >= (3, 13):
22
22
  SamplingType = TypeVar(
23
- "SamplingType", bound=MutableMapping[str, Any], default=dict[str, Any]
23
+ "SamplingType", bound=Mapping[str, Any], default=Mapping[str, Any]
24
24
  )
25
25
  else: # pragma: no cover
26
- SamplingType = TypeVar("SamplingType", bound=MutableMapping[str, Any])
26
+ SamplingType = TypeVar("SamplingType", bound=Mapping[str, Any])
27
27
 
28
28
 
29
29
  class Collection(BaseCollection, ABC, Generic[SamplingType]):
@@ -123,7 +123,10 @@ class Collection(BaseCollection, ABC, Generic[SamplingType]):
123
123
  ...
124
124
  }
125
125
 
126
- _Any_ member/value can be left out and will be sampled automatically.
126
+ *Any* member/value can be left out and will be sampled automatically.
127
+ Note that overrides for columns of members that are annotated with
128
+ ``inline_for_sampling=True`` can be supplied on the top-level instead
129
+ of in a nested dictionary.
127
130
  generator: The (seeded) generator to use for sampling data. If ``None``, a
128
131
  generator with random seed is automatically created.
129
132
 
@@ -198,7 +201,11 @@ class Collection(BaseCollection, ABC, Generic[SamplingType]):
198
201
  else _extract_keys_if_exist(sample, primary_keys)
199
202
  ),
200
203
  **_extract_keys_if_exist(
201
- sample[member] if member in sample else {},
204
+ (
205
+ sample
206
+ if member_infos[member].inline_for_sampling
207
+ else (sample[member] if member in sample else {})
208
+ ),
202
209
  schema.column_names(),
203
210
  ),
204
211
  }
@@ -498,7 +505,7 @@ class Collection(BaseCollection, ABC, Generic[SamplingType]):
498
505
 
499
506
  # ---------------------------------- PERSISTENCE --------------------------------- #
500
507
 
501
- def write_parquet(self, directory: Path):
508
+ def write_parquet(self, directory: Path) -> None:
502
509
  """Write the members of this collection to Parquet files in a directory.
503
510
 
504
511
  This method writes one Parquet file per member into the provided directory.
@@ -590,7 +597,7 @@ class Collection(BaseCollection, ABC, Generic[SamplingType]):
590
597
  return out
591
598
 
592
599
  @classmethod
593
- def _validate_input_keys(cls, data: Mapping[str, FrameType], /):
600
+ def _validate_input_keys(cls, data: Mapping[str, FrameType], /) -> None:
594
601
  actual = set(data)
595
602
 
596
603
  missing = cls.required_members() - actual
@@ -10,6 +10,7 @@ from .enum import Enum
10
10
  from .float import Float, Float32, Float64
11
11
  from .integer import Int8, Int16, Int32, Int64, Integer, UInt8, UInt16, UInt32, UInt64
12
12
  from .list import List
13
+ from .object import Object
13
14
  from .string import String
14
15
  from .struct import Struct
15
16
 
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "Int32",
32
33
  "Int64",
33
34
  "Integer",
35
+ "Object",
34
36
  "UInt8",
35
37
  "UInt16",
36
38
  "UInt32",
@@ -83,7 +83,7 @@ U = TypeVar("U")
83
83
  class IsInMixin(Generic[U], Base):
84
84
  """Mixin to use for types implementing "is in"."""
85
85
 
86
- def __init__(self, *, is_in: Sequence[U] | None = None, **kwargs: Any):
86
+ def __init__(self, *, is_in: Sequence[U] | None = None, **kwargs: Any) -> None:
87
87
  super().__init__(**kwargs)
88
88
  self.is_in = is_in
89
89
 
@@ -148,7 +148,9 @@ class Decimal(OrdinalMixin[decimal.Decimal], Column):
148
148
  # --------------------------------------- UTILS -------------------------------------- #
149
149
 
150
150
 
151
- def _validate(value: decimal.Decimal, precision: int | None, scale: int, name: str):
151
+ def _validate(
152
+ value: decimal.Decimal, precision: int | None, scale: int, name: str
153
+ ) -> None:
152
154
  exponent = value.as_tuple().exponent
153
155
  if not isinstance(exponent, int):
154
156
  raise ValueError(f"Encountered 'inf' or 'NaN' for `{name}`.")
@@ -0,0 +1,63 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import polars as pl
10
+
11
+ from dataframely._compat import pa, sa, sa_TypeEngine
12
+ from dataframely.random import Generator
13
+
14
+ from ._base import Column
15
+
16
+
17
+ class Object(Column):
18
+ """A Python Object column."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ nullable: bool = True,
24
+ primary_key: bool = False,
25
+ check: Callable[[pl.Expr], pl.Expr] | None = None,
26
+ alias: str | None = None,
27
+ metadata: dict[str, Any] | None = None,
28
+ ):
29
+ """
30
+ Args:
31
+ nullable: Whether this column may contain null values.
32
+ primary_key: Whether this column is part of the primary key of the schema.
33
+ check: A custom check to run for this column. Must return a non-aggregated
34
+ boolean expression.
35
+ alias: An overwrite for this column's name which allows for using a column
36
+ name that is not a valid Python identifier. Especially note that setting
37
+ this option does _not_ allow to refer to the column with two different
38
+ names, the specified alias is the only valid name.
39
+ metadata: A dictionary of metadata to attach to the column.
40
+ """
41
+ super().__init__(
42
+ nullable=nullable,
43
+ primary_key=primary_key,
44
+ check=check,
45
+ alias=alias,
46
+ metadata=metadata,
47
+ )
48
+
49
+ @property
50
+ def dtype(self) -> pl.DataType:
51
+ return pl.Object()
52
+
53
+ def sqlalchemy_dtype(self, dialect: sa.Dialect) -> sa_TypeEngine:
54
+ raise NotImplementedError("SQL column cannot have 'Object' type.")
55
+
56
+ @property
57
+ def pyarrow_dtype(self) -> pa.DataType:
58
+ raise NotImplementedError("PyArrow column cannot have 'Object' type.")
59
+
60
+ def _sample_unchecked(self, generator: Generator, n: int) -> pl.Series:
61
+ raise NotImplementedError(
62
+ "Random data sampling not implemented for 'Object' type."
63
+ )
@@ -25,23 +25,23 @@ class Config(contextlib.ContextDecorator):
25
25
  #: Singleton stack to track where to go back after exiting a context.
26
26
  _stack: list[Options] = []
27
27
 
28
- def __init__(self, **options: Unpack[Options]):
28
+ def __init__(self, **options: Unpack[Options]) -> None:
29
29
  self._local_options: Options = {**default_options(), **options}
30
30
 
31
31
  @staticmethod
32
- def set_max_sampling_iterations(iterations: int):
32
+ def set_max_sampling_iterations(iterations: int) -> None:
33
33
  """Set the maximum number of sampling iterations to use on
34
34
  :meth:`Schema.sample`."""
35
35
  Config.options["max_sampling_iterations"] = iterations
36
36
 
37
37
  @staticmethod
38
- def restore_defaults():
38
+ def restore_defaults() -> None:
39
39
  """Restore the defaults of the configuration."""
40
40
  Config.options = default_options()
41
41
 
42
42
  # ------------------------------------ CONTEXT ----------------------------------- #
43
43
 
44
- def __enter__(self):
44
+ def __enter__(self) -> None:
45
45
  Config._stack.append(Config.options)
46
46
  Config.options = self._local_options
47
47
 
@@ -50,5 +50,5 @@ class Config(contextlib.ContextDecorator):
50
50
  exc_type: type[BaseException] | None,
51
51
  exc_val: BaseException | None,
52
52
  exc_tb: TracebackType | None,
53
- ):
53
+ ) -> None:
54
54
  Config.options = Config._stack.pop()
@@ -11,7 +11,7 @@ from ._polars import PolarsDataType
11
11
  class ValidationError(Exception):
12
12
  """Error raised when :mod:`dataframely` validation encounters an issue."""
13
13
 
14
- def __init__(self, message: str):
14
+ def __init__(self, message: str) -> None:
15
15
  super().__init__()
16
16
  self.message = message
17
17
 
@@ -22,7 +22,9 @@ class ValidationError(Exception):
22
22
  class DtypeValidationError(ValidationError):
23
23
  """Validation error raised when column dtypes are wrong."""
24
24
 
25
- def __init__(self, errors: dict[str, tuple[PolarsDataType, PolarsDataType]]):
25
+ def __init__(
26
+ self, errors: dict[str, tuple[PolarsDataType, PolarsDataType]]
27
+ ) -> None:
26
28
  super().__init__(f"{len(errors)} columns have an invalid dtype")
27
29
  self.errors = errors
28
30
 
@@ -37,7 +39,7 @@ class DtypeValidationError(ValidationError):
37
39
  class RuleValidationError(ValidationError):
38
40
  """Complex validation error raised when rule validation fails."""
39
41
 
40
- def __init__(self, errors: dict[str, int]):
42
+ def __init__(self, errors: dict[str, int]) -> None:
41
43
  super().__init__(f"{len(errors)} rules failed validation")
42
44
 
43
45
  # Split into schema errors and column errors
@@ -75,11 +77,11 @@ class RuleValidationError(ValidationError):
75
77
  class MemberValidationError(ValidationError):
76
78
  """Validation error raised when multiple members of a collection fail validation."""
77
79
 
78
- def __init__(self, errors: dict[str, ValidationError]):
80
+ def __init__(self, errors: dict[str, ValidationError]) -> None:
79
81
  super().__init__(f"{len(errors)} members failed validation")
80
82
  self.errors = errors
81
83
 
82
- def __str__(self):
84
+ def __str__(self) -> str:
83
85
  details = [
84
86
  f" > Member '{name}' failed validation:\n"
85
87
  + "\n".join(" " + line for line in str(error).split("\n"))
@@ -95,7 +97,7 @@ class ImplementationError(Exception):
95
97
  class AnnotationImplementationError(ImplementationError):
96
98
  """Error raised when the annotations of a collection are invalid."""
97
99
 
98
- def __init__(self, attr: str, kls: type):
100
+ def __init__(self, attr: str, kls: type) -> None:
99
101
  message = (
100
102
  "Annotations of a 'dy.Collection' may only be an (optional) "
101
103
  f"'dy.LazyFrame', but \"{attr}\" has type '{kls}'."
@@ -106,7 +108,9 @@ class AnnotationImplementationError(ImplementationError):
106
108
  class RuleImplementationError(ImplementationError):
107
109
  """Error raised when a rule is implemented incorrectly."""
108
110
 
109
- def __init__(self, name: str, return_dtype: pl.DataType, is_group_rule: bool):
111
+ def __init__(
112
+ self, name: str, return_dtype: pl.DataType, is_group_rule: bool
113
+ ) -> None:
110
114
  if is_group_rule:
111
115
  details = (
112
116
  " When implementing a group rule (i.e. when using the `group_by` "