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.
- {dataframely-1.1.0 → dataframely-1.2.1}/PKG-INFO +1 -1
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/__init__.py +2 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_base_collection.py +53 -54
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_rule.py +3 -1
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/collection.py +3 -3
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/__init__.py +2 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/enum.py +1 -1
- dataframely-1.2.1/dataframely/columns/object.py +63 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/functional.py +7 -3
- {dataframely-1.1.0 → dataframely-1.2.1}/pyproject.toml +1 -1
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_implementation.py +18 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_sample.py +24 -2
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_enum.py +9 -0
- dataframely-1.2.1/tests/column_types/test_object.py +60 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_sql_schema.py +8 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_validate.py +9 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_typing.py +22 -2
- {dataframely-1.1.0 → dataframely-1.2.1}/.copier-answers.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.envrc +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.gitattributes +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/CODEOWNERS +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/dependabot.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/release-drafter.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/build.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/chore.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/ci.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.github/workflows/update-lockfiles.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.gitignore +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.pre-commit-config.yaml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.prettierignore +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.prettierrc +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/.readthedocs.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/Cargo.lock +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/Cargo.toml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/LICENSE +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/README.md +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_base_schema.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_compat.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_extre.pyi +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_filter.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_polars.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_typing.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/_validation.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_base.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_mixins.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/_utils.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/any.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/bool.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/datetime.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/decimal.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/float.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/integer.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/list.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/string.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/columns/struct.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/config.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/exc.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/failure.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/mypy.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/py.typed +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/random.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/schema.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/__init__.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/const.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/factory.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/mask.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/rules.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/dataframely/testing/typing.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docker-compose.yml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/Makefile +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.collection.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.any.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.bool.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.datetime.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.decimal.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.enum.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.float.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.integer.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.list.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.string.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.columns.struct.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.config.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.exc.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.failure.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.functional.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.mypy.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.random.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.schema.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.const.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.factory.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.mask.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.rules.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/dataframely.testing.typing.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_api/modules.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_static/custom.css +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/_static/favicon.ico +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/conf.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/index.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/make.bat +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/development.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/examples/real-world.ipynb +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/faq.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/installation.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/docs/sites/quickstart.rst +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/pixi.lock +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/pixi.toml +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/src/errdefs.rs +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/src/lib.rs +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/src/regex_repr.rs +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_base.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_cast.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_create_empty.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_filter_one_to_n.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_filter_validate.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_ignore_in_filter.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_optional_members.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/collection/test_validate_input.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/__init__.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_any.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_datetime.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_decimal.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_float.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_integer.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_list.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_string.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/column_types/test_struct.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/__init__.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_alias.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_check.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_default_dtypes.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_metadata.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_polars_schema.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_pyarrow.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_rules.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_sample.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_str.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/columns/test_utils.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/__init__.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_column_validation.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_dtype_validation.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/core_validation/test_rule_evaluation.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/functional/test_concat.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/functional/test_relationships.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_base.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_cast.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_create_empty.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_create_empty_if_none.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_filter.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_inheritance.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_rule_implementation.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/schema/test_sample.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_compat.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_config.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_exc.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_extre.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_failure_info.py +0 -0
- {dataframely-1.1.0 → dataframely-1.2.1}/tests/test_random.py +0 -0
|
@@ -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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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(
|
|
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,
|
|
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=
|
|
23
|
+
"SamplingType", bound=Mapping[str, Any], default=Mapping[str, Any]
|
|
24
24
|
)
|
|
25
25
|
else: # pragma: no cover
|
|
26
|
-
SamplingType = TypeVar("SamplingType", bound=
|
|
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",
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|