framespec 0.0.2__tar.gz → 0.0.3__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.
- framespec-0.0.3/PKG-INFO +76 -0
- framespec-0.0.3/README.md +64 -0
- {framespec-0.0.2 → framespec-0.0.3}/pyproject.toml +5 -1
- {framespec-0.0.2 → framespec-0.0.3}/pyproject.toml.orig +5 -1
- framespec-0.0.3/src/framespec/__init__.py +12 -0
- framespec-0.0.3/src/framespec/colspecs.py +451 -0
- framespec-0.0.3/src/framespec/decorator.py +21 -0
- framespec-0.0.3/src/framespec/spec.py +113 -0
- framespec-0.0.2/PKG-INFO +0 -42
- framespec-0.0.2/README.md +0 -30
- framespec-0.0.2/src/framespec/__init__.py +0 -2
- framespec-0.0.2/src/framespec/core/__init__.py +0 -0
- framespec-0.0.2/src/framespec/core/types.py +0 -225
framespec-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: framespec
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Declarative DataFrame specifications for PySpark.
|
|
5
|
+
Author: RyPy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Dist: pyspark>=4.0.0
|
|
8
|
+
Requires-Dist: packaging>=24.0
|
|
9
|
+
Requires-Dist: typing-extensions>=4.10.0
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# framespec
|
|
14
|
+
|
|
15
|
+
Declarative specifications for PySpark DataFrames.
|
|
16
|
+
|
|
17
|
+
Define frame-level metadata and column contracts in one place, then bind them
|
|
18
|
+
into a Spark schema and typed column references.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install framespec
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
from typing import final
|
|
30
|
+
|
|
31
|
+
from framespec import FrameSpec, framespec, spec
|
|
32
|
+
from framespec import colspecs as C
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@spec
|
|
36
|
+
class TableSpec(FrameSpec):
|
|
37
|
+
name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@final
|
|
41
|
+
@framespec
|
|
42
|
+
class Customer:
|
|
43
|
+
spec = TableSpec(name="customers")
|
|
44
|
+
|
|
45
|
+
customer_id = C.Integer(nullable=False)
|
|
46
|
+
name = C.String(min_length=1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Frame contract
|
|
50
|
+
Customer.spec.schema
|
|
51
|
+
|
|
52
|
+
# Column surfaces
|
|
53
|
+
Customer.name == "name"
|
|
54
|
+
Customer.name.spec.min_length
|
|
55
|
+
Customer.name.col.isNull()
|
|
56
|
+
df.select(Customer.name)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Mental model
|
|
60
|
+
|
|
61
|
+
| Access | Meaning |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `@spec` | Define a frozen contract type |
|
|
64
|
+
| `@framespec` | Bind a frame spec and its columns |
|
|
65
|
+
| `Customer.spec` | Frame-level contract |
|
|
66
|
+
| `Customer.name` | Column name (`str`-like) |
|
|
67
|
+
| `Customer.name.spec` | Column contract |
|
|
68
|
+
| `Customer.name.col` | Spark `Column` (`F.col(...)`) |
|
|
69
|
+
|
|
70
|
+
Extend built-in column types (`C.String`, `C.Integer`, …) with `@spec` for
|
|
71
|
+
domain-specific constraints. The name `spec` is reserved on declaration
|
|
72
|
+
classes for the frame spec.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# framespec
|
|
2
|
+
|
|
3
|
+
Declarative specifications for PySpark DataFrames.
|
|
4
|
+
|
|
5
|
+
Define frame-level metadata and column contracts in one place, then bind them
|
|
6
|
+
into a Spark schema and typed column references.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install framespec
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
from typing import final
|
|
18
|
+
|
|
19
|
+
from framespec import FrameSpec, framespec, spec
|
|
20
|
+
from framespec import colspecs as C
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@spec
|
|
24
|
+
class TableSpec(FrameSpec):
|
|
25
|
+
name: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@final
|
|
29
|
+
@framespec
|
|
30
|
+
class Customer:
|
|
31
|
+
spec = TableSpec(name="customers")
|
|
32
|
+
|
|
33
|
+
customer_id = C.Integer(nullable=False)
|
|
34
|
+
name = C.String(min_length=1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Frame contract
|
|
38
|
+
Customer.spec.schema
|
|
39
|
+
|
|
40
|
+
# Column surfaces
|
|
41
|
+
Customer.name == "name"
|
|
42
|
+
Customer.name.spec.min_length
|
|
43
|
+
Customer.name.col.isNull()
|
|
44
|
+
df.select(Customer.name)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Mental model
|
|
48
|
+
|
|
49
|
+
| Access | Meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `@spec` | Define a frozen contract type |
|
|
52
|
+
| `@framespec` | Bind a frame spec and its columns |
|
|
53
|
+
| `Customer.spec` | Frame-level contract |
|
|
54
|
+
| `Customer.name` | Column name (`str`-like) |
|
|
55
|
+
| `Customer.name.spec` | Column contract |
|
|
56
|
+
| `Customer.name.col` | Spark `Column` (`F.col(...)`) |
|
|
57
|
+
|
|
58
|
+
Extend built-in column types (`C.String`, `C.Integer`, …) with `@spec` for
|
|
59
|
+
domain-specific constraints. The name `spec` is reserved on declaration
|
|
60
|
+
classes for the frame spec.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "framespec"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "Declarative DataFrame specifications for PySpark."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -16,6 +16,7 @@ name = "RyPy"
|
|
|
16
16
|
|
|
17
17
|
[dependency-groups]
|
|
18
18
|
dev = [
|
|
19
|
+
"basedpyright >= 1.29.0",
|
|
19
20
|
"pytest >= 8.0",
|
|
20
21
|
"pytest-cov >= 5.0",
|
|
21
22
|
"mypy >= 1.10",
|
|
@@ -25,3 +26,6 @@ dev = [
|
|
|
25
26
|
[build-system]
|
|
26
27
|
requires = ["uv-build"]
|
|
27
28
|
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[tool.basedpyright]
|
|
31
|
+
reportUnannotatedClassAttribute = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "framespec"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "Declarative DataFrame specifications for PySpark."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "RyPy" }]
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
|
|
16
16
|
[dependency-groups]
|
|
17
17
|
dev = [
|
|
18
|
+
"basedpyright >= 1.29.0",
|
|
18
19
|
"pytest >= 8.0",
|
|
19
20
|
"pytest-cov >= 5.0",
|
|
20
21
|
"mypy >= 1.10",
|
|
@@ -24,3 +25,6 @@ dev = [
|
|
|
24
25
|
[build-system]
|
|
25
26
|
requires = ["uv-build"]
|
|
26
27
|
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.basedpyright]
|
|
30
|
+
reportUnannotatedClassAttribute = "none"
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from typing import override
|
|
6
|
+
|
|
7
|
+
from pyspark.sql import types as T
|
|
8
|
+
|
|
9
|
+
from .spec import ColRef, ColSpec, spec
|
|
10
|
+
|
|
11
|
+
type NestedDType = ColSpec | ColRef[ColSpec] | T.DataType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve_dtype(element: NestedDType) -> T.DataType:
|
|
15
|
+
if isinstance(element, ColSpec):
|
|
16
|
+
return element.dtype
|
|
17
|
+
if isinstance(element, ColRef):
|
|
18
|
+
return element.spec.dtype
|
|
19
|
+
return element
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_non_negative(name: str, value: int | None) -> None:
|
|
23
|
+
if value is not None and value < 0:
|
|
24
|
+
raise ValueError(f"{name} cannot be negative")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _validate_integral_bounds(
|
|
28
|
+
*,
|
|
29
|
+
min_value: int | None,
|
|
30
|
+
max_value: int | None,
|
|
31
|
+
exclusive_min: int | None,
|
|
32
|
+
exclusive_max: int | None,
|
|
33
|
+
) -> None:
|
|
34
|
+
if min_value is not None and max_value is not None and min_value > max_value:
|
|
35
|
+
raise ValueError("min_value cannot be greater than max_value")
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
exclusive_min is not None
|
|
39
|
+
and exclusive_max is not None
|
|
40
|
+
and exclusive_min >= exclusive_max
|
|
41
|
+
):
|
|
42
|
+
raise ValueError("exclusive_min must be less than exclusive_max")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_fractional_bounds(
|
|
46
|
+
*,
|
|
47
|
+
min_value: float | None,
|
|
48
|
+
max_value: float | None,
|
|
49
|
+
exclusive_min: float | None,
|
|
50
|
+
exclusive_max: float | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if min_value is not None and max_value is not None and min_value > max_value:
|
|
53
|
+
raise ValueError("min_value cannot be greater than max_value")
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
exclusive_min is not None
|
|
57
|
+
and exclusive_max is not None
|
|
58
|
+
and exclusive_min >= exclusive_max
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("exclusive_min must be less than exclusive_max")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- String ------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@spec
|
|
67
|
+
class String(ColSpec):
|
|
68
|
+
min_length: int | None = None
|
|
69
|
+
max_length: int | None = None
|
|
70
|
+
allowed_values: tuple[str, ...] | None = None
|
|
71
|
+
pattern: str | None = None
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
if self.allowed_values is not None:
|
|
75
|
+
object.__setattr__(self, "allowed_values", tuple(self.allowed_values))
|
|
76
|
+
|
|
77
|
+
_validate_non_negative("min_length", self.min_length)
|
|
78
|
+
_validate_non_negative("max_length", self.max_length)
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
self.min_length is not None
|
|
82
|
+
and self.max_length is not None
|
|
83
|
+
and self.min_length > self.max_length
|
|
84
|
+
):
|
|
85
|
+
raise ValueError("min_length cannot be greater than max_length")
|
|
86
|
+
|
|
87
|
+
super().__post_init__()
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@override
|
|
91
|
+
def dtype(self) -> T.StringType:
|
|
92
|
+
return T.StringType()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@spec
|
|
96
|
+
class Char(ColSpec):
|
|
97
|
+
length: int = 1
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
if self.length < 1:
|
|
101
|
+
raise ValueError("length must be at least 1")
|
|
102
|
+
|
|
103
|
+
super().__post_init__()
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
@override
|
|
107
|
+
def dtype(self) -> T.CharType:
|
|
108
|
+
return T.CharType(self.length)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@spec
|
|
112
|
+
class Varchar(ColSpec):
|
|
113
|
+
length: int = 1
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
if self.length < 1:
|
|
117
|
+
raise ValueError("length must be at least 1")
|
|
118
|
+
|
|
119
|
+
super().__post_init__()
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
@override
|
|
123
|
+
def dtype(self) -> T.VarcharType:
|
|
124
|
+
return T.VarcharType(self.length)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- Boolean -----------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@spec
|
|
131
|
+
class Boolean(ColSpec):
|
|
132
|
+
@property
|
|
133
|
+
@override
|
|
134
|
+
def dtype(self) -> T.BooleanType:
|
|
135
|
+
return T.BooleanType()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --- Binary ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@spec
|
|
142
|
+
class Binary(ColSpec):
|
|
143
|
+
min_length: int | None = None
|
|
144
|
+
max_length: int | None = None
|
|
145
|
+
content_encoding: str | None = None
|
|
146
|
+
content_media_type: str | None = None
|
|
147
|
+
|
|
148
|
+
def __post_init__(self) -> None:
|
|
149
|
+
_validate_non_negative("min_length", self.min_length)
|
|
150
|
+
_validate_non_negative("max_length", self.max_length)
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
self.min_length is not None
|
|
154
|
+
and self.max_length is not None
|
|
155
|
+
and self.min_length > self.max_length
|
|
156
|
+
):
|
|
157
|
+
raise ValueError("min_length cannot be greater than max_length")
|
|
158
|
+
|
|
159
|
+
super().__post_init__()
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
@override
|
|
163
|
+
def dtype(self) -> T.BinaryType:
|
|
164
|
+
return T.BinaryType()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --- Numeric -----------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@spec
|
|
171
|
+
class _IntegralColSpec(ColSpec, ABC):
|
|
172
|
+
min_value: int | None = None
|
|
173
|
+
max_value: int | None = None
|
|
174
|
+
exclusive_min: int | None = None
|
|
175
|
+
exclusive_max: int | None = None
|
|
176
|
+
multiple_of: int | None = None
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
_validate_integral_bounds(
|
|
180
|
+
min_value=self.min_value,
|
|
181
|
+
max_value=self.max_value,
|
|
182
|
+
exclusive_min=self.exclusive_min,
|
|
183
|
+
exclusive_max=self.exclusive_max,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if self.multiple_of is not None and self.multiple_of <= 0:
|
|
187
|
+
raise ValueError("multiple_of must be positive")
|
|
188
|
+
|
|
189
|
+
super().__post_init__()
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
@abstractmethod
|
|
193
|
+
@override
|
|
194
|
+
def dtype(self) -> T.DataType: ...
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@spec
|
|
198
|
+
class Byte(_IntegralColSpec):
|
|
199
|
+
@property
|
|
200
|
+
@override
|
|
201
|
+
def dtype(self) -> T.ByteType:
|
|
202
|
+
return T.ByteType()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@spec
|
|
206
|
+
class Short(_IntegralColSpec):
|
|
207
|
+
@property
|
|
208
|
+
@override
|
|
209
|
+
def dtype(self) -> T.ShortType:
|
|
210
|
+
return T.ShortType()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@spec
|
|
214
|
+
class Integer(_IntegralColSpec):
|
|
215
|
+
@property
|
|
216
|
+
@override
|
|
217
|
+
def dtype(self) -> T.IntegerType:
|
|
218
|
+
return T.IntegerType()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@spec
|
|
222
|
+
class Long(_IntegralColSpec):
|
|
223
|
+
@property
|
|
224
|
+
@override
|
|
225
|
+
def dtype(self) -> T.LongType:
|
|
226
|
+
return T.LongType()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@spec
|
|
230
|
+
class _FractionalColSpec(ColSpec, ABC):
|
|
231
|
+
min_value: float | None = None
|
|
232
|
+
max_value: float | None = None
|
|
233
|
+
exclusive_min: float | None = None
|
|
234
|
+
exclusive_max: float | None = None
|
|
235
|
+
multiple_of: float | None = None
|
|
236
|
+
|
|
237
|
+
def __post_init__(self) -> None:
|
|
238
|
+
_validate_fractional_bounds(
|
|
239
|
+
min_value=self.min_value,
|
|
240
|
+
max_value=self.max_value,
|
|
241
|
+
exclusive_min=self.exclusive_min,
|
|
242
|
+
exclusive_max=self.exclusive_max,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if self.multiple_of is not None and self.multiple_of <= 0:
|
|
246
|
+
raise ValueError("multiple_of must be positive")
|
|
247
|
+
|
|
248
|
+
super().__post_init__()
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
@abstractmethod
|
|
252
|
+
@override
|
|
253
|
+
def dtype(self) -> T.DataType: ...
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@spec
|
|
257
|
+
class Float(_FractionalColSpec):
|
|
258
|
+
@property
|
|
259
|
+
@override
|
|
260
|
+
def dtype(self) -> T.FloatType:
|
|
261
|
+
return T.FloatType()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@spec
|
|
265
|
+
class Double(_FractionalColSpec):
|
|
266
|
+
@property
|
|
267
|
+
@override
|
|
268
|
+
def dtype(self) -> T.DoubleType:
|
|
269
|
+
return T.DoubleType()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@spec
|
|
273
|
+
class Decimal(ColSpec):
|
|
274
|
+
precision: int = 10
|
|
275
|
+
scale: int = 0
|
|
276
|
+
min_value: float | None = None
|
|
277
|
+
max_value: float | None = None
|
|
278
|
+
exclusive_min: float | None = None
|
|
279
|
+
exclusive_max: float | None = None
|
|
280
|
+
multiple_of: float | None = None
|
|
281
|
+
|
|
282
|
+
def __post_init__(self) -> None:
|
|
283
|
+
if self.precision < 1:
|
|
284
|
+
raise ValueError("precision must be at least 1")
|
|
285
|
+
|
|
286
|
+
if self.scale < 0:
|
|
287
|
+
raise ValueError("scale cannot be negative")
|
|
288
|
+
|
|
289
|
+
if self.scale > self.precision:
|
|
290
|
+
raise ValueError("scale cannot be greater than precision")
|
|
291
|
+
|
|
292
|
+
_validate_fractional_bounds(
|
|
293
|
+
min_value=self.min_value,
|
|
294
|
+
max_value=self.max_value,
|
|
295
|
+
exclusive_min=self.exclusive_min,
|
|
296
|
+
exclusive_max=self.exclusive_max,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if self.multiple_of is not None and self.multiple_of <= 0:
|
|
300
|
+
raise ValueError("multiple_of must be positive")
|
|
301
|
+
|
|
302
|
+
super().__post_init__()
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
@override
|
|
306
|
+
def dtype(self) -> T.DecimalType:
|
|
307
|
+
return T.DecimalType(self.precision, self.scale)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --- Temporal ----------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@spec
|
|
314
|
+
class _TemporalColSpec(ColSpec, ABC):
|
|
315
|
+
minimum: str | None = None
|
|
316
|
+
maximum: str | None = None
|
|
317
|
+
format: str | None = None
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
@abstractmethod
|
|
321
|
+
@override
|
|
322
|
+
def dtype(self) -> T.DataType: ...
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@spec
|
|
326
|
+
class Date(_TemporalColSpec):
|
|
327
|
+
@property
|
|
328
|
+
@override
|
|
329
|
+
def dtype(self) -> T.DateType:
|
|
330
|
+
return T.DateType()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@spec
|
|
334
|
+
class Timestamp(_TemporalColSpec):
|
|
335
|
+
@property
|
|
336
|
+
@override
|
|
337
|
+
def dtype(self) -> T.TimestampType:
|
|
338
|
+
return T.TimestampType()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@spec
|
|
342
|
+
class TimestampNTZ(_TemporalColSpec):
|
|
343
|
+
@property
|
|
344
|
+
@override
|
|
345
|
+
def dtype(self) -> T.TimestampNTZType:
|
|
346
|
+
return T.TimestampNTZType()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --- Collection --------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@spec
|
|
353
|
+
class Array(ColSpec):
|
|
354
|
+
element: NestedDType = field(kw_only=True) # pyright: ignore[reportAny]
|
|
355
|
+
contains_null: bool = True
|
|
356
|
+
min_items: int | None = None
|
|
357
|
+
max_items: int | None = None
|
|
358
|
+
unique_items: bool | None = None
|
|
359
|
+
|
|
360
|
+
def __post_init__(self) -> None:
|
|
361
|
+
_validate_non_negative("min_items", self.min_items)
|
|
362
|
+
_validate_non_negative("max_items", self.max_items)
|
|
363
|
+
|
|
364
|
+
if (
|
|
365
|
+
self.min_items is not None
|
|
366
|
+
and self.max_items is not None
|
|
367
|
+
and self.min_items > self.max_items
|
|
368
|
+
):
|
|
369
|
+
raise ValueError("min_items cannot be greater than max_items")
|
|
370
|
+
|
|
371
|
+
super().__post_init__()
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
@override
|
|
375
|
+
def dtype(self) -> T.ArrayType:
|
|
376
|
+
return T.ArrayType(_resolve_dtype(self.element), self.contains_null)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@spec
|
|
380
|
+
class Map(ColSpec):
|
|
381
|
+
key: NestedDType = field(kw_only=True) # pyright: ignore[reportAny]
|
|
382
|
+
value: NestedDType = field(kw_only=True) # pyright: ignore[reportAny]
|
|
383
|
+
value_contains_null: bool = True
|
|
384
|
+
min_properties: int | None = None
|
|
385
|
+
max_properties: int | None = None
|
|
386
|
+
|
|
387
|
+
def __post_init__(self) -> None:
|
|
388
|
+
_validate_non_negative("min_properties", self.min_properties)
|
|
389
|
+
_validate_non_negative("max_properties", self.max_properties)
|
|
390
|
+
|
|
391
|
+
if (
|
|
392
|
+
self.min_properties is not None
|
|
393
|
+
and self.max_properties is not None
|
|
394
|
+
and self.min_properties > self.max_properties
|
|
395
|
+
):
|
|
396
|
+
raise ValueError("min_properties cannot be greater than max_properties")
|
|
397
|
+
|
|
398
|
+
super().__post_init__()
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
@override
|
|
402
|
+
def dtype(self) -> T.MapType:
|
|
403
|
+
return T.MapType(
|
|
404
|
+
_resolve_dtype(self.key),
|
|
405
|
+
_resolve_dtype(self.value),
|
|
406
|
+
self.value_contains_null,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# --- Interval ----------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@spec
|
|
414
|
+
class DayTimeInterval(ColSpec):
|
|
415
|
+
start_field: int | None = None
|
|
416
|
+
end_field: int | None = None
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
@override
|
|
420
|
+
def dtype(self) -> T.DayTimeIntervalType:
|
|
421
|
+
return T.DayTimeIntervalType(self.start_field, self.end_field)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@spec
|
|
425
|
+
class YearMonthInterval(ColSpec):
|
|
426
|
+
start_field: int | None = None
|
|
427
|
+
end_field: int | None = None
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
@override
|
|
431
|
+
def dtype(self) -> T.YearMonthIntervalType:
|
|
432
|
+
return T.YearMonthIntervalType(self.start_field, self.end_field)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# --- Other -------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@spec
|
|
439
|
+
class Null(ColSpec):
|
|
440
|
+
@property
|
|
441
|
+
@override
|
|
442
|
+
def dtype(self) -> T.NullType:
|
|
443
|
+
return T.NullType()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@spec
|
|
447
|
+
class Variant(ColSpec):
|
|
448
|
+
@property
|
|
449
|
+
@override
|
|
450
|
+
def dtype(self) -> T.VariantType:
|
|
451
|
+
return T.VariantType()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from .spec import ColSpec, FrameSpec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def discover_colspecs(cls: type[object]) -> tuple[ColSpec, ...]:
|
|
8
|
+
attributes = cast(Mapping[str, object], cls.__dict__)
|
|
9
|
+
return tuple(value for value in attributes.values() if isinstance(value, ColSpec))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def framespec[T](cls: type[T]) -> type[T]:
|
|
13
|
+
spec = cls.__dict__.get("spec")
|
|
14
|
+
|
|
15
|
+
if not isinstance(spec, FrameSpec):
|
|
16
|
+
raise TypeError(
|
|
17
|
+
f"{cls.__name__} must define a 'spec' attribute as a FrameSpec instance."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
object.__setattr__(spec, "_colspecs", discover_colspecs(cls))
|
|
21
|
+
return cls
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Self, cast, dataclass_transform
|
|
8
|
+
|
|
9
|
+
from pyspark.sql import Column
|
|
10
|
+
from pyspark.sql import functions as F
|
|
11
|
+
from pyspark.sql import types as T
|
|
12
|
+
|
|
13
|
+
type Metadata = Mapping[str, object]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass_transform(frozen_default=True)
|
|
17
|
+
def spec[T](cls: type[T]) -> type[T]:
|
|
18
|
+
return dataclass(frozen=True, kw_only=True)(cls)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ColRef[T: ColSpec](str):
|
|
22
|
+
"""String-like column name with contract and Spark column accessors."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("_spec",)
|
|
25
|
+
|
|
26
|
+
def __new__(cls, name: str, column_spec: T, /) -> Self:
|
|
27
|
+
instance = str.__new__(cls, name)
|
|
28
|
+
object.__setattr__(instance, "_spec", column_spec)
|
|
29
|
+
return instance
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def spec(self) -> T:
|
|
33
|
+
return self._spec
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def col(self) -> Column:
|
|
37
|
+
return F.col(str(self))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@spec
|
|
41
|
+
class ColSpec(ABC):
|
|
42
|
+
nullable: bool = True
|
|
43
|
+
description: str | None = None
|
|
44
|
+
metadata: Metadata | None = None
|
|
45
|
+
name: str | None = field(default=None, init=False, compare=False)
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.metadata is not None:
|
|
49
|
+
object.__setattr__(
|
|
50
|
+
self,
|
|
51
|
+
"metadata",
|
|
52
|
+
MappingProxyType(dict(self.metadata)),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __set_name__(self, owner: type[object], name: str) -> None:
|
|
56
|
+
if name == "spec":
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"{owner.__name__} cannot declare a column named 'spec'; that name is reserved for the frame spec."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
object.__setattr__(self, "name", name)
|
|
62
|
+
|
|
63
|
+
def __get__(
|
|
64
|
+
self,
|
|
65
|
+
obj: object | None,
|
|
66
|
+
owner: type[object] | None = None,
|
|
67
|
+
) -> ColRef[Self]:
|
|
68
|
+
if owner is None:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
f"{type(self).__name__} must be accessed from a declaring class."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if self.name is None:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
f"{type(self).__name__} is not bound to a class attribute."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return ColRef(self.name, self)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def dtype(self) -> T.DataType: ...
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def field(self) -> T.StructField:
|
|
86
|
+
if self.name is None:
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"ColSpec must be declared as a class attribute before field is accessed."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return T.StructField(
|
|
92
|
+
self.name,
|
|
93
|
+
self.dtype,
|
|
94
|
+
self.nullable,
|
|
95
|
+
dict(self.metadata) if self.metadata else {},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@spec
|
|
100
|
+
class FrameSpec:
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
object.__setattr__(self, "_colspecs", ())
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def colspecs(self) -> tuple[ColSpec, ...]:
|
|
106
|
+
return cast(
|
|
107
|
+
tuple[ColSpec, ...],
|
|
108
|
+
object.__getattribute__(self, "_colspecs"),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def schema(self) -> T.StructType:
|
|
113
|
+
return T.StructType([column.field for column in self.colspecs])
|
framespec-0.0.2/PKG-INFO
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: framespec
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: Declarative DataFrame specifications for PySpark.
|
|
5
|
-
Author: RyPy
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Dist: pyspark>=4.0.0
|
|
8
|
-
Requires-Dist: packaging>=24.0
|
|
9
|
-
Requires-Dist: typing-extensions>=4.10.0
|
|
10
|
-
Requires-Python: >=3.12
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
> 🛠️ Currently under construction...
|
|
14
|
-
|
|
15
|
-
# framespec
|
|
16
|
-
|
|
17
|
-
Declarative specifications for PySpark DataFrames.
|
|
18
|
-
|
|
19
|
-
`framespec` lets you define column types and attach reusable expressions
|
|
20
|
-
that capture validation rules, invariants, and metrics. Specs can be applied
|
|
21
|
-
to any DataFrame—intermediate, transformed, or table-backed.
|
|
22
|
-
|
|
23
|
-
## Installation
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pip install framespec
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Example
|
|
30
|
-
|
|
31
|
-
```py
|
|
32
|
-
from framespec import framespec, Int, Timestamp, greater_than
|
|
33
|
-
|
|
34
|
-
@framespec
|
|
35
|
-
class Events:
|
|
36
|
-
start: Timestamp
|
|
37
|
-
end: Timestamp(expressions=[greater_than(start)])
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## License
|
|
41
|
-
|
|
42
|
-
MIT
|
framespec-0.0.2/README.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
> 🛠️ Currently under construction...
|
|
2
|
-
|
|
3
|
-
# framespec
|
|
4
|
-
|
|
5
|
-
Declarative specifications for PySpark DataFrames.
|
|
6
|
-
|
|
7
|
-
`framespec` lets you define column types and attach reusable expressions
|
|
8
|
-
that capture validation rules, invariants, and metrics. Specs can be applied
|
|
9
|
-
to any DataFrame—intermediate, transformed, or table-backed.
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pip install framespec
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Example
|
|
18
|
-
|
|
19
|
-
```py
|
|
20
|
-
from framespec import framespec, Int, Timestamp, greater_than
|
|
21
|
-
|
|
22
|
-
@framespec
|
|
23
|
-
class Events:
|
|
24
|
-
start: Timestamp
|
|
25
|
-
end: Timestamp(expressions=[greater_than(start)])
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## License
|
|
29
|
-
|
|
30
|
-
MIT
|
|
File without changes
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from pyspark.sql import types as T
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# -- Base Type --------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
@dataclass(frozen=True, kw_only=True)
|
|
8
|
-
class ColumnType:
|
|
9
|
-
"""
|
|
10
|
-
Base class for all framespec column types.
|
|
11
|
-
|
|
12
|
-
Declarative only:
|
|
13
|
-
- immutable
|
|
14
|
-
- engine-agnostic except for .dtype (Spark DataType)
|
|
15
|
-
"""
|
|
16
|
-
nullable: bool = True
|
|
17
|
-
|
|
18
|
-
@property
|
|
19
|
-
def dtype(self) -> T.DataType:
|
|
20
|
-
raise NotImplementedError
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# -- Integer Types ----------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
@dataclass(frozen=True, kw_only=True)
|
|
26
|
-
class Byte(ColumnType):
|
|
27
|
-
min_value: int | None = None
|
|
28
|
-
max_value: int | None = None
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def dtype(self) -> T.ByteType:
|
|
32
|
-
return T.ByteType()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass(frozen=True, kw_only=True)
|
|
36
|
-
class Short(ColumnType):
|
|
37
|
-
min_value: int | None = None
|
|
38
|
-
max_value: int | None = None
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def dtype(self) -> T.ShortType:
|
|
42
|
-
return T.ShortType()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@dataclass(frozen=True, kw_only=True)
|
|
46
|
-
class Int(ColumnType):
|
|
47
|
-
min_value: int | None = None
|
|
48
|
-
max_value: int | None = None
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def dtype(self) -> T.IntegerType:
|
|
52
|
-
return T.IntegerType()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@dataclass(frozen=True, kw_only=True)
|
|
56
|
-
class Long(ColumnType):
|
|
57
|
-
min_value: int | None = None
|
|
58
|
-
max_value: int | None = None
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def dtype(self) -> T.LongType:
|
|
62
|
-
return T.LongType()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# -- Floating Types ---------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
@dataclass(frozen=True, kw_only=True)
|
|
68
|
-
class Float(ColumnType):
|
|
69
|
-
min_value: float | None = None
|
|
70
|
-
max_value: float | None = None
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def dtype(self) -> T.FloatType:
|
|
74
|
-
return T.FloatType()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@dataclass(frozen=True, kw_only=True)
|
|
78
|
-
class Double(ColumnType):
|
|
79
|
-
min_value: float | None = None
|
|
80
|
-
max_value: float | None = None
|
|
81
|
-
|
|
82
|
-
@property
|
|
83
|
-
def dtype(self) -> T.DoubleType:
|
|
84
|
-
return T.DoubleType()
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# -- Decimal Type -----------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
@dataclass(frozen=True, kw_only=True)
|
|
90
|
-
class Decimal(ColumnType):
|
|
91
|
-
precision: int
|
|
92
|
-
scale: int = 0
|
|
93
|
-
min_value: float | None = None
|
|
94
|
-
max_value: float | None = None
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def dtype(self) -> T.DecimalType:
|
|
98
|
-
return T.DecimalType(self.precision, self.scale)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# -- String & Binary Types --------------------------------------------------
|
|
102
|
-
|
|
103
|
-
@dataclass(frozen=True, kw_only=True)
|
|
104
|
-
class String(ColumnType):
|
|
105
|
-
min_length: int | None = None
|
|
106
|
-
max_length: int | None = None
|
|
107
|
-
pattern: str | None = None
|
|
108
|
-
allowed_values: list[str] | None = None
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def dtype(self) -> T.StringType:
|
|
112
|
-
return T.StringType()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@dataclass(frozen=True, kw_only=True)
|
|
116
|
-
class Binary(ColumnType):
|
|
117
|
-
min_length: int | None = None
|
|
118
|
-
max_length: int | None = None
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
def dtype(self) -> T.BinaryType:
|
|
122
|
-
return T.BinaryType()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# -- Boolean Type -----------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
@dataclass(frozen=True, kw_only=True)
|
|
128
|
-
class Boolean(ColumnType):
|
|
129
|
-
@property
|
|
130
|
-
def dtype(self) -> T.BooleanType:
|
|
131
|
-
return T.BooleanType()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# -- Date & Timestamp Types -------------------------------------------------
|
|
135
|
-
|
|
136
|
-
@dataclass(frozen=True, kw_only=True)
|
|
137
|
-
class Date(ColumnType):
|
|
138
|
-
@property
|
|
139
|
-
def dtype(self) -> T.DateType:
|
|
140
|
-
return T.DateType()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@dataclass(frozen=True, kw_only=True)
|
|
144
|
-
class Timestamp(ColumnType):
|
|
145
|
-
@property
|
|
146
|
-
def dtype(self) -> T.TimestampType:
|
|
147
|
-
return T.TimestampType()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@dataclass(frozen=True, kw_only=True)
|
|
151
|
-
class TimestampNTZ(ColumnType):
|
|
152
|
-
@property
|
|
153
|
-
def dtype(self) -> T.TimestampNTZType:
|
|
154
|
-
return T.TimestampNTZType()
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# -- Complex Types: Array, Map, Struct -------------------------------------
|
|
158
|
-
|
|
159
|
-
@dataclass(frozen=True, kw_only=True)
|
|
160
|
-
class Array(ColumnType):
|
|
161
|
-
element_type: ColumnType
|
|
162
|
-
min_items: int | None = None
|
|
163
|
-
max_items: int | None = None
|
|
164
|
-
unique_items: bool = False
|
|
165
|
-
|
|
166
|
-
@property
|
|
167
|
-
def dtype(self) -> T.ArrayType:
|
|
168
|
-
return T.ArrayType(
|
|
169
|
-
elementType=self.element_type.dtype,
|
|
170
|
-
containsNull=self.element_type.nullable,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@dataclass(frozen=True, kw_only=True)
|
|
175
|
-
class Map(ColumnType):
|
|
176
|
-
key_type: ColumnType
|
|
177
|
-
value_type: ColumnType
|
|
178
|
-
min_properties: int | None = None
|
|
179
|
-
max_properties: int | None = None
|
|
180
|
-
|
|
181
|
-
@property
|
|
182
|
-
def dtype(self) -> T.MapType:
|
|
183
|
-
return T.MapType(
|
|
184
|
-
keyType=self.key_type.dtype,
|
|
185
|
-
valueType=self.value_type.dtype,
|
|
186
|
-
valueContainsNull=self.value_type.nullable,
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@dataclass(frozen=True, kw_only=True)
|
|
191
|
-
class Struct(ColumnType):
|
|
192
|
-
fields: dict[str, ColumnType]
|
|
193
|
-
required: list[str] | None = None
|
|
194
|
-
min_properties: int | None = None
|
|
195
|
-
max_properties: int | None = None
|
|
196
|
-
|
|
197
|
-
@property
|
|
198
|
-
def dtype(self) -> T.StructType:
|
|
199
|
-
return T.StructType([
|
|
200
|
-
T.StructField(name, type_.dtype, type_.nullable)
|
|
201
|
-
for name, type_ in self.fields.items()
|
|
202
|
-
])
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# -- Special Spark Types ----------------------------------------------------
|
|
206
|
-
|
|
207
|
-
@dataclass(frozen=True, kw_only=True)
|
|
208
|
-
class Null(ColumnType):
|
|
209
|
-
@property
|
|
210
|
-
def dtype(self) -> T.NullType:
|
|
211
|
-
return T.NullType()
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@dataclass(frozen=True, kw_only=True)
|
|
215
|
-
class CalendarInterval(ColumnType):
|
|
216
|
-
@property
|
|
217
|
-
def dtype(self) -> T.CalendarIntervalType:
|
|
218
|
-
return T.CalendarIntervalType()
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@dataclass(frozen=True, kw_only=True)
|
|
222
|
-
class DayTimeInterval(ColumnType):
|
|
223
|
-
@property
|
|
224
|
-
def dtype(self) -> T.DayTimeIntervalType:
|
|
225
|
-
return T.DayTimeIntervalType()
|