dataframely 1.7.6__tar.gz → 1.8.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.7.6 → dataframely-1.8.0}/.github/workflows/build.yml +2 -2
  2. {dataframely-1.7.6 → dataframely-1.8.0}/.github/workflows/chore.yml +2 -2
  3. {dataframely-1.7.6 → dataframely-1.8.0}/.github/workflows/ci.yml +2 -2
  4. {dataframely-1.7.6 → dataframely-1.8.0}/.github/workflows/nightly.yml +1 -1
  5. {dataframely-1.7.6 → dataframely-1.8.0}/.github/workflows/scorecard.yml +1 -1
  6. {dataframely-1.7.6 → dataframely-1.8.0}/PKG-INFO +2 -2
  7. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/__init__.py +8 -1
  8. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_serialization.py +10 -3
  9. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/collection.py +123 -49
  10. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/__init__.py +2 -0
  11. dataframely-1.8.0/dataframely/columns/categorical.py +77 -0
  12. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/enum.py +7 -1
  13. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/random.py +1 -0
  14. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/schema.py +7 -3
  15. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/const.py +1 -0
  16. {dataframely-1.7.6 → dataframely-1.8.0}/pixi.lock +7308 -11938
  17. {dataframely-1.7.6 → dataframely-1.8.0}/pixi.toml +1 -1
  18. {dataframely-1.7.6 → dataframely-1.8.0}/pyproject.toml +4 -2
  19. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_read_write_parquet.py +114 -1
  20. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_sample.py +27 -49
  21. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_default_dtypes.py +1 -0
  22. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_pyarrow.py +15 -2
  23. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_sql_schema.py +1 -0
  24. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_read_write_parquet.py +16 -0
  25. dataframely-1.8.0/tests/test_serialization.py +22 -0
  26. {dataframely-1.7.6 → dataframely-1.8.0}/.copier-answers.yml +0 -0
  27. {dataframely-1.7.6 → dataframely-1.8.0}/.envrc +0 -0
  28. {dataframely-1.7.6 → dataframely-1.8.0}/.gitattributes +0 -0
  29. {dataframely-1.7.6 → dataframely-1.8.0}/.github/CODEOWNERS +0 -0
  30. {dataframely-1.7.6 → dataframely-1.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  31. {dataframely-1.7.6 → dataframely-1.8.0}/.github/dependabot.yml +0 -0
  32. {dataframely-1.7.6 → dataframely-1.8.0}/.github/release-drafter.yml +0 -0
  33. {dataframely-1.7.6 → dataframely-1.8.0}/.gitignore +0 -0
  34. {dataframely-1.7.6 → dataframely-1.8.0}/.pre-commit-config.yaml +0 -0
  35. {dataframely-1.7.6 → dataframely-1.8.0}/.prettierignore +0 -0
  36. {dataframely-1.7.6 → dataframely-1.8.0}/.prettierrc +0 -0
  37. {dataframely-1.7.6 → dataframely-1.8.0}/.readthedocs.yml +0 -0
  38. {dataframely-1.7.6 → dataframely-1.8.0}/Cargo.lock +0 -0
  39. {dataframely-1.7.6 → dataframely-1.8.0}/Cargo.toml +0 -0
  40. {dataframely-1.7.6 → dataframely-1.8.0}/LICENSE +0 -0
  41. {dataframely-1.7.6 → dataframely-1.8.0}/README.md +0 -0
  42. {dataframely-1.7.6 → dataframely-1.8.0}/SECURITY.md +0 -0
  43. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_base_collection.py +0 -0
  44. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_base_schema.py +0 -0
  45. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_compat.py +0 -0
  46. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_deprecation.py +0 -0
  47. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_extre.pyi +0 -0
  48. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_filter.py +0 -0
  49. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_polars.py +0 -0
  50. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_rule.py +0 -0
  51. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_typing.py +0 -0
  52. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/_validation.py +0 -0
  53. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/_base.py +0 -0
  54. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/_mixins.py +0 -0
  55. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/_registry.py +0 -0
  56. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/_utils.py +0 -0
  57. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/any.py +0 -0
  58. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/array.py +0 -0
  59. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/bool.py +0 -0
  60. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/datetime.py +0 -0
  61. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/decimal.py +0 -0
  62. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/float.py +0 -0
  63. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/integer.py +0 -0
  64. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/list.py +0 -0
  65. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/object.py +0 -0
  66. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/string.py +0 -0
  67. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/columns/struct.py +0 -0
  68. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/config.py +0 -0
  69. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/exc.py +0 -0
  70. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/failure.py +0 -0
  71. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/functional.py +0 -0
  72. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/mypy.py +0 -0
  73. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/py.typed +0 -0
  74. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/__init__.py +0 -0
  75. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/factory.py +0 -0
  76. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/mask.py +0 -0
  77. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/rules.py +0 -0
  78. {dataframely-1.7.6 → dataframely-1.8.0}/dataframely/testing/typing.py +0 -0
  79. {dataframely-1.7.6 → dataframely-1.8.0}/docker-compose.yml +0 -0
  80. {dataframely-1.7.6 → dataframely-1.8.0}/docs/Makefile +0 -0
  81. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.collection.rst +0 -0
  82. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.any.rst +0 -0
  83. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.bool.rst +0 -0
  84. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.datetime.rst +0 -0
  85. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.decimal.rst +0 -0
  86. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.enum.rst +0 -0
  87. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.float.rst +0 -0
  88. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.integer.rst +0 -0
  89. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.list.rst +0 -0
  90. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.rst +0 -0
  91. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.string.rst +0 -0
  92. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.columns.struct.rst +0 -0
  93. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.config.rst +0 -0
  94. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.exc.rst +0 -0
  95. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.failure.rst +0 -0
  96. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.functional.rst +0 -0
  97. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.mypy.rst +0 -0
  98. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.random.rst +0 -0
  99. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.rst +0 -0
  100. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.schema.rst +0 -0
  101. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.const.rst +0 -0
  102. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.factory.rst +0 -0
  103. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.mask.rst +0 -0
  104. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.rst +0 -0
  105. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.rules.rst +0 -0
  106. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/dataframely.testing.typing.rst +0 -0
  107. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_api/modules.rst +0 -0
  108. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_static/custom.css +0 -0
  109. {dataframely-1.7.6 → dataframely-1.8.0}/docs/_static/favicon.ico +0 -0
  110. {dataframely-1.7.6 → dataframely-1.8.0}/docs/conf.py +0 -0
  111. {dataframely-1.7.6 → dataframely-1.8.0}/docs/index.rst +0 -0
  112. {dataframely-1.7.6 → dataframely-1.8.0}/docs/make.bat +0 -0
  113. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/development.rst +0 -0
  114. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/examples/real-world.ipynb +0 -0
  115. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/faq.rst +0 -0
  116. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/installation.rst +0 -0
  117. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/quickstart.rst +0 -0
  118. {dataframely-1.7.6 → dataframely-1.8.0}/docs/sites/versioning.rst +0 -0
  119. {dataframely-1.7.6 → dataframely-1.8.0}/src/errdefs.rs +0 -0
  120. {dataframely-1.7.6 → dataframely-1.8.0}/src/lib.rs +0 -0
  121. {dataframely-1.7.6 → dataframely-1.8.0}/src/regex_repr.rs +0 -0
  122. {dataframely-1.7.6 → dataframely-1.8.0}/tests/benches/conftest.py +0 -0
  123. {dataframely-1.7.6 → dataframely-1.8.0}/tests/benches/test_collection.py +0 -0
  124. {dataframely-1.7.6 → dataframely-1.8.0}/tests/benches/test_failure.py +0 -0
  125. {dataframely-1.7.6 → dataframely-1.8.0}/tests/benches/test_schema.py +0 -0
  126. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_base.py +0 -0
  127. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_cast.py +0 -0
  128. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_collection_future_annotations.py +0 -0
  129. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_create_empty.py +0 -0
  130. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_filter_one_to_n.py +0 -0
  131. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_filter_validate.py +0 -0
  132. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_ignore_in_filter.py +0 -0
  133. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_implementation.py +0 -0
  134. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_matches.py +0 -0
  135. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_optional_members.py +0 -0
  136. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_repr.py +0 -0
  137. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_serialization.py +0 -0
  138. {dataframely-1.7.6 → dataframely-1.8.0}/tests/collection/test_validate_input.py +0 -0
  139. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/__init__.py +0 -0
  140. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_any.py +0 -0
  141. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_array.py +0 -0
  142. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_datetime.py +0 -0
  143. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_decimal.py +0 -0
  144. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_enum.py +0 -0
  145. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_float.py +0 -0
  146. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_integer.py +0 -0
  147. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_list.py +0 -0
  148. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_object.py +0 -0
  149. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_string.py +0 -0
  150. {dataframely-1.7.6 → dataframely-1.8.0}/tests/column_types/test_struct.py +0 -0
  151. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/__init__.py +0 -0
  152. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_alias.py +0 -0
  153. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_check.py +0 -0
  154. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_matches.py +0 -0
  155. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_metadata.py +0 -0
  156. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_polars_schema.py +0 -0
  157. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_rules.py +0 -0
  158. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_sample.py +0 -0
  159. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_str.py +0 -0
  160. {dataframely-1.7.6 → dataframely-1.8.0}/tests/columns/test_utils.py +0 -0
  161. {dataframely-1.7.6 → dataframely-1.8.0}/tests/core_validation/__init__.py +0 -0
  162. {dataframely-1.7.6 → dataframely-1.8.0}/tests/core_validation/test_column_validation.py +0 -0
  163. {dataframely-1.7.6 → dataframely-1.8.0}/tests/core_validation/test_dtype_validation.py +0 -0
  164. {dataframely-1.7.6 → dataframely-1.8.0}/tests/core_validation/test_rule_evaluation.py +0 -0
  165. {dataframely-1.7.6 → dataframely-1.8.0}/tests/functional/test_concat.py +0 -0
  166. {dataframely-1.7.6 → dataframely-1.8.0}/tests/functional/test_relationships.py +0 -0
  167. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_base.py +0 -0
  168. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_cast.py +0 -0
  169. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_create_empty.py +0 -0
  170. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_create_empty_if_none.py +0 -0
  171. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_filter.py +0 -0
  172. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_inheritance.py +0 -0
  173. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_matches.py +0 -0
  174. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_repr.py +0 -0
  175. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_rule_implementation.py +0 -0
  176. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_sample.py +0 -0
  177. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_serialization.py +0 -0
  178. {dataframely-1.7.6 → dataframely-1.8.0}/tests/schema/test_validate.py +0 -0
  179. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_compat.py +0 -0
  180. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_config.py +0 -0
  181. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_deprecation.py +0 -0
  182. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_exc.py +0 -0
  183. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_extre.py +0 -0
  184. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_failure_info.py +0 -0
  185. {dataframely-1.7.6 → dataframely-1.8.0}/tests/test_random.py +0 -0
  186. {dataframely-1.7.6 → dataframely-1.8.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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
20
+ uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
21
21
  with:
22
22
  environments: build
23
23
  - name: Set version
@@ -52,7 +52,7 @@ jobs:
52
52
  with:
53
53
  fetch-depth: 0
54
54
  - name: Set up pixi
55
- uses: prefix-dev/setup-pixi@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
55
+ uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
56
56
  with:
57
57
  environments: build
58
58
  - name: Set version
@@ -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@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
31
+ uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
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@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
48
+ uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
27
+ uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
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@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
48
+ uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
49
49
  with:
50
50
  environments: ${{ matrix.environment }}
51
51
  - name: Install repository
@@ -25,7 +25,7 @@ jobs:
25
25
  - name: Checkout branch
26
26
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27
27
  - name: Set up pixi
28
- uses: prefix-dev/setup-pixi@14c8aabd75893f83f4ab30c03e7cf853c8208961 # v0.8.10
28
+ uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
29
29
  with:
30
30
  environments: nightly
31
31
  - name: Install polars nightly
@@ -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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
77
+ uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
78
78
  with:
79
79
  sarif_file: results.sarif
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframely
3
- Version: 1.7.6
3
+ Version: 1.8.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.10
6
6
  Classifier: Programming Language :: Python :: 3.11
7
7
  Classifier: Programming Language :: Python :: 3.12
8
8
  Classifier: Programming Language :: Python :: 3.13
9
9
  Requires-Dist: numpy
10
- Requires-Dist: polars>=1.30
10
+ Requires-Dist: polars>=1.32
11
11
  Requires-Dist: typing-extensions ; python_full_version < '3.11'
12
12
  License-File: LICENSE
13
13
  Summary: A declarative, polars-native data frame validation library
@@ -15,11 +15,16 @@ from ._base_collection import CollectionMember
15
15
  from ._filter import filter
16
16
  from ._rule import rule
17
17
  from ._typing import DataFrame, LazyFrame, Validation
18
- from .collection import Collection, deserialize_collection
18
+ from .collection import (
19
+ Collection,
20
+ deserialize_collection,
21
+ read_parquet_metadata_collection,
22
+ )
19
23
  from .columns import (
20
24
  Any,
21
25
  Array,
22
26
  Bool,
27
+ Categorical,
23
28
  Column,
24
29
  Date,
25
30
  Datetime,
@@ -70,8 +75,10 @@ __all__ = [
70
75
  "Schema",
71
76
  "deserialize_schema",
72
77
  "read_parquet_metadata_schema",
78
+ "read_parquet_metadata_collection",
73
79
  "Any",
74
80
  "Bool",
81
+ "Categorical",
75
82
  "Column",
76
83
  "Date",
77
84
  "Datetime",
@@ -11,6 +11,7 @@ from typing import Any, cast
11
11
  import polars as pl
12
12
 
13
13
  SCHEMA_METADATA_KEY = "dataframely_schema"
14
+ COLLECTION_METADATA_KEY = "dataframely_collection"
14
15
  SERIALIZATION_FORMAT_VERSION = "1"
15
16
 
16
17
 
@@ -45,7 +46,7 @@ class SchemaJSONEncoder(JSONEncoder):
45
46
  case pl.Expr():
46
47
  return {
47
48
  "__type__": "expression",
48
- "value": obj.meta.serialize(format="json"),
49
+ "value": base64.b64encode(obj.meta.serialize()).decode("utf-8"),
49
50
  }
50
51
  case pl.LazyFrame():
51
52
  return {
@@ -86,8 +87,14 @@ class SchemaJSONDecoder(JSONDecoder):
86
87
  case "tuple":
87
88
  return tuple(dct["value"])
88
89
  case "expression":
89
- data = BytesIO(cast(str, dct["value"]).encode("utf-8"))
90
- return pl.Expr.deserialize(data, format="json")
90
+ value_str = cast(str, dct["value"]).encode("utf-8")
91
+ if value_str.startswith(b"{"):
92
+ # NOTE: This branch is for backwards-compatibility only
93
+ data = BytesIO(value_str)
94
+ return pl.Expr.deserialize(data, format="json")
95
+ else:
96
+ data = BytesIO(base64.b64decode(value_str))
97
+ return pl.Expr.deserialize(data)
91
98
  case "lazyframe":
92
99
  data = BytesIO(
93
100
  base64.b64decode(cast(str, dct["value"]).encode("utf-8"))
@@ -1,14 +1,16 @@
1
1
  # Copyright (c) QuantCo 2025-2025
2
2
  # SPDX-License-Identifier: BSD-3-Clause
3
+ from __future__ import annotations
3
4
 
4
5
  import json
5
6
  import sys
6
7
  import warnings
7
8
  from abc import ABC
8
- from collections.abc import Mapping, Sequence
9
+ from collections.abc import Iterable, Mapping, Sequence
9
10
  from dataclasses import asdict
11
+ from json import JSONDecodeError
10
12
  from pathlib import Path
11
- from typing import Annotated, Any, cast
13
+ from typing import IO, Annotated, Any, cast
12
14
 
13
15
  import polars as pl
14
16
  import polars.exceptions as plexc
@@ -17,6 +19,7 @@ from ._base_collection import BaseCollection, CollectionMember
17
19
  from ._filter import Filter
18
20
  from ._polars import FrameType, join_all_inner, join_all_outer
19
21
  from ._serialization import (
22
+ COLLECTION_METADATA_KEY,
20
23
  SERIALIZATION_FORMAT_VERSION,
21
24
  SchemaJSONDecoder,
22
25
  SchemaJSONEncoder,
@@ -147,9 +150,16 @@ class Collection(BaseCollection, ABC):
147
150
  A collection where all members (including optional ones) have been sampled
148
151
  according to the input parameters.
149
152
 
153
+ Attention:
154
+ In case the collection has members with a common primary key, the
155
+ `_preprocess_sample` method must return distinct primary key values for each
156
+ sample. The default implementation does this on a best-effort basis but may
157
+ cause primary key violations. Hence, it is recommended to override this
158
+ method and ensure that all primary key columns are set.
159
+
150
160
  Raises:
151
161
  ValueError: If the :meth:`_preprocess_sample` method does not return all
152
- common primary keys for all samples.
162
+ common primary key columns for all samples.
153
163
  ValidationError: If the sampled members violate any of the collection
154
164
  filters. If the collection does not have filters, this error is never
155
165
  raised. To prevent validation errors, overwrite the
@@ -247,7 +257,7 @@ class Collection(BaseCollection, ABC):
247
257
  return cls.validate(members)
248
258
 
249
259
  @classmethod
250
- def matches(cls, other: type["Collection"]) -> bool:
260
+ def matches(cls, other: type[Collection]) -> bool:
251
261
  """Check whether this collection semantically matches another.
252
262
 
253
263
  Args:
@@ -317,9 +327,7 @@ class Collection(BaseCollection, ABC):
317
327
  of fuzzy sampling rounds when sampling individual members.
318
328
 
319
329
  Args:
320
- sample: The sample to preprocess. By default, this is a simple dictionary.
321
- Subclasses may decide, however, to introduce a :class:`TypedDict` to
322
- ease the creation of samples.
330
+ sample: The sample to preprocess.
323
331
  index: The index of the sample in the list of samples. Typically, this value
324
332
  can be used to assign unique primary keys for samples.
325
333
  generator: The generator to use when performing random sampling within the
@@ -331,10 +339,18 @@ class Collection(BaseCollection, ABC):
331
339
  primary keys.
332
340
  """
333
341
  if len(cls.members()) > 1 and len(cls.common_primary_keys()) > 0:
334
- raise ValueError(
335
- "`_preprocess_sample` must be overwritten for collections with more "
336
- "than 1 member sharing a common primary key."
337
- )
342
+ # If we have multiple members with a common primary key, we need to ensure
343
+ # that the samples have a value set for all common primary key columns.
344
+ # NOTE: This is experimental as we commit to a primary key that cannot be
345
+ # changed at a later point (e.g. due to primary key violations).
346
+ first_member_columns = next(iter(cls.member_schemas().values())).columns()
347
+ for primary_key in cls.common_primary_keys():
348
+ if primary_key in sample:
349
+ continue
350
+
351
+ value = first_member_columns[primary_key].sample(generator).item()
352
+ sample[primary_key] = value
353
+
338
354
  return sample
339
355
 
340
356
  # ---------------------------------- VALIDATION ---------------------------------- #
@@ -638,9 +654,6 @@ class Collection(BaseCollection, ABC):
638
654
  Each parquet file is named ``<member>.parquet``. No file is written for optional
639
655
  members which are not provided in the current collection.
640
656
 
641
- In addition, one JSON file named ``schema.json`` is written, serializing the
642
- collection's definition for fast reads.
643
-
644
657
  Args:
645
658
  directory: The directory where the Parquet files should be written to. If
646
659
  the directory does not exist, it is created automatically, including all
@@ -661,9 +674,6 @@ class Collection(BaseCollection, ABC):
661
674
  Each parquet file is named ``<member>.parquet``. No file is written for optional
662
675
  members which are not provided in the current collection.
663
676
 
664
- In addition, one JSON file named ``schema.json`` is written, serializing the
665
- collection's definition for fast reads.
666
-
667
677
  Args:
668
678
  directory: The directory where the Parquet files should be written to. If
669
679
  the directory does not exist, it is created automatically, including all
@@ -680,8 +690,11 @@ class Collection(BaseCollection, ABC):
680
690
  def _to_parquet(self, directory: str | Path, *, sink: bool, **kwargs: Any) -> None:
681
691
  path = Path(directory) if isinstance(directory, str) else directory
682
692
  path.mkdir(parents=True, exist_ok=True)
683
- with open(path / "schema.json", "w") as f:
684
- f.write(self.serialize())
693
+
694
+ # The collection schema is serialized as part of the member parquet metadata
695
+ kwargs["metadata"] = kwargs.get("metadata", {}) | {
696
+ COLLECTION_METADATA_KEY: self.serialize()
697
+ }
685
698
 
686
699
  member_schemas = self.member_schemas()
687
700
  for key, lf in self.to_dict().items():
@@ -719,16 +732,16 @@ class Collection(BaseCollection, ABC):
719
732
  Parquet files may have been written with Hive partitioning.
720
733
  validation: The strategy for running validation when reading the data:
721
734
 
722
- - ``"allow"`: The method tries to read the ``schema.json`` file in the
723
- directory. If the stored collection schema matches this collection
735
+ - ``"allow"`: The method tries to read the schema data from the parquet
736
+ files. If the stored collection schema matches this collection
724
737
  schema, the collection is read without validation. If the stored
725
- schema mismatches this schema or no ``schema.json`` can be found in
726
- the directory, this method automatically runs :meth:`validate` with
727
- ``cast=True``.
738
+ schema mismatches this schema no metadata can be found in
739
+ the parquets, or the files have conflicting metadata,
740
+ this method automatically runs :meth:`validate` with ``cast=True``.
728
741
  - ``"warn"`: The method behaves similarly to ``"allow"``. However,
729
742
  it prints a warning if validation is necessary.
730
743
  - ``"forbid"``: The method never runs validation automatically and only
731
- returns if the ``schema.json`` stores a collection schema that matches
744
+ returns if the metadata stores a collection schema that matches
732
745
  this collection.
733
746
  - ``"skip"``: The method never runs validation and simply reads the
734
747
  data, entrusting the user that the schema is valid. _Use this option
@@ -747,13 +760,20 @@ class Collection(BaseCollection, ABC):
747
760
  all required members.
748
761
  ValidationError: If the collection cannot be validate.
749
762
 
763
+ Note:
764
+ This method is backward compatible with older versions of dataframely
765
+ in which the schema metadata was saved to `schema.json` files instead of
766
+ being encoded into the parquet files.
767
+
750
768
  Attention:
751
769
  Be aware that this method suffers from the same limitations as
752
770
  :meth:`serialize`.
753
771
  """
754
772
  path = Path(directory)
755
- data = cls._from_parquet(path, scan=True, **kwargs)
756
- if not cls._requires_validation_for_reading_parquets(path, validation):
773
+ data, collection_type = cls._from_parquet(path, scan=False, **kwargs)
774
+ if not cls._requires_validation_for_reading_parquets(
775
+ path, collection_type, validation
776
+ ):
757
777
  cls._validate_input_keys(data)
758
778
  return cls._init(data)
759
779
  return cls.validate(data, cast=True)
@@ -776,16 +796,16 @@ class Collection(BaseCollection, ABC):
776
796
  Parquet files may have been written with Hive partitioning.
777
797
  validation: The strategy for running validation when reading the data:
778
798
 
779
- - ``"allow"`: The method tries to read the ``schema.json`` file in the
780
- directory. If the stored collection schema matches this collection
799
+ - ``"allow"`: The method tries to read the schema data from the parquet
800
+ files. If the stored collection schema matches this collection
781
801
  schema, the collection is read without validation. If the stored
782
- schema mismatches this schema or no ``schema.json`` can be found in
783
- the directory, this method automatically runs :meth:`validate` with
784
- ``cast=True``.
802
+ schema mismatches this schema no metadata can be found in
803
+ the parquets, or the files have conflicting metadata,
804
+ this method automatically runs :meth:`validate` with ``cast=True``.
785
805
  - ``"warn"`: The method behaves similarly to ``"allow"``. However,
786
806
  it prints a warning if validation is necessary.
787
807
  - ``"forbid"``: The method never runs validation automatically and only
788
- returns if the ``schema.json`` stores a collection schema that matches
808
+ returns if the metadata stores a collection schema that matches
789
809
  this collection.
790
810
  - ``"skip"``: The method never runs validation and simply reads the
791
811
  data, entrusting the user that the schema is valid. _Use this option
@@ -808,13 +828,19 @@ class Collection(BaseCollection, ABC):
808
828
  parquet file into memory if ``"validation"`` is ``"warn"`` or ``"allow"``
809
829
  and validation is required.
810
830
 
831
+ Note: This method is backward compatible with older versions of dataframely
832
+ in which the schema metadata was saved to `schema.json` files instead of
833
+ being encoded into the parquet files.
834
+
811
835
  Attention:
812
836
  Be aware that this method suffers from the same limitations as
813
837
  :meth:`serialize`.
814
838
  """
815
839
  path = Path(directory)
816
- data = cls._from_parquet(path, scan=True, **kwargs)
817
- if not cls._requires_validation_for_reading_parquets(path, validation):
840
+ data, collection_type = cls._from_parquet(path, scan=True, **kwargs)
841
+ if not cls._requires_validation_for_reading_parquets(
842
+ path, collection_type, validation
843
+ ):
818
844
  cls._validate_input_keys(data)
819
845
  return cls._init(data)
820
846
  return cls.validate(data, cast=True)
@@ -822,8 +848,9 @@ class Collection(BaseCollection, ABC):
822
848
  @classmethod
823
849
  def _from_parquet(
824
850
  cls, path: Path, scan: bool, **kwargs: Any
825
- ) -> dict[str, pl.LazyFrame]:
851
+ ) -> tuple[dict[str, pl.LazyFrame], type[Collection] | None]:
826
852
  data = {}
853
+ collection_types = set()
827
854
  for key in cls.members():
828
855
  if (source_path := cls._member_source_path(path, key)) is not None:
829
856
  data[key] = (
@@ -831,7 +858,22 @@ class Collection(BaseCollection, ABC):
831
858
  if scan
832
859
  else pl.read_parquet(source_path, **kwargs).lazy()
833
860
  )
834
- return data
861
+ if source_path.is_file():
862
+ collection_types.add(read_parquet_metadata_collection(source_path))
863
+ else:
864
+ for file in source_path.glob("**/*.parquet"):
865
+ collection_types.add(read_parquet_metadata_collection(file))
866
+ collection_type = _reconcile_collection_types(collection_types)
867
+
868
+ # Backward compatibility: If the parquets do not have schema information,
869
+ # fall back to looking for schema.json
870
+ if (collection_type is None) and (schema_file := path / "schema.json").exists():
871
+ try:
872
+ collection_type = deserialize_collection(schema_file.read_text())
873
+ except (JSONDecodeError, plexc.ComputeError):
874
+ pass
875
+
876
+ return data, collection_type
835
877
 
836
878
  @classmethod
837
879
  def _member_source_path(cls, base_path: Path, name: str) -> Path | None:
@@ -847,27 +889,20 @@ class Collection(BaseCollection, ABC):
847
889
  def _requires_validation_for_reading_parquets(
848
890
  cls,
849
891
  directory: Path,
892
+ collection_type: type[Collection] | None,
850
893
  validation: Validation,
851
894
  ) -> bool:
852
895
  if validation == "skip":
853
896
  return False
854
897
 
855
- # First, we check whether the path provides the serialization of the collection.
856
- # If it does, we check whether it matches this collection. If it does, we assume
857
- # that the data adheres to the collection and we do not need to run validation.
858
- if (json_serialization := directory / "schema.json").exists():
859
- metadata = json_serialization.read_text()
860
- serialized_collection = deserialize_collection(metadata)
861
- if cls.matches(serialized_collection):
862
- return False
863
- else:
864
- serialized_collection = None
898
+ if collection_type is not None and cls.matches(collection_type):
899
+ return False
865
900
 
866
- # Otherwise, we definitely need to run validation. However, we emit different
901
+ # Now we definitely need to run validation. However, we emit different
867
902
  # information to the user depending on the value of `validate`.
868
903
  msg = (
869
904
  "current collection schema does not match stored collection schema"
870
- if serialized_collection is not None
905
+ if collection_type is not None
871
906
  else "no collection schema to check validity can be read from the source"
872
907
  )
873
908
  if validation == "forbid":
@@ -903,6 +938,27 @@ class Collection(BaseCollection, ABC):
903
938
  )
904
939
 
905
940
 
941
+ def read_parquet_metadata_collection(
942
+ source: str | Path | IO[bytes] | bytes,
943
+ ) -> type[Collection] | None:
944
+ """Read a dataframely Collection type from the metadata of a parquet file.
945
+
946
+ Args:
947
+ source: Path to a parquet file or a file-like object that contains the metadata.
948
+
949
+ Returns:
950
+ The collection that was serialized to the metadata. ``None`` if no collection
951
+ metadata is found or the deserialization fails.
952
+ """
953
+ metadata = pl.read_parquet_metadata(source)
954
+ if (schema_metadata := metadata.get(COLLECTION_METADATA_KEY)) is not None:
955
+ try:
956
+ return deserialize_collection(schema_metadata)
957
+ except (JSONDecodeError, plexc.ComputeError):
958
+ return None
959
+ return None
960
+
961
+
906
962
  def deserialize_collection(data: str) -> type[Collection]:
907
963
  """Deserialize a collection from a JSON string.
908
964
 
@@ -968,3 +1024,21 @@ def _extract_keys_if_exist(
968
1024
  data: Mapping[str, Any], keys: Sequence[str]
969
1025
  ) -> dict[str, Any]:
970
1026
  return {key: data[key] for key in keys if key in data}
1027
+
1028
+
1029
+ def _reconcile_collection_types(
1030
+ collection_types: Iterable[type[Collection] | None],
1031
+ ) -> type[Collection] | None:
1032
+ # When reading serialized collections, we may have collection type information from multiple sources
1033
+ # (E.g. one set of information for each parquet file).
1034
+ # This function determines which of them should finally be used
1035
+ if not (collection_types := list(collection_types)):
1036
+ return None
1037
+ if (first_type := collection_types[0]) is None:
1038
+ return None
1039
+ for t in collection_types[1:]:
1040
+ if t is None:
1041
+ return None
1042
+ if not first_type.matches(t):
1043
+ return None
1044
+ return first_type
@@ -6,6 +6,7 @@ from ._registry import column_from_dict
6
6
  from .any import Any
7
7
  from .array import Array
8
8
  from .bool import Bool
9
+ from .categorical import Categorical
9
10
  from .datetime import Date, Datetime, Duration, Time
10
11
  from .decimal import Decimal
11
12
  from .enum import Enum
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "Any",
23
24
  "Array",
24
25
  "Bool",
26
+ "Categorical",
25
27
  "Date",
26
28
  "Datetime",
27
29
  "Decimal",
@@ -0,0 +1,77 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from typing import Any
5
+
6
+ import polars as pl
7
+ from sqlalchemy import Dialect
8
+
9
+ from dataframely._compat import pa, sa, sa_TypeEngine
10
+ from dataframely.random import Generator
11
+
12
+ from ._base import Check, Column
13
+ from ._registry import register
14
+
15
+
16
+ @register
17
+ class Categorical(Column):
18
+ """A column of categorical (string) values."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ nullable: bool | None = None,
24
+ primary_key: bool = False,
25
+ check: Check | 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
+ Explicitly set `nullable=True` if you want your column to be nullable.
33
+ In a future release, `nullable=False` will be the default if `nullable`
34
+ is not specified.
35
+ primary_key: Whether this column is part of the primary key of the schema.
36
+ If ``True``, ``nullable`` is automatically set to ``False``.
37
+ check: A custom rule or multiple rules to run for this column. This can be:
38
+ - A single callable that returns a non-aggregated boolean expression.
39
+ The name of the rule is derived from the callable name, or defaults to
40
+ "check" for lambdas.
41
+ - A list of callables, where each callable returns a non-aggregated
42
+ boolean expression. The name of the rule is derived from the callable
43
+ name, or defaults to "check" for lambdas. Where multiple rules result
44
+ in the same name, the suffix __i is appended to the name.
45
+ - A dictionary mapping rule names to callables, where each callable
46
+ returns a non-aggregated boolean expression.
47
+ All rule names provided here are given the prefix "check_".
48
+ alias: An overwrite for this column's name which allows for using a column
49
+ name that is not a valid Python identifier. Especially note that setting
50
+ this option does _not_ allow to refer to the column with two different
51
+ names, the specified alias is the only valid name.
52
+ metadata: A dictionary of metadata to attach to the column.
53
+ """
54
+ super().__init__(
55
+ nullable=nullable,
56
+ primary_key=primary_key,
57
+ check=check,
58
+ alias=alias,
59
+ metadata=metadata,
60
+ )
61
+
62
+ @property
63
+ def dtype(self) -> pl.DataType:
64
+ return pl.Categorical()
65
+
66
+ def sqlalchemy_dtype(self, dialect: Dialect) -> sa_TypeEngine:
67
+ return sa.String()
68
+
69
+ @property
70
+ def pyarrow_dtype(self) -> pa.DataType:
71
+ return pa.dictionary(pa.uint32(), pa.large_string())
72
+
73
+ def _sample_unchecked(self, generator: Generator, n: int) -> pl.Series:
74
+ # We simply sample low-cardinality strings here
75
+ return generator.sample_string(
76
+ n, regex=r"[a-z]{1,2}", null_probability=self._null_probability
77
+ ).cast(self.dtype)
@@ -82,7 +82,13 @@ class Enum(Column):
82
82
 
83
83
  @property
84
84
  def pyarrow_dtype(self) -> pa.DataType:
85
- return pa.dictionary(pa.uint32(), pa.large_string())
85
+ if len(self.categories) <= 2**8 - 2:
86
+ dtype = pa.uint8()
87
+ elif len(self.categories) <= 2**16 - 2:
88
+ dtype = pa.uint16()
89
+ else:
90
+ dtype = pa.uint32()
91
+ return pa.dictionary(dtype, pa.large_string())
86
92
 
87
93
  def _sample_unchecked(self, generator: Generator, n: int) -> pl.Series:
88
94
  return generator.sample_choice(
@@ -40,6 +40,7 @@ class Generator:
40
40
  seed: The seed to use for initializing the random number generator used
41
41
  for all sampling methods.
42
42
  """
43
+ self.seed = seed
43
44
  self.numpy_generator = np.random.default_rng(seed)
44
45
 
45
46
  def sample_seed(self) -> int:
@@ -8,6 +8,7 @@ import sys
8
8
  import warnings
9
9
  from abc import ABC
10
10
  from collections.abc import Iterable, Mapping, Sequence
11
+ from json import JSONDecodeError
11
12
  from pathlib import Path
12
13
  from typing import IO, Any, Literal, overload
13
14
 
@@ -970,12 +971,15 @@ def read_parquet_metadata_schema(
970
971
  source: Path to a parquet file or a file-like object that contains the metadata.
971
972
 
972
973
  Returns:
973
- The schema that was serialized to the metadata or ``None`` if no schema metadata
974
- is found.
974
+ The schema that was serialized to the metadata. ``None`` if no schema metadata
975
+ is found or the deserialization fails.
975
976
  """
976
977
  metadata = pl.read_parquet_metadata(source)
977
978
  if (schema_metadata := metadata.get(SCHEMA_METADATA_KEY)) is not None:
978
- return deserialize_schema(schema_metadata)
979
+ try:
980
+ return deserialize_schema(schema_metadata)
981
+ except (JSONDecodeError, plexc.ComputeError):
982
+ return None
979
983
  return None
980
984
 
981
985
 
@@ -21,6 +21,7 @@ COLUMN_TYPES: list[type[dc.Column]] = [
21
21
  dc.UInt32,
22
22
  dc.UInt64,
23
23
  dc.String,
24
+ dc.Categorical,
24
25
  ]
25
26
  INTEGER_COLUMN_TYPES: list[type[dc.Column]] = [
26
27
  dc.Integer,