dataframely 1.0.0__cp311-abi3-win_amd64.whl

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 (44) hide show
  1. dataframely/__init__.py +92 -0
  2. dataframely/_base_collection.py +285 -0
  3. dataframely/_base_schema.py +186 -0
  4. dataframely/_compat.py +39 -0
  5. dataframely/_extre.pyd +0 -0
  6. dataframely/_extre.pyi +54 -0
  7. dataframely/_filter.py +42 -0
  8. dataframely/_polars.py +63 -0
  9. dataframely/_rule.py +130 -0
  10. dataframely/_typing.py +97 -0
  11. dataframely/_validation.py +85 -0
  12. dataframely/collection.py +616 -0
  13. dataframely/columns/__init__.py +41 -0
  14. dataframely/columns/_base.py +191 -0
  15. dataframely/columns/_mixins.py +94 -0
  16. dataframely/columns/_utils.py +60 -0
  17. dataframely/columns/any.py +72 -0
  18. dataframely/columns/bool.py +31 -0
  19. dataframely/columns/datetime.py +501 -0
  20. dataframely/columns/decimal.py +162 -0
  21. dataframely/columns/enum.py +76 -0
  22. dataframely/columns/float.py +201 -0
  23. dataframely/columns/integer.py +349 -0
  24. dataframely/columns/list.py +150 -0
  25. dataframely/columns/string.py +126 -0
  26. dataframely/columns/struct.py +105 -0
  27. dataframely/config.py +54 -0
  28. dataframely/exc.py +125 -0
  29. dataframely/failure.py +144 -0
  30. dataframely/functional.py +85 -0
  31. dataframely/mypy.py +401 -0
  32. dataframely/py.typed +0 -0
  33. dataframely/random.py +409 -0
  34. dataframely/schema.py +615 -0
  35. dataframely/testing/__init__.py +27 -0
  36. dataframely/testing/const.py +49 -0
  37. dataframely/testing/factory.py +63 -0
  38. dataframely/testing/mask.py +46 -0
  39. dataframely/testing/rules.py +33 -0
  40. dataframely/testing/typing.py +30 -0
  41. dataframely-1.0.0.dist-info/METADATA +97 -0
  42. dataframely-1.0.0.dist-info/RECORD +44 -0
  43. dataframely-1.0.0.dist-info/WHEEL +4 -0
  44. dataframely-1.0.0.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,92 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ import importlib.metadata
5
+ import warnings
6
+
7
+ try:
8
+ __version__ = importlib.metadata.version(__name__)
9
+ except importlib.metadata.PackageNotFoundError as e: # pragma: no cover
10
+ warnings.warn(f"Could not determine version of {__name__}\n{e!s}", stacklevel=2)
11
+ __version__ = "unknown"
12
+
13
+ from . import random
14
+ from ._base_collection import CollectionMember
15
+ from ._filter import filter
16
+ from ._rule import rule
17
+ from ._typing import DataFrame, LazyFrame
18
+ from .collection import Collection
19
+ from .columns import (
20
+ Any,
21
+ Bool,
22
+ Column,
23
+ Date,
24
+ Datetime,
25
+ Decimal,
26
+ Duration,
27
+ Enum,
28
+ Float,
29
+ Float32,
30
+ Float64,
31
+ Int8,
32
+ Int16,
33
+ Int32,
34
+ Int64,
35
+ Integer,
36
+ List,
37
+ String,
38
+ Struct,
39
+ Time,
40
+ UInt8,
41
+ UInt16,
42
+ UInt32,
43
+ UInt64,
44
+ )
45
+ from .config import Config
46
+ from .failure import FailureInfo
47
+ from .functional import (
48
+ concat_collection_members,
49
+ filter_relationship_one_to_at_least_one,
50
+ filter_relationship_one_to_one,
51
+ )
52
+ from .schema import Schema
53
+
54
+ __all__ = [
55
+ "random",
56
+ "filter",
57
+ "rule",
58
+ "DataFrame",
59
+ "LazyFrame",
60
+ "Collection",
61
+ "CollectionMember",
62
+ "Config",
63
+ "FailureInfo",
64
+ "concat_collection_members",
65
+ "filter_relationship_one_to_at_least_one",
66
+ "filter_relationship_one_to_one",
67
+ "Schema",
68
+ "Any",
69
+ "Bool",
70
+ "Column",
71
+ "Date",
72
+ "Datetime",
73
+ "Decimal",
74
+ "Duration",
75
+ "Time",
76
+ "Enum",
77
+ "Float",
78
+ "Float32",
79
+ "Float64",
80
+ "Int8",
81
+ "Int16",
82
+ "Int32",
83
+ "Int64",
84
+ "Integer",
85
+ "UInt8",
86
+ "UInt16",
87
+ "UInt32",
88
+ "UInt64",
89
+ "String",
90
+ "Struct",
91
+ "List",
92
+ ]
@@ -0,0 +1,285 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from __future__ import annotations
5
+
6
+ import typing
7
+ from abc import ABCMeta
8
+ from collections.abc import Iterable
9
+ from dataclasses import dataclass, field
10
+ from typing import Annotated, Any, Self, get_args, get_origin
11
+
12
+ import polars as pl
13
+
14
+ from ._filter import Filter
15
+ from ._typing import LazyFrame as TypedLazyFrame
16
+ from .exc import AnnotationImplementationError, ImplementationError
17
+ from .schema import Schema
18
+
19
+ _MEMBER_ATTR = "__dataframely_members__"
20
+ _FILTER_ATTR = "__dataframely_filters__"
21
+
22
+
23
+ @dataclass(kw_only=True)
24
+ class CollectionMember:
25
+ """An annotation class that configures different behavior for a collection member.
26
+
27
+ Members:
28
+ ignored_in_filters: Indicates that a member should be ignored in the
29
+ ``@dy.filter`` methods of a collection. This also affects the computation
30
+ of the shared primary key in the collection.
31
+
32
+ Example:
33
+ .. code:: python
34
+
35
+ class MyCollection(dy.Collection):
36
+ a: dy.LazyFrame[MySchema1]
37
+ b: dy.LazyFrame[MySchema2]
38
+
39
+ ignored_member: Annotated[
40
+ dy.LazyFrame[MySchema3],
41
+ dy.CollectionMember(ignored_in_filters=True)
42
+ ]
43
+
44
+ @dy.filter
45
+ def my_filter(self) -> pl.DataFrame:
46
+ return self.a.join(self.b, on="shared_key")
47
+ """
48
+
49
+ #: Whether the member should be ignored in the filter method.
50
+ ignored_in_filters: bool = False
51
+
52
+
53
+ # --------------------------------------- UTILS -------------------------------------- #
54
+
55
+
56
+ def _common_primary_keys(columns: Iterable[type[Schema]]) -> set[str]:
57
+ return set.intersection(*[set(schema.primary_keys()) for schema in columns])
58
+
59
+
60
+ # ------------------------------------------------------------------------------------ #
61
+ # COLLECTION META #
62
+ # ------------------------------------------------------------------------------------ #
63
+
64
+
65
+ @dataclass
66
+ class MemberInfo(CollectionMember):
67
+ """Information about a member of a collection."""
68
+
69
+ #: The schema of the member.
70
+ schema: type[Schema]
71
+ #: Whether the member is optional.
72
+ is_optional: bool
73
+
74
+
75
+ @dataclass
76
+ class Metadata:
77
+ """Utility class to gather members and filters associated with a collection."""
78
+
79
+ members: dict[str, MemberInfo] = field(default_factory=dict)
80
+ filters: dict[str, Filter] = field(default_factory=dict)
81
+
82
+ def update(self, other: Self):
83
+ self.members.update(other.members)
84
+ self.filters.update(other.filters)
85
+
86
+
87
+ class CollectionMeta(ABCMeta):
88
+ def __new__(
89
+ mcs, # noqa: N804
90
+ name: str,
91
+ bases: tuple[type[object], ...],
92
+ namespace: dict[str, Any],
93
+ *args: Any,
94
+ **kwargs: Any,
95
+ ):
96
+ result = Metadata()
97
+ for base in bases:
98
+ result.update(mcs._get_metadata_recursively(base))
99
+ result.update(mcs._get_metadata(namespace))
100
+ namespace[_MEMBER_ATTR] = result.members
101
+ namespace[_FILTER_ATTR] = result.filters
102
+
103
+ # We now have all necessary information about filters and members. We want to
104
+ # check some preconditions to not run into issues later...
105
+
106
+ non_ignored_member_schemas = [
107
+ m.schema for m in result.members.values() if not m.ignored_in_filters
108
+ ]
109
+
110
+ # 1) Check that there are overlapping primary keys that allow the application
111
+ # of filters.
112
+ if len(non_ignored_member_schemas) > 0 and len(result.filters) > 0:
113
+ if len(_common_primary_keys(non_ignored_member_schemas)) == 0:
114
+ raise ImplementationError(
115
+ "Members of a collection must have an overlapping primary key "
116
+ "but did not find any."
117
+ )
118
+
119
+ # 2) Check that filter names do not overlap with any column or rule names
120
+ if len(result.members) > 0:
121
+ taken = set.union(
122
+ *(
123
+ set(member.schema.column_names())
124
+ for member in result.members.values()
125
+ ),
126
+ *(
127
+ set(member.schema._validation_rules())
128
+ for member in result.members.values()
129
+ ),
130
+ )
131
+ intersection = taken & set(result.filters)
132
+ if len(intersection) > 0:
133
+ raise ImplementationError(
134
+ "Filters defined on the collection must not be named the same as any "
135
+ "column or rule in any of the member frames but found "
136
+ f"{len(intersection)} such filters: {sorted(intersection)}."
137
+ )
138
+
139
+ return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
140
+
141
+ @staticmethod
142
+ def _get_metadata_recursively(kls: type[object]) -> Metadata:
143
+ result = Metadata()
144
+ for base in kls.__bases__:
145
+ result.update(CollectionMeta._get_metadata_recursively(base))
146
+ result.update(CollectionMeta._get_metadata(kls.__dict__)) # type: ignore
147
+ return result
148
+
149
+ @staticmethod
150
+ def _get_metadata(source: dict[str, Any]) -> Metadata:
151
+ result = Metadata()
152
+
153
+ # Get all members via the annotations
154
+ if "__annotations__" in source:
155
+ for attr, kls in source["__annotations__"].items():
156
+ origin = get_origin(kls)
157
+
158
+ # optional annotation
159
+ collection_member = CollectionMember()
160
+
161
+ if origin is Annotated:
162
+ annotation_args = get_args(kls)
163
+ origin_arg0 = get_origin(annotation_args[0])
164
+ if not origin_arg0 or not issubclass(origin_arg0, TypedLazyFrame):
165
+ raise AnnotationImplementationError(attr, kls)
166
+ if len(annotation_args) > 2:
167
+ raise AnnotationImplementationError(attr, kls)
168
+ if not isinstance(annotation_args[1], CollectionMember):
169
+ raise AnnotationImplementationError(attr, kls)
170
+
171
+ # Continue with wrapped FrameType
172
+ collection_member = annotation_args[1]
173
+ kls = annotation_args[0]
174
+ origin = origin_arg0
175
+
176
+ if origin is None:
177
+ # `None` annotation is not allowed
178
+ raise AnnotationImplementationError(attr, kls)
179
+ elif origin == typing.Union:
180
+ # Happy path: optional member
181
+ union_args = get_args(kls)
182
+ if len(union_args) != 2:
183
+ raise AnnotationImplementationError(attr, kls)
184
+ if not any(get_origin(arg) is None for arg in union_args):
185
+ raise AnnotationImplementationError(attr, kls)
186
+
187
+ [not_none_arg] = [
188
+ arg for arg in union_args if get_origin(arg) is not None
189
+ ]
190
+ if not issubclass(get_origin(not_none_arg), TypedLazyFrame):
191
+ raise AnnotationImplementationError(attr, kls)
192
+
193
+ result.members[attr] = MemberInfo(
194
+ schema=get_args(not_none_arg)[0],
195
+ is_optional=True,
196
+ ignored_in_filters=collection_member.ignored_in_filters,
197
+ )
198
+ elif issubclass(origin, TypedLazyFrame):
199
+ # Happy path: required member
200
+ result.members[attr] = MemberInfo(
201
+ schema=get_args(kls)[0],
202
+ is_optional=False,
203
+ ignored_in_filters=collection_member.ignored_in_filters,
204
+ )
205
+ else:
206
+ # Some other unknown annotation
207
+ raise AnnotationImplementationError(attr, kls)
208
+
209
+ # Get all filters by traversing the source
210
+ for attr, value in {
211
+ k: v for k, v in source.items() if not k.startswith("__")
212
+ }.items():
213
+ if isinstance(value, Filter):
214
+ result.filters[attr] = value
215
+
216
+ return result
217
+
218
+
219
+ class BaseCollection(metaclass=CollectionMeta):
220
+ """Internal utility abstraction to reference collections without introducing
221
+ cyclical dependencies."""
222
+
223
+ @classmethod
224
+ def members(cls) -> dict[str, MemberInfo]:
225
+ """Information about the members of the collection."""
226
+ return getattr(cls, _MEMBER_ATTR)
227
+
228
+ @classmethod
229
+ def member_schemas(cls) -> dict[str, type[Schema]]:
230
+ """The schemas of all members of the collection."""
231
+ return {name: member.schema for name, member in cls.members().items()}
232
+
233
+ @classmethod
234
+ def required_members(cls) -> set[str]:
235
+ """The names of all required members of the collection."""
236
+ return {
237
+ name for name, member in cls.members().items() if not member.is_optional
238
+ }
239
+
240
+ @classmethod
241
+ def optional_members(cls) -> set[str]:
242
+ """The names of all optional members of the collection."""
243
+ return {name for name, member in cls.members().items() if member.is_optional}
244
+
245
+ @classmethod
246
+ def ignored_members(cls) -> set[str]:
247
+ """The names of all members of the collection that are ignored in filters."""
248
+ return {
249
+ name for name, member in cls.members().items() if member.ignored_in_filters
250
+ }
251
+
252
+ @classmethod
253
+ def non_ignored_members(cls) -> set[str]:
254
+ """The names of all members of the collection that are not ignored in filters
255
+ (default)."""
256
+ return {
257
+ name
258
+ for name, member in cls.members().items()
259
+ if not member.ignored_in_filters
260
+ }
261
+
262
+ @classmethod
263
+ def common_primary_keys(cls) -> list[str]:
264
+ """The primary keys shared by non ignored members of the collection."""
265
+ return sorted(
266
+ _common_primary_keys(
267
+ [
268
+ member.schema
269
+ for member in cls.members().values()
270
+ if not member.ignored_in_filters
271
+ ]
272
+ )
273
+ )
274
+
275
+ @classmethod
276
+ def _filters(cls) -> dict[str, Filter[Self]]:
277
+ return getattr(cls, _FILTER_ATTR)
278
+
279
+ def to_dict(self) -> dict[str, pl.LazyFrame]:
280
+ """Return a dictionary representation of this collection."""
281
+ return {
282
+ member: getattr(self, member)
283
+ for member in self.member_schemas()
284
+ if getattr(self, member) is not None
285
+ }
@@ -0,0 +1,186 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from __future__ import annotations
5
+
6
+ from abc import ABCMeta
7
+ from copy import copy
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Self
10
+
11
+ import polars as pl
12
+
13
+ from ._rule import GroupRule, Rule, with_evaluation_rules
14
+ from .columns import Column
15
+ from .exc import ImplementationError, RuleImplementationError
16
+
17
+ _COLUMN_ATTR = "__dataframely_columns__"
18
+ _RULE_ATTR = "__dataframely_rules__"
19
+
20
+ # --------------------------------------- UTILS -------------------------------------- #
21
+
22
+
23
+ def _build_rules(
24
+ custom: dict[str, Rule], columns: dict[str, Column]
25
+ ) -> dict[str, Rule]:
26
+ # NOTE: Copy here to prevent in-place modification of the custom rules
27
+ rules: dict[str, Rule] = copy(custom)
28
+
29
+ # Add primary key validation to the list of rules if applicable
30
+ primary_keys = _primary_keys(columns)
31
+ if len(primary_keys) > 0:
32
+ rules["primary_key"] = Rule(~pl.struct(primary_keys).is_duplicated())
33
+
34
+ # Add column-specific rules
35
+ column_rules = {
36
+ f"{col_name}|{rule_name}": Rule(expr)
37
+ for col_name, column in columns.items()
38
+ for rule_name, expr in column.validation_rules(pl.col(col_name)).items()
39
+ }
40
+ rules.update(column_rules)
41
+
42
+ return rules
43
+
44
+
45
+ def _primary_keys(columns: dict[str, Column]) -> list[str]:
46
+ return list(k for k, col in columns.items() if col.primary_key)
47
+
48
+
49
+ # ------------------------------------------------------------------------------------ #
50
+ # SCHEMA META #
51
+ # ------------------------------------------------------------------------------------ #
52
+
53
+
54
+ @dataclass
55
+ class Metadata:
56
+ """Utility class to gather columns and rules associated with a schema."""
57
+
58
+ columns: dict[str, Column] = field(default_factory=dict)
59
+ rules: dict[str, Rule] = field(default_factory=dict)
60
+
61
+ def update(self, other: Self):
62
+ self.columns.update(other.columns)
63
+ self.rules.update(other.rules)
64
+
65
+
66
+ class SchemaMeta(ABCMeta):
67
+ def __new__(
68
+ mcs, # noqa: N804
69
+ name: str,
70
+ bases: tuple[type[object], ...],
71
+ namespace: dict[str, Any],
72
+ *args: Any,
73
+ **kwargs: Any,
74
+ ):
75
+ result = Metadata()
76
+ for base in bases:
77
+ result.update(mcs._get_metadata_recursively(base))
78
+ result.update(mcs._get_metadata(namespace))
79
+ namespace[_COLUMN_ATTR] = result.columns
80
+ namespace[_RULE_ATTR] = result.rules
81
+
82
+ # At this point, we already know all columns and custom rules. We want to run
83
+ # some checks...
84
+
85
+ # 1) Check that the column names clash with none of the rule names. To this end,
86
+ # we assume that users cast dtypes, i.e. additional rules for dtype casting
87
+ # are also checked.
88
+ all_column_names = set(result.columns)
89
+ all_rule_names = set(_build_rules(result.rules, result.columns).keys()) | set(
90
+ f"{col}|dtype" for col in result.columns
91
+ )
92
+ common_names = all_column_names & all_rule_names
93
+ if len(common_names) > 0:
94
+ common_list = ", ".join(sorted(f"'{col}'" for col in common_names))
95
+ raise ImplementationError(
96
+ "Rules and columns must not be named equally but found "
97
+ f"{len(common_names)} overlaps: {common_list}."
98
+ )
99
+
100
+ # 2) Check that the columns referenced in the group rules exist.
101
+ for name, rule in result.rules.items():
102
+ if isinstance(rule, GroupRule):
103
+ missing_columns = set(rule.group_columns) - set(result.columns)
104
+ if len(missing_columns) > 0:
105
+ missing_list = ", ".join(
106
+ sorted(f"'{col}'" for col in missing_columns)
107
+ )
108
+ raise ImplementationError(
109
+ f"Group validation rule '{name}' has been implemented "
110
+ f"incorrectly. It references {len(missing_columns)} columns "
111
+ f"which are not in the schema: {missing_list}."
112
+ )
113
+
114
+ # 3) Assuming that non-custom rules are implemented correctly, we check that all
115
+ # custom rules are _also_ implemented correctly by evaluating rules on an
116
+ # empty data frame and checking for the evaluated dtypes.
117
+ if len(result.rules) > 0:
118
+ lf_empty = pl.LazyFrame(
119
+ schema={name: col.dtype for name, col in result.columns.items()}
120
+ )
121
+ # NOTE: For some reason, `polars` does not yield correct dtypes when calling
122
+ # `collect_schema()`
123
+ schema = with_evaluation_rules(lf_empty, result.rules).collect().schema
124
+ for name, rule in result.rules.items():
125
+ dtype = schema[name]
126
+ if not isinstance(dtype, pl.Boolean):
127
+ raise RuleImplementationError(
128
+ name, dtype, isinstance(rule, GroupRule)
129
+ )
130
+
131
+ return super().__new__(mcs, name, bases, namespace, *args, **kwargs)
132
+
133
+ @staticmethod
134
+ def _get_metadata_recursively(kls: type[object]) -> Metadata:
135
+ result = Metadata()
136
+ for base in kls.__bases__:
137
+ result.update(SchemaMeta._get_metadata_recursively(base))
138
+ result.update(SchemaMeta._get_metadata(kls.__dict__)) # type: ignore
139
+ return result
140
+
141
+ @staticmethod
142
+ def _get_metadata(source: dict[str, Any]) -> Metadata:
143
+ result = Metadata()
144
+ for attr, value in {
145
+ k: v for k, v in source.items() if not k.startswith("__")
146
+ }.items():
147
+ if isinstance(value, Column):
148
+ if not value.alias:
149
+ value.alias = attr
150
+ result.columns[value.alias] = value
151
+ if isinstance(value, Rule):
152
+ # We must ensure that custom rules do not clash with internal rules.
153
+ if attr == "primary_key":
154
+ raise ImplementationError(
155
+ "Custom validation rule must not be named `primary_key`."
156
+ )
157
+ result.rules[attr] = value
158
+ return result
159
+
160
+
161
+ class BaseSchema(metaclass=SchemaMeta):
162
+ """Internal utility abstraction to reference schemas without introducing cyclical
163
+ dependencies."""
164
+
165
+ @classmethod
166
+ def column_names(cls) -> list[str]:
167
+ """The column names of this schema."""
168
+ return list(getattr(cls, _COLUMN_ATTR).keys())
169
+
170
+ @classmethod
171
+ def columns(cls) -> dict[str, Column]:
172
+ """The column definitions of this schema."""
173
+ return getattr(cls, _COLUMN_ATTR)
174
+
175
+ @classmethod
176
+ def primary_keys(cls) -> list[str]:
177
+ """The primary key columns in this schema (possibly empty)."""
178
+ return _primary_keys(cls.columns())
179
+
180
+ @classmethod
181
+ def _validation_rules(cls) -> dict[str, Rule]:
182
+ return _build_rules(cls._schema_validation_rules(), cls.columns())
183
+
184
+ @classmethod
185
+ def _schema_validation_rules(cls) -> dict[str, Rule]:
186
+ return getattr(cls, _RULE_ATTR)
dataframely/_compat.py ADDED
@@ -0,0 +1,39 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+
5
+ from typing import Any
6
+
7
+
8
+ class _DummyModule: # pragma: no cover
9
+ def __init__(self, module: str):
10
+ self.module = module
11
+
12
+ def __getattr__(self, name: str) -> Any:
13
+ raise ValueError(f"Module '{self.module}' is not installed.")
14
+
15
+
16
+ # ------------------------------------ SQLALCHEMY ------------------------------------ #
17
+
18
+ try:
19
+ import sqlalchemy as sa
20
+ import sqlalchemy.dialects.mssql as sa_mssql
21
+ from sqlalchemy.sql.type_api import TypeEngine as sa_TypeEngine
22
+ except ImportError: # pragma: no cover
23
+ sa = _DummyModule("sqlalchemy") # type: ignore
24
+ sa_mssql = _DummyModule("sqlalchemy") # type: ignore
25
+
26
+ class sa_TypeEngine: # type: ignore # noqa: N801
27
+ pass
28
+
29
+
30
+ # -------------------------------------- PYARROW ------------------------------------- #
31
+
32
+ try:
33
+ import pyarrow as pa
34
+ except ImportError: # pragma: no cover
35
+ pa = _DummyModule("pyarrow")
36
+
37
+ # ------------------------------------------------------------------------------------ #
38
+
39
+ __all__ = ["sa", "sa_mssql", "sa_TypeEngine", "pa"]
dataframely/_extre.pyd ADDED
Binary file
dataframely/_extre.pyi ADDED
@@ -0,0 +1,54 @@
1
+ from typing import Literal, overload
2
+
3
+ def matching_string_length(regex: str) -> tuple[int, int | None]:
4
+ """
5
+ Compute the minimum and maximum length (if available) of strings matching a regular expression.
6
+
7
+ Args:
8
+ regex: The regular expression to analyze. The regular expression must not
9
+ contain any lookaround operators.
10
+
11
+ Returns:
12
+ A tuple of the minimum of maximum length of the matching strings. While the minimum
13
+ length is guaranteed to be available, the maximum length may be ``None`` if ``regex``
14
+ matches strings of potentially infinite length (e.g. due to the use of ``+`` or ``*``).
15
+
16
+ Raises:
17
+ ValueError: If the regex cannot be parsed or analyzed.
18
+ """
19
+
20
+ @overload
21
+ def sample(
22
+ regex: str, n: int, max_repetitions: int = 16, seed: int | None = None
23
+ ) -> list[str]:
24
+ """
25
+ Sample a random (set of) string(s) matching the provided regular expression.
26
+
27
+ Args:
28
+ regex: The regular expression generated strings must match. The regular
29
+ expression must not contain any lookaround operators.
30
+ n: The number of random strings to generate or ``None`` if a single one should
31
+ be generated.
32
+ max_repetitions: The maximum number of repetitions for ``+`` and ``*``
33
+ quantifiers.
34
+ seed: The seed to use for the random sampling procedure.
35
+
36
+ Returns:
37
+ A single randomly generated string if ``n is None`` or a list of randomly
38
+ generated strings if ``n`` is an integer.
39
+
40
+ Raises:
41
+ ValueError: If the regex cannot be parsed.
42
+
43
+ Attention:
44
+ Using wildcards (i.e. ``.``) really means _any_ valid Unicode character.
45
+ Consider using more precise regular expressions if this is undesired.
46
+ """
47
+
48
+ @overload
49
+ def sample(
50
+ regex: str,
51
+ n: Literal[None] = None,
52
+ max_repetitions: int = 16,
53
+ seed: int | None = None,
54
+ ) -> str: ...
dataframely/_filter.py ADDED
@@ -0,0 +1,42 @@
1
+ # Copyright (c) QuantCo 2025-2025
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ from collections.abc import Callable
5
+ from typing import Generic, TypeVar
6
+
7
+ import polars as pl
8
+
9
+ C = TypeVar("C")
10
+
11
+
12
+ class Filter(Generic[C]):
13
+ """Internal class representing logic for filtering members of a collection."""
14
+
15
+ def __init__(self, logic: Callable[[C], pl.LazyFrame]):
16
+ self.logic = logic
17
+
18
+
19
+ def filter() -> Callable[[Callable[[C], pl.LazyFrame]], Filter[C]]:
20
+ """Mark a function as filters for rows in the members of a collection.
21
+
22
+ The name of the function will be used as the name of the filter. The name must not
23
+ clash with the name of any column in the member schemas or rules defined on the
24
+ member schemas.
25
+
26
+ A filter receives a collection as input and must return a data frame like the
27
+ following:
28
+
29
+ - The columns must be a superset of the common primary keys across all members.
30
+ - The rows must provide the primary keys which ought to be *kept* across the
31
+ members. The filter results in the removal of rows which are lost as the result
32
+ of inner-joining members onto the return value of this function.
33
+
34
+ Attention:
35
+ Make sure to provide unique combinations of the primary keys or the filters
36
+ might introduce duplicate rows.
37
+ """
38
+
39
+ def decorator(validation_fn: Callable[[C], pl.LazyFrame]) -> Filter[C]:
40
+ return Filter(logic=validation_fn)
41
+
42
+ return decorator