dataframely 1.1.0__tar.gz → 1.2.1__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 (161) hide show
  1. {dataframely-1.1.0 → dataframely-1.2.1}/PKG-INFO +1 -1
  2. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/__init__.py +2 -0
  3. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_base_collection.py +53 -54
  4. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_rule.py +3 -1
  5. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/collection.py +3 -3
  6. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/__init__.py +2 -0
  7. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/enum.py +1 -1
  8. dataframely-1.2.1/dataframely/columns/object.py +63 -0
  9. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/functional.py +7 -3
  10. {dataframely-1.1.0 → dataframely-1.2.1}/pyproject.toml +1 -1
  11. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_implementation.py +18 -0
  12. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_sample.py +24 -2
  13. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_enum.py +9 -0
  14. dataframely-1.2.1/tests/column_types/test_object.py +60 -0
  15. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_sql_schema.py +8 -0
  16. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_validate.py +9 -0
  17. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_typing.py +22 -2
  18. {dataframely-1.1.0 → dataframely-1.2.1}/.copier-answers.yml +0 -0
  19. {dataframely-1.1.0 → dataframely-1.2.1}/.envrc +0 -0
  20. {dataframely-1.1.0 → dataframely-1.2.1}/.gitattributes +0 -0
  21. {dataframely-1.1.0 → dataframely-1.2.1}/.github/CODEOWNERS +0 -0
  22. {dataframely-1.1.0 → dataframely-1.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {dataframely-1.1.0 → dataframely-1.2.1}/.github/dependabot.yml +0 -0
  24. {dataframely-1.1.0 → dataframely-1.2.1}/.github/release-drafter.yml +0 -0
  25. {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/build.yml +0 -0
  26. {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/chore.yml +0 -0
  27. {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/ci.yml +0 -0
  28. {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/update-lockfiles.yml +0 -0
  29. {dataframely-1.1.0 → dataframely-1.2.1}/.gitignore +0 -0
  30. {dataframely-1.1.0 → dataframely-1.2.1}/.pre-commit-config.yaml +0 -0
  31. {dataframely-1.1.0 → dataframely-1.2.1}/.prettierignore +0 -0
  32. {dataframely-1.1.0 → dataframely-1.2.1}/.prettierrc +0 -0
  33. {dataframely-1.1.0 → dataframely-1.2.1}/.readthedocs.yml +0 -0
  34. {dataframely-1.1.0 → dataframely-1.2.1}/Cargo.lock +0 -0
  35. {dataframely-1.1.0 → dataframely-1.2.1}/Cargo.toml +0 -0
  36. {dataframely-1.1.0 → dataframely-1.2.1}/LICENSE +0 -0
  37. {dataframely-1.1.0 → dataframely-1.2.1}/README.md +0 -0
  38. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_base_schema.py +0 -0
  39. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_compat.py +0 -0
  40. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_extre.pyi +0 -0
  41. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_filter.py +0 -0
  42. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_polars.py +0 -0
  43. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_typing.py +0 -0
  44. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_validation.py +0 -0
  45. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_base.py +0 -0
  46. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_mixins.py +0 -0
  47. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_utils.py +0 -0
  48. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/any.py +0 -0
  49. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/bool.py +0 -0
  50. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/datetime.py +0 -0
  51. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/decimal.py +0 -0
  52. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/float.py +0 -0
  53. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/integer.py +0 -0
  54. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/list.py +0 -0
  55. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/string.py +0 -0
  56. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/struct.py +0 -0
  57. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/config.py +0 -0
  58. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/exc.py +0 -0
  59. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/failure.py +0 -0
  60. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/mypy.py +0 -0
  61. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/py.typed +0 -0
  62. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/random.py +0 -0
  63. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/schema.py +0 -0
  64. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/__init__.py +0 -0
  65. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/const.py +0 -0
  66. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/factory.py +0 -0
  67. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/mask.py +0 -0
  68. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/rules.py +0 -0
  69. {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/typing.py +0 -0
  70. {dataframely-1.1.0 → dataframely-1.2.1}/docker-compose.yml +0 -0
  71. {dataframely-1.1.0 → dataframely-1.2.1}/docs/Makefile +0 -0
  72. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.collection.rst +0 -0
  73. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.any.rst +0 -0
  74. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.bool.rst +0 -0
  75. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.datetime.rst +0 -0
  76. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.decimal.rst +0 -0
  77. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.enum.rst +0 -0
  78. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.float.rst +0 -0
  79. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.integer.rst +0 -0
  80. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.list.rst +0 -0
  81. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.rst +0 -0
  82. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.string.rst +0 -0
  83. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.struct.rst +0 -0
  84. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.config.rst +0 -0
  85. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.exc.rst +0 -0
  86. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.failure.rst +0 -0
  87. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.functional.rst +0 -0
  88. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.mypy.rst +0 -0
  89. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.random.rst +0 -0
  90. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.rst +0 -0
  91. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.schema.rst +0 -0
  92. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.const.rst +0 -0
  93. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.factory.rst +0 -0
  94. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.mask.rst +0 -0
  95. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.rst +0 -0
  96. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.rules.rst +0 -0
  97. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.typing.rst +0 -0
  98. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/modules.rst +0 -0
  99. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_static/custom.css +0 -0
  100. {dataframely-1.1.0 → dataframely-1.2.1}/docs/_static/favicon.ico +0 -0
  101. {dataframely-1.1.0 → dataframely-1.2.1}/docs/conf.py +0 -0
  102. {dataframely-1.1.0 → dataframely-1.2.1}/docs/index.rst +0 -0
  103. {dataframely-1.1.0 → dataframely-1.2.1}/docs/make.bat +0 -0
  104. {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/development.rst +0 -0
  105. {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/examples/real-world.ipynb +0 -0
  106. {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/faq.rst +0 -0
  107. {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/installation.rst +0 -0
  108. {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/quickstart.rst +0 -0
  109. {dataframely-1.1.0 → dataframely-1.2.1}/pixi.lock +0 -0
  110. {dataframely-1.1.0 → dataframely-1.2.1}/pixi.toml +0 -0
  111. {dataframely-1.1.0 → dataframely-1.2.1}/src/errdefs.rs +0 -0
  112. {dataframely-1.1.0 → dataframely-1.2.1}/src/lib.rs +0 -0
  113. {dataframely-1.1.0 → dataframely-1.2.1}/src/regex_repr.rs +0 -0
  114. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_base.py +0 -0
  115. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_cast.py +0 -0
  116. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_create_empty.py +0 -0
  117. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_filter_one_to_n.py +0 -0
  118. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_filter_validate.py +0 -0
  119. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_ignore_in_filter.py +0 -0
  120. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_optional_members.py +0 -0
  121. {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_validate_input.py +0 -0
  122. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/__init__.py +0 -0
  123. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_any.py +0 -0
  124. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_datetime.py +0 -0
  125. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_decimal.py +0 -0
  126. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_float.py +0 -0
  127. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_integer.py +0 -0
  128. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_list.py +0 -0
  129. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_string.py +0 -0
  130. {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_struct.py +0 -0
  131. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/__init__.py +0 -0
  132. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_alias.py +0 -0
  133. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_check.py +0 -0
  134. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_default_dtypes.py +0 -0
  135. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_metadata.py +0 -0
  136. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_polars_schema.py +0 -0
  137. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_pyarrow.py +0 -0
  138. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_rules.py +0 -0
  139. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_sample.py +0 -0
  140. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_str.py +0 -0
  141. {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_utils.py +0 -0
  142. {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/__init__.py +0 -0
  143. {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_column_validation.py +0 -0
  144. {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_dtype_validation.py +0 -0
  145. {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_rule_evaluation.py +0 -0
  146. {dataframely-1.1.0 → dataframely-1.2.1}/tests/functional/test_concat.py +0 -0
  147. {dataframely-1.1.0 → dataframely-1.2.1}/tests/functional/test_relationships.py +0 -0
  148. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_base.py +0 -0
  149. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_cast.py +0 -0
  150. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_create_empty.py +0 -0
  151. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_create_empty_if_none.py +0 -0
  152. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_filter.py +0 -0
  153. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_inheritance.py +0 -0
  154. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_rule_implementation.py +0 -0
  155. {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_sample.py +0 -0
  156. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_compat.py +0 -0
  157. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_config.py +0 -0
  158. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_exc.py +0 -0
  159. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_extre.py +0 -0
  160. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_failure_info.py +0 -0
  161. {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_random.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframely
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -34,6 +34,7 @@ from .columns import (
34
34
  Int64,
35
35
  Integer,
36
36
  List,
37
+ Object,
37
38
  String,
38
39
  Struct,
39
40
  Time,
@@ -89,4 +90,5 @@ __all__ = [
89
90
  "String",
90
91
  "Struct",
91
92
  "List",
93
+ "Object",
92
94
  ]
@@ -7,7 +7,7 @@ import typing
7
7
  from abc import ABCMeta
8
8
  from collections.abc import Iterable
9
9
  from dataclasses import dataclass, field
10
- from typing import Annotated, Any, Self, get_args, get_origin
10
+ from typing import Annotated, Any, Self, cast, get_args, get_origin
11
11
 
12
12
  import polars as pl
13
13
 
@@ -183,59 +183,9 @@ class CollectionMeta(ABCMeta):
183
183
  # Get all members via the annotations
184
184
  if "__annotations__" in source:
185
185
  for attr, kls in source["__annotations__"].items():
186
- origin = get_origin(kls)
187
-
188
- # optional annotation
189
- collection_member = CollectionMember()
190
-
191
- if origin is Annotated:
192
- annotation_args = get_args(kls)
193
- origin_arg0 = get_origin(annotation_args[0])
194
- if not origin_arg0 or not issubclass(origin_arg0, TypedLazyFrame):
195
- raise AnnotationImplementationError(attr, kls)
196
- if len(annotation_args) > 2:
197
- raise AnnotationImplementationError(attr, kls)
198
- if not isinstance(annotation_args[1], CollectionMember):
199
- raise AnnotationImplementationError(attr, kls)
200
-
201
- # Continue with wrapped FrameType
202
- collection_member = annotation_args[1]
203
- kls = annotation_args[0]
204
- origin = origin_arg0
205
-
206
- if origin is None:
207
- # `None` annotation is not allowed
208
- raise AnnotationImplementationError(attr, kls)
209
- elif origin == typing.Union:
210
- # Happy path: optional member
211
- union_args = get_args(kls)
212
- if len(union_args) != 2:
213
- raise AnnotationImplementationError(attr, kls)
214
- if not any(get_origin(arg) is None for arg in union_args):
215
- raise AnnotationImplementationError(attr, kls)
216
-
217
- [not_none_arg] = [
218
- arg for arg in union_args if get_origin(arg) is not None
219
- ]
220
- if not issubclass(get_origin(not_none_arg), TypedLazyFrame):
221
- raise AnnotationImplementationError(attr, kls)
222
-
223
- result.members[attr] = MemberInfo(
224
- schema=get_args(not_none_arg)[0],
225
- is_optional=True,
226
- ignored_in_filters=collection_member.ignored_in_filters,
227
- )
228
- elif issubclass(origin, TypedLazyFrame):
229
- # Happy path: required member
230
- result.members[attr] = MemberInfo(
231
- schema=get_args(kls)[0],
232
- is_optional=False,
233
- ignored_in_filters=collection_member.ignored_in_filters,
234
- inline_for_sampling=collection_member.inline_for_sampling,
235
- )
236
- else:
237
- # Some other unknown annotation
238
- raise AnnotationImplementationError(attr, kls)
186
+ result.members[attr] = CollectionMeta._derive_member_info(
187
+ attr, kls, CollectionMember()
188
+ )
239
189
 
240
190
  # Get all filters by traversing the source
241
191
  for attr, value in {
@@ -246,6 +196,55 @@ class CollectionMeta(ABCMeta):
246
196
 
247
197
  return result
248
198
 
199
+ @staticmethod
200
+ def _derive_member_info(
201
+ attr: str, type_annotation: Any, collection_member: CollectionMember
202
+ ) -> MemberInfo:
203
+ origin = get_origin(type_annotation)
204
+
205
+ if origin is None:
206
+ # `None` annotation is not allowed
207
+ raise AnnotationImplementationError(attr, type_annotation)
208
+ elif origin == Annotated:
209
+ # Maybe happy path: annotated member, dispatch recursively
210
+ annotation_args = cast(list[Any], get_args(type_annotation))
211
+ if len(annotation_args) > 2:
212
+ raise AnnotationImplementationError(attr, type_annotation)
213
+ if not isinstance(annotation_args[1], CollectionMember):
214
+ raise AnnotationImplementationError(attr, type_annotation)
215
+ return CollectionMeta._derive_member_info(
216
+ attr, annotation_args[0], annotation_args[1]
217
+ )
218
+ elif origin == typing.Union:
219
+ # Happy path: optional member
220
+ union_args = get_args(type_annotation)
221
+ if len(union_args) != 2:
222
+ raise AnnotationImplementationError(attr, type_annotation)
223
+ if not any(get_origin(arg) is None for arg in union_args):
224
+ raise AnnotationImplementationError(attr, type_annotation)
225
+
226
+ [not_none_arg] = [arg for arg in union_args if get_origin(arg) is not None]
227
+ if not issubclass(get_origin(not_none_arg), TypedLazyFrame):
228
+ raise AnnotationImplementationError(attr, type_annotation)
229
+
230
+ return MemberInfo(
231
+ schema=get_args(not_none_arg)[0],
232
+ is_optional=True,
233
+ ignored_in_filters=collection_member.ignored_in_filters,
234
+ inline_for_sampling=collection_member.inline_for_sampling,
235
+ )
236
+ elif issubclass(origin, TypedLazyFrame):
237
+ # Happy path: required member
238
+ return MemberInfo(
239
+ schema=get_args(type_annotation)[0],
240
+ is_optional=False,
241
+ ignored_in_filters=collection_member.ignored_in_filters,
242
+ inline_for_sampling=collection_member.inline_for_sampling,
243
+ )
244
+ else:
245
+ # Some other unknown annotation
246
+ raise AnnotationImplementationError(attr, type_annotation)
247
+
249
248
 
250
249
  class BaseCollection(metaclass=CollectionMeta):
251
250
  """Internal utility abstraction to reference collections without introducing
@@ -126,5 +126,7 @@ def _with_group_rules(lf: pl.LazyFrame, rules: dict[str, GroupRule]) -> pl.LazyF
126
126
  # preserves the order of the left data frame.
127
127
  result = lf
128
128
  for group_columns, frame in group_evaluations.items():
129
- result = result.join(frame, on=list(group_columns), how="left")
129
+ result = result.join(
130
+ frame, on=list(group_columns), how="left", nulls_equal=True
131
+ )
130
132
  return result
@@ -4,7 +4,7 @@
4
4
  import sys
5
5
  import warnings
6
6
  from abc import ABC
7
- from collections.abc import Mapping, MutableMapping, Sequence
7
+ from collections.abc import Mapping, Sequence
8
8
  from pathlib import Path
9
9
  from typing import Any, Generic, Self, TypeVar, cast
10
10
 
@@ -20,10 +20,10 @@ from .random import Generator
20
20
 
21
21
  if sys.version_info >= (3, 13):
22
22
  SamplingType = TypeVar(
23
- "SamplingType", bound=MutableMapping[str, Any], default=dict[str, Any]
23
+ "SamplingType", bound=Mapping[str, Any], default=Mapping[str, Any]
24
24
  )
25
25
  else: # pragma: no cover
26
- SamplingType = TypeVar("SamplingType", bound=MutableMapping[str, Any])
26
+ SamplingType = TypeVar("SamplingType", bound=Mapping[str, Any])
27
27
 
28
28
 
29
29
  class Collection(BaseCollection, ABC, Generic[SamplingType]):
@@ -10,6 +10,7 @@ from .enum import Enum
10
10
  from .float import Float, Float32, Float64
11
11
  from .integer import Int8, Int16, Int32, Int64, Integer, UInt8, UInt16, UInt32, UInt64
12
12
  from .list import List
13
+ from .object import Object
13
14
  from .string import String
14
15
  from .struct import Struct
15
16
 
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "Int32",
32
33
  "Int64",
33
34
  "Integer",
35
+ "Object",
34
36
  "UInt8",
35
37
  "UInt16",
36
38
  "UInt32",
@@ -49,7 +49,7 @@ class Enum(Column):
49
49
  alias=alias,
50
50
  metadata=metadata,
51
51
  )
52
- self.categories = categories
52
+ self.categories = list(categories)
53
53
 
54
54
  @property
55
55
  def dtype(self) -> pl.DataType:
@@ -0,0 +1,63 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import polars as pl
10
+
11
+ from dataframely._compat import pa, sa, sa_TypeEngine
12
+ from dataframely.random import Generator
13
+
14
+ from ._base import Column
15
+
16
+
17
+ class Object(Column):
18
+ """A Python Object column."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ nullable: bool = True,
24
+ primary_key: bool = False,
25
+ check: Callable[[pl.Expr], pl.Expr] | None = None,
26
+ alias: str | None = None,
27
+ metadata: dict[str, Any] | None = None,
28
+ ):
29
+ """
30
+ Args:
31
+ nullable: Whether this column may contain null values.
32
+ primary_key: Whether this column is part of the primary key of the schema.
33
+ check: A custom check to run for this column. Must return a non-aggregated
34
+ boolean expression.
35
+ alias: An overwrite for this column's name which allows for using a column
36
+ name that is not a valid Python identifier. Especially note that setting
37
+ this option does _not_ allow to refer to the column with two different
38
+ names, the specified alias is the only valid name.
39
+ metadata: A dictionary of metadata to attach to the column.
40
+ """
41
+ super().__init__(
42
+ nullable=nullable,
43
+ primary_key=primary_key,
44
+ check=check,
45
+ alias=alias,
46
+ metadata=metadata,
47
+ )
48
+
49
+ @property
50
+ def dtype(self) -> pl.DataType:
51
+ return pl.Object()
52
+
53
+ def sqlalchemy_dtype(self, dialect: sa.Dialect) -> sa_TypeEngine:
54
+ raise NotImplementedError("SQL column cannot have 'Object' type.")
55
+
56
+ @property
57
+ def pyarrow_dtype(self) -> pa.DataType:
58
+ raise NotImplementedError("PyArrow column cannot have 'Object' type.")
59
+
60
+ def _sample_unchecked(self, generator: Generator, n: int) -> pl.Series:
61
+ raise NotImplementedError(
62
+ "Random data sampling not implemented for 'Object' type."
63
+ )
@@ -1,17 +1,21 @@
1
1
  # Copyright (c) QuantCo 2025-2025
2
2
  # SPDX-License-Identifier: BSD-3-Clause
3
3
 
4
+ from collections.abc import Sequence
4
5
  from typing import TypeVar
5
6
 
6
7
  import polars as pl
7
8
 
9
+ from ._base_collection import BaseCollection
8
10
  from ._typing import LazyFrame
9
- from .collection import Collection
10
11
  from .schema import Schema
11
12
 
12
13
  S = TypeVar("S", bound=Schema)
13
14
  T = TypeVar("T", bound=Schema)
14
- C = TypeVar("C", bound=Collection)
15
+
16
+ # NOTE: Binding to `BaseCollection` is required here as the TypeVar default for the
17
+ # sampling type otherwise causes issues for Python 3.13.
18
+ C = TypeVar("C", bound=BaseCollection)
15
19
 
16
20
  # ------------------------------------------------------------------------------------ #
17
21
  # FILTER #
@@ -62,7 +66,7 @@ def filter_relationship_one_to_at_least_one(
62
66
  # ------------------------------------------------------------------------------------ #
63
67
 
64
68
 
65
- def concat_collection_members(collections: list[C], /) -> dict[str, pl.LazyFrame]:
69
+ def concat_collection_members(collections: Sequence[C], /) -> dict[str, pl.LazyFrame]:
66
70
  """Concatenate the members of collections with the same type.
67
71
 
68
72
  Args:
@@ -19,7 +19,7 @@ description = "A declarative, polars-native data frame validation library"
19
19
  name = "dataframely"
20
20
  readme = "README.md"
21
21
  requires-python = ">=3.11"
22
- version = "1.1.0"
22
+ version = "1.2.1"
23
23
 
24
24
  [project.urls]
25
25
  Repository = "https://github.com/quantco/dataframely"
@@ -81,6 +81,24 @@ def test_annotation_union_conflicting_types_failure() -> None:
81
81
  )
82
82
 
83
83
 
84
+ def test_annotation_annotated_success() -> None:
85
+ """When we use an Annotated type, it must accept a union type."""
86
+ create_collection_raw(
87
+ "test",
88
+ {
89
+ "first": Annotated[
90
+ dy.LazyFrame[MyTestSchema] | None, dy.CollectionMember()
91
+ ],
92
+ },
93
+ )
94
+ create_collection_raw(
95
+ "test",
96
+ {
97
+ "first": dy.LazyFrame[MyTestSchema] | None,
98
+ },
99
+ )
100
+
101
+
84
102
  def test_annotation_only_none_failure() -> None:
85
103
  """Annotations must not just be None."""
86
104
  with pytest.raises(AnnotationImplementationError):
@@ -54,6 +54,21 @@ class MyInlinedCollection(dy.Collection):
54
54
  return sample
55
55
 
56
56
 
57
+ class MyInlinedCollectionWithOptional(dy.Collection):
58
+ first: Annotated[
59
+ dy.LazyFrame[MyFirstSchema] | None,
60
+ dy.CollectionMember(inline_for_sampling=True),
61
+ ]
62
+ second: dy.LazyFrame[MySecondSchema]
63
+
64
+ @classmethod
65
+ def _preprocess_sample(
66
+ cls, sample: dict[str, Any], index: int, generator: Generator
67
+ ) -> dict[str, Any]:
68
+ sample["a"] = index
69
+ return sample
70
+
71
+
57
72
  class SmallCollection(dy.Collection):
58
73
  first: dy.LazyFrame[MyFirstSchema]
59
74
 
@@ -117,13 +132,20 @@ def test_sample_with_overrides() -> None:
117
132
  assert collection.second.collect()["c"].to_list() == [3, 4, 6]
118
133
 
119
134
 
120
- def test_sample_inline_with_overrides() -> None:
121
- collection = MyInlinedCollection.sample(
135
+ @pytest.mark.parametrize(
136
+ "collection_type", [MyInlinedCollection, MyInlinedCollectionWithOptional]
137
+ )
138
+ def test_sample_inline_with_overrides(
139
+ collection_type: type[MyInlinedCollection] | type[MyInlinedCollectionWithOptional],
140
+ ) -> None:
141
+ collection = collection_type.sample(
122
142
  overrides=[
123
143
  {"b": 4, "second": [{"c": 3}, {"c": 4}]},
124
144
  {"b": 8, "second": [{"c": 6}]},
125
145
  ]
126
146
  )
147
+
148
+ assert collection.first is not None
127
149
  assert collection.first.collect()["a"].to_list() == [0, 1]
128
150
  assert collection.first.collect()["b"].to_list() == [4, 8]
129
151
 
@@ -52,3 +52,12 @@ def test_valid_cast(
52
52
  schema = create_schema("test", {"a": enum})
53
53
  df = df_type(data)
54
54
  assert schema.is_valid(df, cast=True) == valid
55
+
56
+
57
+ @pytest.mark.parametrize("type1", [list, tuple])
58
+ @pytest.mark.parametrize("type2", [list, tuple])
59
+ def test_different_sequences(type1: type, type2: type) -> None:
60
+ allowed = ["a", "b"]
61
+ S = create_schema("test", {"x": dy.Enum(type1(allowed))})
62
+ df = pl.DataFrame({"x": pl.Series(["a", "b"], dtype=pl.Enum(type2(allowed)))})
63
+ S.validate(df)
@@ -0,0 +1,60 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+
5
+ import polars as pl
6
+ import pytest
7
+
8
+ import dataframely as dy
9
+ from dataframely.columns._base import Column
10
+ from dataframely.random import Generator
11
+ from dataframely.testing import create_schema
12
+
13
+
14
+ class CustomObject:
15
+ def __init__(self, a: int, b: str) -> None:
16
+ self.a = a
17
+ self.b = b
18
+
19
+
20
+ def test_simple_object() -> None:
21
+ schema = create_schema("test", {"o": dy.Object()})
22
+ assert schema.is_valid(
23
+ pl.DataFrame({"o": [CustomObject(a=1, b="foo"), CustomObject(a=2, b="bar")]})
24
+ )
25
+
26
+
27
+ @pytest.mark.parametrize(
28
+ ("column", "dtype", "is_valid"),
29
+ [
30
+ (
31
+ dy.Object(),
32
+ pl.Object(),
33
+ True,
34
+ ),
35
+ (
36
+ dy.Object(),
37
+ object(),
38
+ False,
39
+ ),
40
+ ],
41
+ )
42
+ def test_validate_dtype(column: Column, dtype: pl.DataType, is_valid: bool) -> None:
43
+ assert column.validate_dtype(dtype) == is_valid
44
+
45
+
46
+ def test_pyarrow_dtype_raises() -> None:
47
+ column = dy.Object()
48
+ with pytest.raises(
49
+ NotImplementedError, match="PyArrow column cannot have 'Object' type."
50
+ ):
51
+ column.pyarrow_dtype
52
+
53
+
54
+ def test_sampling_raises() -> None:
55
+ column = dy.Object()
56
+ with pytest.raises(
57
+ NotImplementedError,
58
+ match="Random data sampling not implemented for 'Object' type.",
59
+ ):
60
+ column.sample(generator=Generator(), n=10)
@@ -145,3 +145,11 @@ def test_raise_for_struct_column(dialect: sa.Dialect) -> None:
145
145
  NotImplementedError, match="SQL column cannot have 'Struct' type."
146
146
  ):
147
147
  dy.Struct({"a": dy.String()}).sqlalchemy_dtype(dialect)
148
+
149
+
150
+ @pytest.mark.parametrize("dialect", [MSDialect_pyodbc(), PGDialect_psycopg2()])
151
+ def test_raise_for_object_column(dialect: sa.Dialect) -> None:
152
+ with pytest.raises(
153
+ NotImplementedError, match="SQL column cannot have 'Object' type."
154
+ ):
155
+ dy.Object().sqlalchemy_dtype(dialect)
@@ -116,3 +116,12 @@ def test_success_multi_row_strip_cast(
116
116
  )
117
117
  assert_frame_equal(actual, expected)
118
118
  assert MySchema.is_valid(df, cast=True)
119
+
120
+
121
+ @pytest.mark.parametrize("df_type", [pl.DataFrame, pl.LazyFrame])
122
+ def test_group_rule_on_nulls(df_type: type[pl.DataFrame] | type[pl.LazyFrame]) -> None:
123
+ # The schema is violated because we have multiple "b" values for the same "a" value
124
+ df = df_type({"a": [None, None], "b": [1, 2]})
125
+ with pytest.raises(RuleValidationError):
126
+ MyComplexSchema.validate(df, cast=True)
127
+ assert not MyComplexSchema.is_valid(df, cast=True)
@@ -9,7 +9,7 @@
9
9
  import datetime
10
10
  import decimal
11
11
  import functools
12
- from typing import Any
12
+ from typing import Any, NotRequired, TypedDict
13
13
 
14
14
  import polars as pl
15
15
  import pytest
@@ -61,7 +61,21 @@ class MySecondSchema(dy.Schema):
61
61
  b = dy.Integer()
62
62
 
63
63
 
64
- class MyCollection(dy.Collection):
64
+ class SamplingTypeFirst(TypedDict):
65
+ a: NotRequired[int]
66
+
67
+
68
+ class SamplingTypeSecond(TypedDict):
69
+ a: NotRequired[int]
70
+ b: NotRequired[int]
71
+
72
+
73
+ class SamplingType(TypedDict):
74
+ first: NotRequired[SamplingTypeFirst]
75
+ second: NotRequired[SamplingTypeSecond]
76
+
77
+
78
+ class MyCollection(dy.Collection[SamplingType]):
65
79
  first: dy.LazyFrame[MyFirstSchema]
66
80
  second: dy.LazyFrame[MySecondSchema]
67
81
 
@@ -73,6 +87,12 @@ def test_collection_filter_return_value() -> None:
73
87
  assert len(failure["third"]) == 0 # type: ignore[misc]
74
88
 
75
89
 
90
+ def test_collection_concat() -> None:
91
+ c1 = MyCollection.create_empty()
92
+ c2 = MyCollection.create_empty()
93
+ dy.concat_collection_members([c1, c2])
94
+
95
+
76
96
  # ------------------------------------------------------------------------------------ #
77
97
  # ITER ROWS #
78
98
  # ------------------------------------------------------------------------------------ #
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes