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.
@@ -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.2"
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.2"
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,12 @@
1
+ from . import colspecs
2
+ from .decorator import framespec
3
+ from .spec import ColRef, ColSpec, FrameSpec, spec
4
+
5
+ __all__ = [
6
+ "ColRef",
7
+ "ColSpec",
8
+ "FrameSpec",
9
+ "colspecs",
10
+ "framespec",
11
+ "spec",
12
+ ]
@@ -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
@@ -1,2 +0,0 @@
1
- def main() -> None:
2
- print("Hello from framespec!")
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()