dataframely 1.8.2__tar.gz → 1.10.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 (200) hide show
  1. {dataframely-1.8.2 → dataframely-1.10.0}/.github/workflows/ci.yml +9 -1
  2. {dataframely-1.8.2 → dataframely-1.10.0}/.gitignore +3 -0
  3. {dataframely-1.8.2 → dataframely-1.10.0}/Cargo.toml +1 -0
  4. {dataframely-1.8.2 → dataframely-1.10.0}/PKG-INFO +7 -1
  5. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_base_collection.py +3 -3
  6. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_base_schema.py +24 -7
  7. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_compat.py +29 -1
  8. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_polars.py +0 -22
  9. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_rule.py +35 -9
  10. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_serialization.py +0 -2
  11. dataframely-1.10.0/dataframely/_storage/__init__.py +8 -0
  12. dataframely-1.10.0/dataframely/_storage/_base.py +204 -0
  13. dataframely-1.10.0/dataframely/_storage/_exc.py +10 -0
  14. dataframely-1.10.0/dataframely/_storage/constants.py +6 -0
  15. dataframely-1.10.0/dataframely/_storage/delta.py +200 -0
  16. dataframely-1.10.0/dataframely/_storage/parquet.py +239 -0
  17. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/collection.py +364 -133
  18. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/decimal.py +6 -5
  19. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/failure.py +135 -32
  20. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/schema.py +311 -78
  21. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/factory.py +23 -3
  22. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/rules.py +1 -1
  23. dataframely-1.10.0/dataframely/testing/storage.py +293 -0
  24. {dataframely-1.8.2 → dataframely-1.10.0}/pixi.lock +6250 -6485
  25. {dataframely-1.8.2 → dataframely-1.10.0}/pixi.toml +9 -3
  26. {dataframely-1.8.2 → dataframely-1.10.0}/pyproject.toml +9 -1
  27. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_filter_validate.py +23 -0
  28. dataframely-1.10.0/tests/collection/test_join.py +92 -0
  29. dataframely-1.10.0/tests/collection/test_storage.py +407 -0
  30. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_array.py +1 -7
  31. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_decimal.py +16 -3
  32. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_pyarrow.py +2 -0
  33. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_sql_schema.py +10 -10
  34. dataframely-1.10.0/tests/failure_info/test_storage.py +131 -0
  35. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_filter.py +36 -0
  36. dataframely-1.10.0/tests/schema/test_read_write_parquet.py +27 -0
  37. dataframely-1.10.0/tests/schema/test_storage.py +250 -0
  38. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_validate.py +20 -0
  39. dataframely-1.10.0/tests/storage/test_delta.py +54 -0
  40. dataframely-1.10.0/tests/test_factory.py +89 -0
  41. dataframely-1.8.2/tests/collection/test_read_write_parquet.py +0 -398
  42. dataframely-1.8.2/tests/schema/test_read_write_parquet.py +0 -234
  43. dataframely-1.8.2/tests/test_failure_info.py +0 -81
  44. {dataframely-1.8.2 → dataframely-1.10.0}/.copier-answers.yml +0 -0
  45. {dataframely-1.8.2 → dataframely-1.10.0}/.envrc +0 -0
  46. {dataframely-1.8.2 → dataframely-1.10.0}/.gitattributes +0 -0
  47. {dataframely-1.8.2 → dataframely-1.10.0}/.github/CODEOWNERS +0 -0
  48. {dataframely-1.8.2 → dataframely-1.10.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  49. {dataframely-1.8.2 → dataframely-1.10.0}/.github/dependabot.yml +0 -0
  50. {dataframely-1.8.2 → dataframely-1.10.0}/.github/release-drafter.yml +0 -0
  51. {dataframely-1.8.2 → dataframely-1.10.0}/.github/workflows/build.yml +0 -0
  52. {dataframely-1.8.2 → dataframely-1.10.0}/.github/workflows/chore.yml +0 -0
  53. {dataframely-1.8.2 → dataframely-1.10.0}/.github/workflows/nightly.yml +0 -0
  54. {dataframely-1.8.2 → dataframely-1.10.0}/.github/workflows/scorecard.yml +0 -0
  55. {dataframely-1.8.2 → dataframely-1.10.0}/.pre-commit-config.yaml +0 -0
  56. {dataframely-1.8.2 → dataframely-1.10.0}/.prettierignore +0 -0
  57. {dataframely-1.8.2 → dataframely-1.10.0}/.prettierrc +0 -0
  58. {dataframely-1.8.2 → dataframely-1.10.0}/.readthedocs.yml +0 -0
  59. {dataframely-1.8.2 → dataframely-1.10.0}/Cargo.lock +0 -0
  60. {dataframely-1.8.2 → dataframely-1.10.0}/LICENSE +0 -0
  61. {dataframely-1.8.2 → dataframely-1.10.0}/README.md +0 -0
  62. {dataframely-1.8.2 → dataframely-1.10.0}/SECURITY.md +0 -0
  63. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/__init__.py +0 -0
  64. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_deprecation.py +0 -0
  65. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_extre.pyi +0 -0
  66. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_filter.py +0 -0
  67. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_typing.py +0 -0
  68. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/_validation.py +0 -0
  69. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/__init__.py +0 -0
  70. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/_base.py +0 -0
  71. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/_mixins.py +0 -0
  72. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/_registry.py +0 -0
  73. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/_utils.py +0 -0
  74. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/any.py +0 -0
  75. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/array.py +0 -0
  76. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/bool.py +0 -0
  77. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/categorical.py +0 -0
  78. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/datetime.py +0 -0
  79. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/enum.py +0 -0
  80. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/float.py +0 -0
  81. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/integer.py +0 -0
  82. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/list.py +0 -0
  83. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/object.py +0 -0
  84. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/string.py +0 -0
  85. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/columns/struct.py +0 -0
  86. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/config.py +0 -0
  87. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/exc.py +0 -0
  88. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/functional.py +0 -0
  89. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/mypy.py +0 -0
  90. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/py.typed +0 -0
  91. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/random.py +0 -0
  92. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/__init__.py +0 -0
  93. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/const.py +0 -0
  94. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/mask.py +0 -0
  95. {dataframely-1.8.2 → dataframely-1.10.0}/dataframely/testing/typing.py +0 -0
  96. {dataframely-1.8.2 → dataframely-1.10.0}/docker-compose.yml +0 -0
  97. {dataframely-1.8.2 → dataframely-1.10.0}/docs/Makefile +0 -0
  98. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.collection.rst +0 -0
  99. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.any.rst +0 -0
  100. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.bool.rst +0 -0
  101. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
  102. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
  103. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.enum.rst +0 -0
  104. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.float.rst +0 -0
  105. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.integer.rst +0 -0
  106. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.list.rst +0 -0
  107. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.rst +0 -0
  108. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.string.rst +0 -0
  109. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.columns.struct.rst +0 -0
  110. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.config.rst +0 -0
  111. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.exc.rst +0 -0
  112. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.failure.rst +0 -0
  113. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.functional.rst +0 -0
  114. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.mypy.rst +0 -0
  115. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.random.rst +0 -0
  116. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.rst +0 -0
  117. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.schema.rst +0 -0
  118. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.const.rst +0 -0
  119. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.factory.rst +0 -0
  120. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.mask.rst +0 -0
  121. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.rst +0 -0
  122. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.rules.rst +0 -0
  123. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/dataframely.testing.typing.rst +0 -0
  124. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_api/modules.rst +0 -0
  125. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_static/custom.css +0 -0
  126. {dataframely-1.8.2 → dataframely-1.10.0}/docs/_static/favicon.ico +0 -0
  127. {dataframely-1.8.2 → dataframely-1.10.0}/docs/conf.py +0 -0
  128. {dataframely-1.8.2 → dataframely-1.10.0}/docs/index.rst +0 -0
  129. {dataframely-1.8.2 → dataframely-1.10.0}/docs/make.bat +0 -0
  130. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/development.rst +0 -0
  131. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/examples/real-world.ipynb +0 -0
  132. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/faq.rst +0 -0
  133. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/installation.rst +0 -0
  134. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/quickstart.rst +0 -0
  135. {dataframely-1.8.2 → dataframely-1.10.0}/docs/sites/versioning.rst +0 -0
  136. {dataframely-1.8.2 → dataframely-1.10.0}/src/errdefs.rs +0 -0
  137. {dataframely-1.8.2 → dataframely-1.10.0}/src/lib.rs +0 -0
  138. {dataframely-1.8.2 → dataframely-1.10.0}/src/regex_repr.rs +0 -0
  139. {dataframely-1.8.2 → dataframely-1.10.0}/tests/benches/conftest.py +0 -0
  140. {dataframely-1.8.2 → dataframely-1.10.0}/tests/benches/test_collection.py +0 -0
  141. {dataframely-1.8.2 → dataframely-1.10.0}/tests/benches/test_failure.py +0 -0
  142. {dataframely-1.8.2 → dataframely-1.10.0}/tests/benches/test_schema.py +0 -0
  143. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_base.py +0 -0
  144. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_cast.py +0 -0
  145. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_collection_future_annotations.py +0 -0
  146. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_create_empty.py +0 -0
  147. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_filter_one_to_n.py +0 -0
  148. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_ignore_in_filter.py +0 -0
  149. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_implementation.py +0 -0
  150. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_matches.py +0 -0
  151. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_optional_members.py +0 -0
  152. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_repr.py +0 -0
  153. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_sample.py +0 -0
  154. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_serialization.py +0 -0
  155. {dataframely-1.8.2 → dataframely-1.10.0}/tests/collection/test_validate_input.py +0 -0
  156. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/__init__.py +0 -0
  157. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_any.py +0 -0
  158. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_datetime.py +0 -0
  159. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_enum.py +0 -0
  160. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_float.py +0 -0
  161. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_integer.py +0 -0
  162. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_list.py +0 -0
  163. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_object.py +0 -0
  164. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_string.py +0 -0
  165. {dataframely-1.8.2 → dataframely-1.10.0}/tests/column_types/test_struct.py +0 -0
  166. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/__init__.py +0 -0
  167. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_alias.py +0 -0
  168. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_check.py +0 -0
  169. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_default_dtypes.py +0 -0
  170. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_matches.py +0 -0
  171. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_metadata.py +0 -0
  172. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_polars_schema.py +0 -0
  173. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_rules.py +0 -0
  174. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_sample.py +0 -0
  175. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_str.py +0 -0
  176. {dataframely-1.8.2 → dataframely-1.10.0}/tests/columns/test_utils.py +0 -0
  177. {dataframely-1.8.2 → dataframely-1.10.0}/tests/core_validation/__init__.py +0 -0
  178. {dataframely-1.8.2 → dataframely-1.10.0}/tests/core_validation/test_column_validation.py +0 -0
  179. {dataframely-1.8.2 → dataframely-1.10.0}/tests/core_validation/test_dtype_validation.py +0 -0
  180. {dataframely-1.8.2 → dataframely-1.10.0}/tests/core_validation/test_rule_evaluation.py +0 -0
  181. {dataframely-1.8.2 → dataframely-1.10.0}/tests/functional/test_concat.py +0 -0
  182. {dataframely-1.8.2 → dataframely-1.10.0}/tests/functional/test_relationships.py +0 -0
  183. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_base.py +0 -0
  184. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_cast.py +0 -0
  185. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_create_empty.py +0 -0
  186. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_create_empty_if_none.py +0 -0
  187. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_inheritance.py +0 -0
  188. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_matches.py +0 -0
  189. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_repr.py +0 -0
  190. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_rule_implementation.py +0 -0
  191. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_sample.py +0 -0
  192. {dataframely-1.8.2 → dataframely-1.10.0}/tests/schema/test_serialization.py +0 -0
  193. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_compat.py +0 -0
  194. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_config.py +0 -0
  195. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_deprecation.py +0 -0
  196. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_exc.py +0 -0
  197. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_extre.py +0 -0
  198. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_random.py +0 -0
  199. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_serialization.py +0 -0
  200. {dataframely-1.8.2 → dataframely-1.10.0}/tests/test_typing.py +0 -0
@@ -41,6 +41,14 @@ jobs:
41
41
  matrix:
42
42
  os: [ubuntu-latest, windows-latest]
43
43
  environment: [py310, py311, py312, py313]
44
+ with_optionals: [false]
45
+ include:
46
+ - os: ubuntu-latest
47
+ environment: py313-optionals
48
+ with_optionals: true
49
+ - os: windows-latest
50
+ environment: py313-optionals
51
+ with_optionals: true
44
52
  steps:
45
53
  - name: Checkout branch
46
54
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -51,7 +59,7 @@ jobs:
51
59
  - name: Install repository
52
60
  run: pixi run -e ${{ matrix.environment }} postinstall
53
61
  - name: Run pytest
54
- run: pixi run -e ${{ matrix.environment }} test-coverage --color=yes
62
+ run: pixi run -e ${{ matrix.environment }} test-coverage --color=yes ${{ matrix.with_optionals && '-m with_optionals' || '-m "not with_optionals"'}} --cov=dataframely --cov-report=xml
55
63
  - name: Upload codecov
56
64
  uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
57
65
  with:
@@ -388,3 +388,6 @@ $RECYCLE.BIN/
388
388
 
389
389
  # interactive testing
390
390
  /*.ipynb
391
+
392
+ # pytest-benchmark
393
+ .benchmarks/
@@ -2,6 +2,7 @@
2
2
  edition = "2021"
3
3
  name = "dataframely"
4
4
  version = "0.1.0"
5
+ readme = "README.md"
5
6
 
6
7
  [lib]
7
8
  crate-type = ["cdylib"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframely
3
- Version: 1.8.2
3
+ Version: 1.10.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.10
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -9,6 +9,12 @@ Classifier: Programming Language :: Python :: 3.13
9
9
  Requires-Dist: numpy
10
10
  Requires-Dist: polars>=1.32
11
11
  Requires-Dist: typing-extensions ; python_full_version < '3.11'
12
+ Requires-Dist: deltalake ; extra == 'deltalake'
13
+ Requires-Dist: sqlalchemy ; extra == 'sqlalchemy'
14
+ Requires-Dist: pyarrow ; extra == 'pyarrow'
15
+ Provides-Extra: deltalake
16
+ Provides-Extra: sqlalchemy
17
+ Provides-Extra: pyarrow
12
18
  License-File: LICENSE
13
19
  Summary: A declarative, polars-native data frame validation library
14
20
  Author-email: Andreas Albert <andreas.albert@quantco.com>, Daniel Elsner <daniel.elsner@quantco.com>, Oliver Borchert <oliver.borchert@quantco.com>
@@ -66,8 +66,8 @@ class CollectionMember:
66
66
  # --------------------------------------- UTILS -------------------------------------- #
67
67
 
68
68
 
69
- def _common_primary_keys(columns: Iterable[type[Schema]]) -> set[str]:
70
- return set.intersection(*[set(schema.primary_keys()) for schema in columns])
69
+ def _common_primary_keys(schemas: Iterable[type[Schema]]) -> set[str]:
70
+ return set.intersection(*[set(schema.primary_keys()) for schema in schemas])
71
71
 
72
72
 
73
73
  # ------------------------------------------------------------------------------------ #
@@ -137,7 +137,7 @@ class CollectionMeta(ABCMeta):
137
137
  for member in result.members.values()
138
138
  ),
139
139
  *(
140
- set(member.schema._validation_rules())
140
+ set(member.schema._validation_rules(with_cast=True))
141
141
  for member in result.members.values()
142
142
  ),
143
143
  )
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  import polars as pl
14
14
 
15
- from ._rule import GroupRule, Rule
15
+ from ._rule import DtypeCastRule, GroupRule, Rule
16
16
  from .columns import Column
17
17
  from .exc import ImplementationError
18
18
 
@@ -24,11 +24,13 @@ else:
24
24
  _COLUMN_ATTR = "__dataframely_columns__"
25
25
  _RULE_ATTR = "__dataframely_rules__"
26
26
 
27
+ ORIGINAL_COLUMN_PREFIX = "__DATAFRAMELY_ORIGINAL__"
28
+
27
29
  # --------------------------------------- UTILS -------------------------------------- #
28
30
 
29
31
 
30
32
  def _build_rules(
31
- custom: dict[str, Rule], columns: dict[str, Column]
33
+ custom: dict[str, Rule], columns: dict[str, Column], *, with_cast: bool
32
34
  ) -> dict[str, Rule]:
33
35
  # NOTE: Copy here to prevent in-place modification of the custom rules
34
36
  rules: dict[str, Rule] = copy(custom)
@@ -46,6 +48,21 @@ def _build_rules(
46
48
  }
47
49
  rules.update(column_rules)
48
50
 
51
+ # Add casting rules if requested. Here, we can simply check whether the nullability
52
+ # property of a column changes due to lenient dtype casting. Whenever casting fails,
53
+ # the value is set to `null`, mismatching the previous nullability.
54
+ # NOTE: This check assumes that both the original and cast column are present in the
55
+ # data frame.
56
+ if with_cast:
57
+ casting_rules = {
58
+ f"{col_name}|dtype": DtypeCastRule(
59
+ pl.col(col_name).is_null()
60
+ == pl.col(f"{ORIGINAL_COLUMN_PREFIX}{col_name}").is_null()
61
+ )
62
+ for col_name in columns
63
+ }
64
+ rules.update(casting_rules)
65
+
49
66
  return rules
50
67
 
51
68
 
@@ -93,9 +110,7 @@ class SchemaMeta(ABCMeta):
93
110
  # we assume that users cast dtypes, i.e. additional rules for dtype casting
94
111
  # are also checked.
95
112
  all_column_names = set(result.columns)
96
- all_rule_names = set(_build_rules(result.rules, result.columns).keys()) | set(
97
- f"{col}|dtype" for col in result.columns
98
- )
113
+ all_rule_names = set(_build_rules(result.rules, result.columns, with_cast=True))
99
114
  common_names = all_column_names & all_rule_names
100
115
  if len(common_names) > 0:
101
116
  common_list = ", ".join(sorted(f"'{col}'" for col in common_names))
@@ -189,8 +204,10 @@ class BaseSchema(metaclass=SchemaMeta):
189
204
  return _primary_keys(cls.columns())
190
205
 
191
206
  @classmethod
192
- def _validation_rules(cls) -> dict[str, Rule]:
193
- return _build_rules(cls._schema_validation_rules(), cls.columns())
207
+ def _validation_rules(cls, *, with_cast: bool) -> dict[str, Rule]:
208
+ return _build_rules(
209
+ cls._schema_validation_rules(), cls.columns(), with_cast=with_cast
210
+ )
194
211
 
195
212
  @classmethod
196
213
  def _schema_validation_rules(cls) -> dict[str, Rule]:
@@ -13,11 +13,24 @@ class _DummyModule: # pragma: no cover
13
13
  raise ValueError(f"Module '{self.module}' is not installed.")
14
14
 
15
15
 
16
+ # ------------------------------------ DELTALAKE ------------------------------------- #
17
+
18
+ try:
19
+ import deltalake
20
+ from deltalake import DeltaTable
21
+ except ImportError: # pragma: no cover
22
+ deltalake = _DummyModule("deltalake") # type: ignore
23
+
24
+ class DeltaTable: # type: ignore # noqa: N801
25
+ pass
16
26
  # ------------------------------------ SQLALCHEMY ------------------------------------ #
17
27
 
18
28
  try:
19
29
  import sqlalchemy as sa
20
30
  import sqlalchemy.dialects.mssql as sa_mssql
31
+ from sqlalchemy import Dialect
32
+ from sqlalchemy.dialects.mssql.pyodbc import MSDialect_pyodbc
33
+ from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
21
34
  from sqlalchemy.sql.type_api import TypeEngine as sa_TypeEngine
22
35
  except ImportError: # pragma: no cover
23
36
  sa = _DummyModule("sqlalchemy") # type: ignore
@@ -26,7 +39,14 @@ except ImportError: # pragma: no cover
26
39
  class sa_TypeEngine: # type: ignore # noqa: N801
27
40
  pass
28
41
 
42
+ class MSDialect_pyodbc: # type: ignore # noqa: N801
43
+ pass
44
+
45
+ class PGDialect_psycopg2: # type: ignore # noqa: N801
46
+ pass
29
47
 
48
+ class Dialect: # type: ignore # noqa: N801
49
+ pass
30
50
  # -------------------------------------- PYARROW ------------------------------------- #
31
51
 
32
52
  try:
@@ -36,4 +56,12 @@ except ImportError: # pragma: no cover
36
56
 
37
57
  # ------------------------------------------------------------------------------------ #
38
58
 
39
- __all__ = ["sa", "sa_mssql", "sa_TypeEngine", "pa"]
59
+ __all__ = [
60
+ "deltalake",
61
+ "sa",
62
+ "sa_mssql",
63
+ "sa_TypeEngine",
64
+ "pa",
65
+ "MSDialect_pyodbc",
66
+ "PGDialect_psycopg2",
67
+ ]
@@ -2,7 +2,6 @@
2
2
  # SPDX-License-Identifier: BSD-3-Clause
3
3
 
4
4
  import datetime as dt
5
- from collections.abc import Iterable
6
5
  from typing import TypeVar
7
6
 
8
7
  import polars as pl
@@ -14,27 +13,6 @@ FrameType = TypeVar("FrameType", pl.DataFrame, pl.LazyFrame)
14
13
  EPOCH_DATETIME = dt.datetime(1970, 1, 1)
15
14
  SECONDS_PER_DAY = 86400
16
15
 
17
- # --------------------------------------- JOINS -------------------------------------- #
18
-
19
-
20
- def join_all_inner(dfs: Iterable[FrameType], on: str | list[str]) -> FrameType:
21
- it = iter(dfs)
22
- result = next(it)
23
- while (df := next(it, None)) is not None:
24
- result = result.join(df, on=on)
25
- return result
26
-
27
-
28
- def join_all_outer(dfs: Iterable[FrameType], on: str | list[str]) -> FrameType:
29
- it = iter(dfs)
30
- result = next(it)
31
- while (df := next(it, None)) is not None:
32
- result = result.join(df, on=on, how="full", coalesce=True)
33
- return result
34
-
35
-
36
- # ------------------------------------- DATETIMES ------------------------------------ #
37
-
38
16
 
39
17
  def date_matches_resolution(t: dt.date, resolution: str) -> bool:
40
18
  return pl.Series([t], dtype=pl.Date).dt.truncate(resolution).item() == t
@@ -59,6 +59,14 @@ class Rule:
59
59
  return str(self.expr)
60
60
 
61
61
 
62
+ class DtypeCastRule(Rule):
63
+ """Rule that evaluates whether casting a column to another dtype is successful.
64
+
65
+ The only purpose of this rule is to provide a runtime type to distinguish it from
66
+ other rules.
67
+ """
68
+
69
+
62
70
  class GroupRule(Rule):
63
71
  """Rule that is evaluated on a group of columns."""
64
72
 
@@ -147,7 +155,8 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
147
155
  while ``False`` indicates an issue.
148
156
  """
149
157
  # Rules must be distinguished into two types of rules:
150
- # 1. Simple rules can simply be selected on the data frame
158
+ # 1. Simple rules can simply be selected on the data frame (this includes rules
159
+ # that check whether dtype casts succeeded)
151
160
  # 2. "Group" rules require a `group_by` and a subsequent join
152
161
  simple_exprs = {
153
162
  name: rule.expr
@@ -160,7 +169,7 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
160
169
 
161
170
  # Before we can select all of the simple expressions, we need to turn the
162
171
  # group rules into something to use in a `select` statement as well.
163
- return (
172
+ result = (
164
173
  # NOTE: A value of `null` always validates successfully as nullability should
165
174
  # already be checked via dedicated rules.
166
175
  _with_group_rules(lf, group_rules).with_columns(
@@ -168,6 +177,27 @@ def with_evaluation_rules(lf: pl.LazyFrame, rules: dict[str, Rule]) -> pl.LazyFr
168
177
  )
169
178
  )
170
179
 
180
+ # If there is at least one rule that checks for successful dtype casting, we need
181
+ # to take an extra step: rules other than the "dtype rules" might not be reliable
182
+ # if casting failed, i.e. if any of the "dtype rules" evaluated to `False`. For
183
+ # this reason, we set all other rule evaluations to `null` in the case of dtype
184
+ # casting failure.
185
+ dtype_rule_names = [
186
+ name for name, rule in rules.items() if isinstance(rule, DtypeCastRule)
187
+ ]
188
+ if len(dtype_rule_names) > 0:
189
+ non_dtype_rule_names = [
190
+ name for name, rule in rules.items() if not isinstance(rule, DtypeCastRule)
191
+ ]
192
+ all_dtype_casts_valid = pl.all_horizontal(dtype_rule_names)
193
+ return result.with_columns(
194
+ pl.when(all_dtype_casts_valid)
195
+ .then(pl.col(non_dtype_rule_names))
196
+ .otherwise(pl.lit(None, dtype=pl.Boolean))
197
+ )
198
+
199
+ return result
200
+
171
201
 
172
202
  def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyFrame:
173
203
  # First, we partition the rules by group columns. This will minimize the number
@@ -184,16 +214,12 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
184
214
  # We group by the group columns and apply all expressions
185
215
  group_evaluations[group_columns] = lf.group_by(group_columns).agg(**group_rules)
186
216
 
187
- # Eventually, we apply the rule evaluations onto the input data frame. For this,
188
- # we're using left-joins. This has two effects:
189
- # - We're essentially "broadcasting" the results within each group across rows
190
- # in the same group.
191
- # - While an inner-join would be semantically more accurate, the left-join
192
- # preserves the order of the left data frame.
217
+ # Eventually, we apply the rule evaluations onto the input data frame. For this, we
218
+ # "broadcast" the results within each group across rows in the same group.
193
219
  result = lf
194
220
  for group_columns, frame in group_evaluations.items():
195
221
  result = result.join(
196
- frame, on=list(group_columns), how="left", nulls_equal=True
222
+ frame, on=list(group_columns), nulls_equal=True, maintain_order="left"
197
223
  )
198
224
  return result
199
225
 
@@ -10,8 +10,6 @@ from typing import Any, cast
10
10
 
11
11
  import polars as pl
12
12
 
13
- SCHEMA_METADATA_KEY = "dataframely_schema"
14
- COLLECTION_METADATA_KEY = "dataframely_collection"
15
13
  SERIALIZATION_FORMAT_VERSION = "1"
16
14
 
17
15
 
@@ -0,0 +1,8 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from ._base import StorageBackend
5
+
6
+ __all__ = [
7
+ "StorageBackend",
8
+ ]
@@ -0,0 +1,204 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ import polars as pl
9
+
10
+ SerializedSchema = str
11
+ SerializedCollection = str
12
+ SerializedRules = str
13
+
14
+
15
+ class StorageBackend(ABC):
16
+ """Base class for storage backends.
17
+
18
+ A storage backend encapsulates a way of serializing and deserializing dataframlely
19
+ data-/lazyframes and collections. This base class provides a unified interface for
20
+ all such use cases.
21
+
22
+ The interface is designed to operate on data provided as polars frames, and metadata
23
+ provided as serialized strings. This design is meant to limit the coupling between
24
+ the Schema/Collection classes and specifics of how data and metadata is stored.
25
+ """
26
+
27
+ # ----------------------------------- Schemas -------------------------------------
28
+ @abstractmethod
29
+ def sink_frame(
30
+ self, lf: pl.LazyFrame, serialized_schema: SerializedSchema, **kwargs: Any
31
+ ) -> None:
32
+ """Stream the contents of a dataframe, and its metadata to the storage backend.
33
+
34
+ Args:
35
+ lf: A frame containing the data to be stored.
36
+ serialized_schema: String-serialized schema information.
37
+ kwargs: Additional keyword arguments to pass to the underlying storage
38
+ implementation.
39
+ """
40
+
41
+ @abstractmethod
42
+ def write_frame(
43
+ self, df: pl.DataFrame, serialized_schema: SerializedSchema, **kwargs: Any
44
+ ) -> None:
45
+ """Write the contents of a dataframe, and its metadata to the storage backend.
46
+
47
+ Args:
48
+ df: A dataframe containing the data to be stored.
49
+ frame: String-serialized schema information.
50
+ kwargs: Additional keyword arguments to pass to the underlying storage
51
+ implementation.
52
+ """
53
+
54
+ @abstractmethod
55
+ def scan_frame(self, **kwargs: Any) -> tuple[pl.LazyFrame, SerializedSchema | None]:
56
+ """Lazily read frame data and metadata from the storage backend.
57
+
58
+ Args:
59
+ kwargs: Keyword arguments to pass to the underlying storage.
60
+ Refer to the individual implementation to see which keywords
61
+ are available.
62
+ Returns:
63
+ A tuple of the lazy frame data and metadata if available.
64
+ """
65
+
66
+ @abstractmethod
67
+ def read_frame(self, **kwargs: Any) -> tuple[pl.DataFrame, SerializedSchema | None]:
68
+ """Eagerly read frame data and metadata from the storage backend.
69
+
70
+ Args:
71
+ kwargs: Keyword arguments to pass to the underlying storage.
72
+ Refer to the individual implementation to see which keywords
73
+ are available.
74
+ Returns:
75
+ A tuple of the lazy frame data and metadata if available.
76
+ """
77
+
78
+ # ------------------------------ Collections ---------------------------------------
79
+ @abstractmethod
80
+ def sink_collection(
81
+ self,
82
+ dfs: dict[str, pl.LazyFrame],
83
+ serialized_collection: SerializedCollection,
84
+ serialized_schemas: dict[str, str],
85
+ **kwargs: Any,
86
+ ) -> None:
87
+ """Stream the members of this collection into the storage backend.
88
+
89
+ Args:
90
+ dfs: Dictionary containing the data to be stored.
91
+ serialized_collection: String-serialized information about the origin Collection.
92
+ serialized_schemas: String-serialized information about the individual Schemas
93
+ for each of the member frames. This information is also logically included
94
+ in the collection metadata, but it is passed separately here to ensure that
95
+ each member can also be read back as an individual frame.
96
+ """
97
+
98
+ @abstractmethod
99
+ def write_collection(
100
+ self,
101
+ dfs: dict[str, pl.LazyFrame],
102
+ serialized_collection: SerializedCollection,
103
+ serialized_schemas: dict[str, str],
104
+ **kwargs: Any,
105
+ ) -> None:
106
+ """Write the members of this collection into the storage backend.
107
+
108
+ Args:
109
+ dfs: Dictionary containing the data to be stored.
110
+ serialized_collection: String-serialized information about the origin Collection.
111
+ serialized_schemas: String-serialized information about the individual Schemas
112
+ for each of the member frames. This information is also logically included
113
+ in the collection metadata, but it is passed separately here to ensure that
114
+ each member can also be read back as an individual frame.
115
+ """
116
+
117
+ @abstractmethod
118
+ def scan_collection(
119
+ self, members: Iterable[str], **kwargs: Any
120
+ ) -> tuple[dict[str, pl.LazyFrame], list[SerializedCollection | None]]:
121
+ """Lazily read all collection members from the storage backend.
122
+
123
+ Args:
124
+ members: Collection member names to read.
125
+ kwargs: Additional keyword arguments to pass to the underlying storage.
126
+ Refer to the individual implementation to see which keywords are available.
127
+ Returns:
128
+ A tuple of the collection data and metadata if available.
129
+ Depending on the storage implementation, multiple copies of the metadata
130
+ may be available, which are returned as a list.
131
+ It is up to the caller to decide how to handle the presence/absence/consistency
132
+ of the returned values.
133
+ """
134
+
135
+ @abstractmethod
136
+ def read_collection(
137
+ self, members: Iterable[str], **kwargs: Any
138
+ ) -> tuple[dict[str, pl.LazyFrame], list[SerializedCollection | None]]:
139
+ """Lazily read all collection members from the storage backend.
140
+
141
+ Args:
142
+ members: Collection member names to read.
143
+ kwargs: Additional keyword arguments to pass to the underlying storage.
144
+ Refer to the individual implementation to see which keywords are available.
145
+ Returns:
146
+ A tuple of the collection data and metadata if available.
147
+ Depending on the storage implementation, multiple copies of the metadata
148
+ may be available, which are returned as a list.
149
+ It is up to the caller to decide how to handle the presence/absence/consistency
150
+ of the returned values.
151
+ """
152
+
153
+ # ------------------------------ Failure Info --------------------------------------
154
+ @abstractmethod
155
+ def sink_failure_info(
156
+ self,
157
+ lf: pl.LazyFrame,
158
+ serialized_rules: SerializedRules,
159
+ serialized_schema: SerializedSchema,
160
+ **kwargs: Any,
161
+ ) -> None:
162
+ """Stream the failure info to the storage backend.
163
+
164
+ Args:
165
+ lf: LazyFrame backing the failure info.
166
+ serialized_rules: JSON-serialized list of rule column names
167
+ used for validation.
168
+ serialized_schema: String-serialized schema information.
169
+ """
170
+
171
+ @abstractmethod
172
+ def write_failure_info(
173
+ self,
174
+ df: pl.DataFrame,
175
+ serialized_rules: SerializedRules,
176
+ serialized_schema: SerializedSchema,
177
+ **kwargs: Any,
178
+ ) -> None:
179
+ """Write the failure info to the storage backend.
180
+
181
+ Args:
182
+ df: DataFrame backing the failure info.
183
+ serialized_rules: JSON-serialized list of rule column names
184
+ used for validation.
185
+ serialized_schema: String-serialized schema information.
186
+ """
187
+
188
+ @abstractmethod
189
+ def scan_failure_info(
190
+ self, **kwargs: Any
191
+ ) -> tuple[pl.LazyFrame, SerializedRules, SerializedSchema]:
192
+ """Lazily read the failure info from the storage backend."""
193
+
194
+ def read_failure_info(
195
+ self, **kwargs: Any
196
+ ) -> tuple[pl.DataFrame, SerializedRules, SerializedSchema]:
197
+ """Read the failure info from the storage backend."""
198
+
199
+ lf, rule_metadata, schema_metadata = self.scan_failure_info(**kwargs)
200
+ return (
201
+ lf.collect(),
202
+ rule_metadata,
203
+ schema_metadata,
204
+ )
@@ -0,0 +1,10 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+
5
+ def assert_failure_info_metadata(metadata: str | None) -> str:
6
+ if metadata:
7
+ return metadata
8
+ raise ValueError(
9
+ "The required FailureInfo metadata was not found in the storage backend."
10
+ )
@@ -0,0 +1,6 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ SCHEMA_METADATA_KEY = "dataframely_schema"
5
+ COLLECTION_METADATA_KEY = "dataframely_collection"
6
+ RULE_METADATA_KEY = "dataframely_rule_columns"