framespec 0.0.1__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
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "framespec"
3
+ version = "0.0.3"
4
+ description = "Declarative DataFrame specifications for PySpark."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ dependencies = [
9
+ "pyspark >= 4.0.0",
10
+ "packaging >= 24.0",
11
+ "typing-extensions >= 4.10.0",
12
+ ]
13
+
14
+ [[project.authors]]
15
+ name = "RyPy"
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "basedpyright >= 1.29.0",
20
+ "pytest >= 8.0",
21
+ "pytest-cov >= 5.0",
22
+ "mypy >= 1.10",
23
+ "ruff >= 0.5.0",
24
+ ]
25
+
26
+ [build-system]
27
+ requires = ["uv-build"]
28
+ build-backend = "uv_build"
29
+
30
+ [tool.basedpyright]
31
+ reportUnannotatedClassAttribute = "none"
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "framespec"
3
+ version = "0.0.3"
4
+ description = "Declarative DataFrame specifications for PySpark."
5
+ readme = "README.md"
6
+ authors = [{ name = "RyPy" }]
7
+ requires-python = ">=3.12"
8
+ license = "MIT"
9
+
10
+ dependencies = [
11
+ "pyspark >= 4.0.0",
12
+ "packaging >= 24.0",
13
+ "typing-extensions >= 4.10.0",
14
+ ]
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "basedpyright >= 1.29.0",
19
+ "pytest >= 8.0",
20
+ "pytest-cov >= 5.0",
21
+ "mypy >= 1.10",
22
+ "ruff >= 0.5.0",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["uv-build"]
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.1/PKG-INFO DELETED
@@ -1,38 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: framespec
3
- Version: 0.0.1
4
- Summary: Declarative DataFrame specifications for PySpark.
5
- Author: Ryan
6
- Requires-Python: >=3.12
7
- Description-Content-Type: text/markdown
8
-
9
- > 🛠️ Currently under construction...
10
-
11
- # framespec
12
-
13
- Declarative specifications for PySpark DataFrames.
14
-
15
- `framespec` lets you define column types and attach reusable expressions
16
- that capture validation rules, invariants, and metrics. Specs can be applied
17
- to any DataFrame—intermediate, transformed, or table-backed.
18
-
19
- ## Installation
20
-
21
- ```bash
22
- pip install framespec
23
- ```
24
-
25
- ## Example
26
-
27
- ```py
28
- from framespec import framespec, Int, Timestamp, greater_than
29
-
30
- @framespec
31
- class Events:
32
- start: Timestamp
33
- end: Timestamp(expressions=[greater_than(start)])
34
- ```
35
-
36
- ## License
37
-
38
- MIT
framespec-0.0.1/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,14 +0,0 @@
1
- [project]
2
- name = "framespec"
3
- version = "0.0.1"
4
- description = "Declarative DataFrame specifications for PySpark."
5
- readme = "README.md"
6
- requires-python = ">=3.12"
7
- dependencies = []
8
-
9
- [[project.authors]]
10
- name = "Ryan"
11
-
12
- [build-system]
13
- requires = ["uv_build>=0.12.5,<0.13.0"]
14
- build-backend = "uv_build"
@@ -1,12 +0,0 @@
1
- [project]
2
- name = "framespec"
3
- version = "0.0.1"
4
- description = "Declarative DataFrame specifications for PySpark."
5
- readme = "README.md"
6
- authors = [{ name = "Ryan" }]
7
- requires-python = ">=3.12"
8
- dependencies = []
9
-
10
- [build-system]
11
- requires = ["uv_build>=0.12.5,<0.13.0"]
12
- build-backend = "uv_build"
@@ -1,2 +0,0 @@
1
- def main() -> None:
2
- print("Hello from framespec!")