dataframely 1.5.0__tar.gz → 1.7.0__tar.gz

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