calc-flow-python 4.0.0__cp313-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.
@@ -0,0 +1,377 @@
1
+ """Immutable feature sets and programs over symbolic declarations.
2
+
3
+ ``FeatureSet`` and ``Program`` are the program-level declaration values of the
4
+ frozen public surface. A program owns its declared inputs and outputs, the
5
+ ``calc_flow.symbolic.declaration.v1`` program fingerprint, and the declaration
6
+ processing entry points ``analyze``/``explain``. Compilation to execution plans
7
+ is a later lowering stage and is deliberately absent here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING
16
+
17
+ from calc_flow.symbolic.domains import type_name
18
+ from calc_flow.symbolic.expr import ArrayExpr, ColumnExpr, Parameter, TableExpr
19
+ from calc_flow.symbolic.nodes import (
20
+ _MAGIC,
21
+ CStr,
22
+ Node,
23
+ _text,
24
+ _u64,
25
+ )
26
+ from calc_flow.symbolic.types import CompileMode
27
+
28
+ if TYPE_CHECKING:
29
+ from calc_flow.pipeline import BatchExecutionPlan, Runtime, StreamExecutionPlan
30
+ from calc_flow.symbolic.analyzer import AnalysisResult
31
+ from calc_flow.symbolic.types import LatePolicy
32
+
33
+ _PROGRAM_TAG = 0x21
34
+
35
+
36
+ @dataclass(frozen=True, slots=True, eq=False, init=False)
37
+ class FeatureSet:
38
+ """An ordered immutable set of uniquely named column expressions."""
39
+
40
+ _features: tuple[tuple[str, ColumnExpr], ...]
41
+
42
+ def __init__(
43
+ self,
44
+ features: Sequence[tuple[str, ColumnExpr]] = (),
45
+ /,
46
+ ) -> None:
47
+ copied: list[tuple[str, ColumnExpr]] = []
48
+ seen: set[str] = set()
49
+ for index, item in enumerate(features):
50
+ if not isinstance(item, tuple) or len(item) != 2:
51
+ raise TypeError(
52
+ f"features[{index}]: must be a (name, ColumnExpr) pair; got"
53
+ f" {type_name(item)}"
54
+ )
55
+ name, value = item
56
+ if type(name) is not str:
57
+ raise TypeError(
58
+ f"features[{index}].name: must be a string; got {type_name(name)}"
59
+ )
60
+ if not isinstance(value, ColumnExpr):
61
+ raise TypeError(
62
+ f"features[{index}].value: must be a ColumnExpr; got"
63
+ f" {type_name(value)}"
64
+ )
65
+ if name in seen:
66
+ raise ValueError(
67
+ f"features[{index}].name: duplicate_name: duplicate feature"
68
+ f" name {name!r}"
69
+ )
70
+ seen.add(name)
71
+ copied.append((name, value))
72
+ object.__setattr__(self, "_features", tuple(copied))
73
+
74
+ @property
75
+ def features(self) -> tuple[tuple[str, ColumnExpr], ...]:
76
+ """The declared features in declaration order."""
77
+
78
+ return self._features
79
+
80
+ def with_feature(self, name: str, value: ColumnExpr, /) -> FeatureSet:
81
+ """Return a new feature set with one feature appended."""
82
+
83
+ return FeatureSet((*self._features, (name, value)))
84
+
85
+
86
+ def _node_name(node: Node, /) -> str:
87
+ value = node.attr("name")
88
+ if isinstance(value, CStr):
89
+ return value.value
90
+ raise TypeError("declaration node is missing its name attribute")
91
+
92
+
93
+ def _collect_program_nodes(
94
+ inputs: tuple[TableExpr | Parameter[object], ...],
95
+ outputs: tuple[tuple[str, TableExpr | ArrayExpr], ...],
96
+ /,
97
+ ) -> dict[str, tuple[bytes, Node]]:
98
+ """Return every unique node reachable from a declared input or output."""
99
+
100
+ nodes: dict[str, tuple[bytes, Node]] = {}
101
+
102
+ def visit(node: Node) -> None:
103
+ existing = nodes.get(node.digest)
104
+ if existing is not None:
105
+ if existing[0] != node.node_bytes:
106
+ raise ValueError(
107
+ "Program.fingerprint: unresolved_type: digest collision for"
108
+ f" node {node.op.name}@{node.op.version}"
109
+ )
110
+ return
111
+ nodes[node.digest] = (node.node_bytes, node)
112
+ for child in node.args:
113
+ visit(child)
114
+
115
+ for value in inputs:
116
+ visit(value._node)
117
+ for _, value in outputs:
118
+ visit(value._node)
119
+ return nodes
120
+
121
+
122
+ def _program_fingerprint(
123
+ name: str,
124
+ inputs: tuple[TableExpr | Parameter[object], ...],
125
+ outputs: tuple[tuple[str, TableExpr | ArrayExpr], ...],
126
+ /,
127
+ ) -> str:
128
+ """Compute the frozen v1 program fingerprint over the declaration graph."""
129
+
130
+ nodes = _collect_program_nodes(inputs, outputs)
131
+ edges = _program_edges(nodes)
132
+ node_records = sorted(
133
+ ((bytes.fromhex(digest), record) for digest, record in nodes.items()),
134
+ key=lambda item: item[0],
135
+ )
136
+ body = (
137
+ bytes((_PROGRAM_TAG,))
138
+ + _text(name)
139
+ + _u64(len(inputs))
140
+ + b"".join(_input_records(inputs))
141
+ + _u64(len(outputs))
142
+ + b"".join(
143
+ _text(output_name) + bytes.fromhex(value._node.digest)
144
+ for output_name, value in outputs
145
+ )
146
+ + _u64(len(node_records))
147
+ + b"".join(
148
+ digest + _u64(len(record[0])) + record[0] for digest, record in node_records
149
+ )
150
+ + _u64(len(edges))
151
+ + b"".join(parent + _u64(index) + child for parent, index, child in edges)
152
+ )
153
+ return hashlib.sha256(_MAGIC + b"\x02" + _u64(len(body)) + body).hexdigest()
154
+
155
+
156
+ def _program_edges(
157
+ nodes: dict[str, tuple[bytes, Node]],
158
+ /,
159
+ ) -> list[tuple[bytes, int, bytes]]:
160
+ edges: list[tuple[bytes, int, bytes]] = []
161
+ for _, node in nodes.values():
162
+ for index, child in enumerate(node.args):
163
+ edges.append(
164
+ (
165
+ bytes.fromhex(node.digest),
166
+ index,
167
+ bytes.fromhex(child.digest),
168
+ )
169
+ )
170
+ edges.sort()
171
+ return edges
172
+
173
+
174
+ def _input_records(
175
+ inputs: tuple[TableExpr | Parameter[object], ...],
176
+ /,
177
+ ) -> list[bytes]:
178
+ return [
179
+ _text(_node_name(value._node)) + bytes.fromhex(value._node.digest)
180
+ for value in inputs
181
+ ]
182
+
183
+
184
+ def _validated_inputs(
185
+ inputs: Sequence[TableExpr | Parameter[object]], /
186
+ ) -> tuple[TableExpr | Parameter[object], ...]:
187
+ declared: dict[str, str] = {}
188
+ copied: list[TableExpr | Parameter[object]] = []
189
+ for index, value in enumerate(inputs):
190
+ if not isinstance(value, (TableExpr, Parameter)):
191
+ raise TypeError(
192
+ f"Program.inputs[{index}]: expected TableExpr |"
193
+ f" Parameter[object]; got {type_name(value)}"
194
+ )
195
+ if value._node.op.name not in ("table_input", "parameter"):
196
+ raise ValueError(
197
+ f"Program.inputs[{index}]: invalid_literal: program inputs"
198
+ " must be declared table_input or parameter values; got"
199
+ f" {value._node.op.name}"
200
+ )
201
+ input_name = _node_name(value._node)
202
+ root = "static_inputs" if isinstance(value, Parameter) else "inputs"
203
+ if input_name in declared:
204
+ raise ValueError(
205
+ f"{root}.{input_name}: duplicate_name: duplicate input name"
206
+ f" {input_name!r}"
207
+ )
208
+ declared[input_name] = root
209
+ copied.append(value)
210
+ return tuple(copied)
211
+
212
+
213
+ def _validated_output(index: int, item: object, /) -> tuple[str, TableExpr | ArrayExpr]:
214
+ if not isinstance(item, tuple) or len(item) != 2:
215
+ raise TypeError(
216
+ f"Program.outputs[{index}]: must be a (name, TableExpr |"
217
+ f" ArrayExpr) pair; got {type_name(item)}"
218
+ )
219
+ output_name, value = item
220
+ if type(output_name) is not str:
221
+ raise TypeError(
222
+ f"Program.outputs[{index}].name: must be a string; got"
223
+ f" {type_name(output_name)}"
224
+ )
225
+ if not output_name:
226
+ raise ValueError(
227
+ f"Program.outputs[{index}].name: invalid_literal: must be a"
228
+ " non-empty string"
229
+ )
230
+ if not isinstance(value, (TableExpr, ArrayExpr)):
231
+ raise TypeError(
232
+ f"Program.outputs[{index}].value: expected TableExpr |"
233
+ f" ArrayExpr; got {type_name(value)}"
234
+ )
235
+ return output_name, value
236
+
237
+
238
+ def _validated_outputs(
239
+ outputs: Sequence[tuple[str, TableExpr | ArrayExpr]], /
240
+ ) -> tuple[tuple[str, TableExpr | ArrayExpr], ...]:
241
+ copied: list[tuple[str, TableExpr | ArrayExpr]] = []
242
+ names: set[str] = set()
243
+ for index, item in enumerate(outputs):
244
+ output_name, value = _validated_output(index, item)
245
+ if output_name in names:
246
+ raise ValueError(
247
+ f"outputs.{output_name}: duplicate_name: duplicate output"
248
+ f" name {output_name!r}"
249
+ )
250
+ names.add(output_name)
251
+ copied.append((output_name, value))
252
+ return tuple(copied)
253
+
254
+
255
+ @dataclass(frozen=True, slots=True, eq=False, init=False)
256
+ class Program:
257
+ """An immutable program of declared inputs, outputs, and expressions."""
258
+
259
+ _name: str
260
+ _inputs: tuple[TableExpr | Parameter[object], ...]
261
+ _outputs: tuple[tuple[str, TableExpr | ArrayExpr], ...]
262
+ _fingerprint: str
263
+
264
+ def __init__(
265
+ self,
266
+ name: str,
267
+ /,
268
+ *,
269
+ inputs: Sequence[TableExpr | Parameter[object]] = (),
270
+ outputs: Sequence[tuple[str, TableExpr | ArrayExpr]] = (),
271
+ ) -> None:
272
+ if type(name) is not str:
273
+ raise TypeError(f"Program.name must be a string; got {type_name(name)}")
274
+ if not name:
275
+ raise ValueError(
276
+ "Program.name: invalid_literal: must be a non-empty string"
277
+ )
278
+ copied_inputs = _validated_inputs(inputs)
279
+ copied_outputs = _validated_outputs(outputs)
280
+ object.__setattr__(self, "_name", name)
281
+ object.__setattr__(self, "_inputs", copied_inputs)
282
+ object.__setattr__(self, "_outputs", copied_outputs)
283
+ object.__setattr__(
284
+ self,
285
+ "_fingerprint",
286
+ _program_fingerprint(name, copied_inputs, copied_outputs),
287
+ )
288
+
289
+ @property
290
+ def name(self) -> str:
291
+ """The declared program name."""
292
+
293
+ return self._name
294
+
295
+ @property
296
+ def inputs(self) -> tuple[TableExpr | Parameter[object], ...]:
297
+ """The declared inputs in declaration order."""
298
+
299
+ return self._inputs
300
+
301
+ @property
302
+ def outputs(self) -> tuple[tuple[str, TableExpr | ArrayExpr], ...]:
303
+ """The declared outputs in declaration order."""
304
+
305
+ return self._outputs
306
+
307
+ @property
308
+ def fingerprint(self) -> str:
309
+ """The runtime-independent v1 program fingerprint."""
310
+
311
+ return self._fingerprint
312
+
313
+ def with_input(self, value: TableExpr | Parameter[object], /) -> Program:
314
+ """Return a new program with one declared input appended."""
315
+
316
+ return Program(
317
+ self._name,
318
+ inputs=(*self._inputs, value),
319
+ outputs=self._outputs,
320
+ )
321
+
322
+ def output(self, name: str, value: TableExpr | ArrayExpr, /) -> Program:
323
+ """Return a new program with one declared output appended."""
324
+
325
+ return Program(
326
+ self._name,
327
+ inputs=self._inputs,
328
+ outputs=(*self._outputs, (name, value)),
329
+ )
330
+
331
+ def analyze(self, runtime: Runtime, /, *, mode: CompileMode) -> AnalysisResult:
332
+ """Analyze this program against one immutable capability snapshot."""
333
+
334
+ from calc_flow.symbolic.analyzer import analyze_program
335
+
336
+ return analyze_program(self, runtime, mode)
337
+
338
+ def explain(self, runtime: Runtime, /, *, mode: CompileMode) -> str:
339
+ """Render deterministic analysis facts for this program."""
340
+
341
+ from calc_flow.symbolic.analyzer import explain_program
342
+
343
+ return explain_program(self, runtime, mode)
344
+
345
+ def compile_batch(self, runtime: Runtime, /) -> BatchExecutionPlan:
346
+ """Lower this program to a strict project-v3 batch execution plan.
347
+
348
+ Compilation is declaration processing only: it captures one immutable
349
+ capability snapshot, lowers one strict project-v3 document, and invokes
350
+ the Rust graph compiler for final validation. No data, source, sink,
351
+ or runner is accepted.
352
+ """
353
+
354
+ from calc_flow.symbolic.lower import compile_program_batch
355
+
356
+ return compile_program_batch(self, runtime)
357
+
358
+ def compile_stream(
359
+ self,
360
+ runtime: Runtime,
361
+ /,
362
+ *,
363
+ allowed_lateness_micros: int = 0,
364
+ late_policy: LatePolicy = "error",
365
+ ) -> StreamExecutionPlan:
366
+ """Lower this program to a strict project-v3 continuous plan.
367
+
368
+ The lateness arguments are validated and serialized into every lowered
369
+ rolling or cross-section stage. Row-local-only programs accept the same
370
+ compile signature but have no stateful late-row surface.
371
+ """
372
+
373
+ from calc_flow.symbolic.lower import compile_program_stream
374
+
375
+ return compile_program_stream(
376
+ self, runtime, allowed_lateness_micros, late_policy
377
+ )
@@ -0,0 +1,110 @@
1
+ """Strict declaration types and validators for symbolic expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ from calc_flow.capabilities import PORTABLE_ARROW_TYPES
10
+ from calc_flow.symbolic.domains import type_name
11
+
12
+ type BatchKind = Literal["table", "array"]
13
+ type CompileMode = Literal["batch", "stream"]
14
+ type LatePolicy = Literal["error", "drop"]
15
+ type ScalarLiteral = None | bool | int | float | str
16
+
17
+ # The initial portable field spelling adds ``timestamp[us, UTC]`` to the
18
+ # current portable Arrow names (API note section 2.2).
19
+ TABLE_FIELD_TYPES = frozenset((*PORTABLE_ARROW_TYPES, "timestamp[us, UTC]"))
20
+ # Array declaration dtypes stay inside the static-input digest v1 coverage:
21
+ # bool, the integer types, float32, and float64.
22
+ ARRAY_DTYPES = frozenset(
23
+ (
24
+ "bool",
25
+ "float32",
26
+ "float64",
27
+ "int16",
28
+ "int32",
29
+ "int64",
30
+ "int8",
31
+ "uint16",
32
+ "uint32",
33
+ "uint64",
34
+ "uint8",
35
+ )
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Field:
41
+ """One exact table field declaration with host-type validation."""
42
+
43
+ name: str
44
+ data_type: str
45
+ nullable: bool = True
46
+
47
+ def __post_init__(self) -> None:
48
+ if type(self.name) is not str:
49
+ raise TypeError(f"Field.name must be a string; got {type_name(self.name)}")
50
+ if type(self.data_type) is not str:
51
+ raise TypeError(
52
+ f"Field.data_type must be a string; got {type_name(self.data_type)}"
53
+ )
54
+ if type(self.nullable) is not bool:
55
+ raise TypeError(
56
+ f"Field.nullable must be a boolean; got {type_name(self.nullable)}"
57
+ )
58
+
59
+
60
+ def require_str(value: object, path: str, /) -> str:
61
+ if type(value) is not str:
62
+ raise TypeError(f"{path} must be a string; got {type_name(value)}")
63
+ return value
64
+
65
+
66
+ def require_non_empty_str(value: object, path: str, /) -> str:
67
+ require_str(value, path)
68
+ if not value: # type: ignore[arg-type]
69
+ raise ValueError(f"{path}: invalid_literal: must be a non-empty string")
70
+ return value # type: ignore[return-value]
71
+
72
+
73
+ def require_int(value: object, path: str, /) -> int:
74
+ if type(value) is not int:
75
+ raise TypeError(f"{path} must be an integer; got {type_name(value)}")
76
+ return value
77
+
78
+
79
+ def require_positive_int(value: object, path: str, /) -> int:
80
+ require_int(value, path)
81
+ if value <= 0:
82
+ raise ValueError(f"{path}: invalid_literal: must be a positive integer")
83
+ return value
84
+
85
+
86
+ def require_non_negative_int(value: object, path: str, /) -> int:
87
+ require_int(value, path)
88
+ if value < 0:
89
+ raise ValueError(f"{path}: invalid_literal: must be a non-negative integer")
90
+ return value
91
+
92
+
93
+ def require_finite_number(value: object, path: str, /) -> int | float:
94
+ if type(value) not in (int, float):
95
+ raise TypeError(f"{path} must be a finite number; got {type_name(value)}")
96
+ if isinstance(value, float) and not math.isfinite(value):
97
+ raise ValueError(f"{path}: invalid_literal: must be finite")
98
+ return value
99
+
100
+
101
+ def check_table_field_type(data_type: str, path: str, /) -> None:
102
+ if data_type not in TABLE_FIELD_TYPES:
103
+ raise ValueError(
104
+ f"{path}: unsupported_type: unknown portable Arrow type {data_type!r}"
105
+ )
106
+
107
+
108
+ def check_array_dtype(dtype: str, path: str, /) -> None:
109
+ if dtype not in ARRAY_DTYPES:
110
+ raise ValueError(f"{path}: unsupported_type: unknown array dtype {dtype!r}")
@@ -0,0 +1,153 @@
1
+ """Explicit row-count, duration, and event-time grouping declarations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ from calc_flow.symbolic.domains import namespace_error
9
+ from calc_flow.symbolic.expr import ColumnExpr
10
+ from calc_flow.symbolic.types import require_positive_int
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class RowFrame:
15
+ """A positive row-count rolling frame ``rows [i - size + 1, i]``."""
16
+
17
+ size: int
18
+
19
+ def __post_init__(self) -> None:
20
+ require_positive_int(self.size, "RowFrame.size")
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class DurationFrame:
25
+ """A positive exact-microseconds duration frame ``(t - micros, t]``."""
26
+
27
+ micros: int
28
+
29
+ def __post_init__(self) -> None:
30
+ require_positive_int(self.micros, "DurationFrame.micros")
31
+
32
+
33
+ @dataclass(frozen=True, slots=True, eq=False)
34
+ class EventTimeBucket:
35
+ """A fixed UTC event-time bucket of exact positive width."""
36
+
37
+ event_time: ColumnExpr
38
+ width_micros: int
39
+ partition_by: tuple[ColumnExpr, ...] = ()
40
+
41
+ def __post_init__(self) -> None:
42
+ if not isinstance(self.event_time, ColumnExpr):
43
+ raise namespace_error(
44
+ "event_time_bucket", "event_time", "ColumnExpr", self.event_time
45
+ )
46
+ require_positive_int(
47
+ self.width_micros, "calc_flow.symbolic.event_time_bucket.width_micros"
48
+ )
49
+ object.__setattr__(self, "partition_by", tuple(self.partition_by))
50
+ for index, column in enumerate(self.partition_by):
51
+ if not isinstance(column, ColumnExpr):
52
+ raise namespace_error(
53
+ "event_time_bucket",
54
+ f"partition_by[{index}]",
55
+ "ColumnExpr",
56
+ column,
57
+ )
58
+
59
+
60
+ @dataclass(frozen=True, slots=True, eq=False)
61
+ class CrossSectionGroup:
62
+ """One complete cross-section grouping declaration."""
63
+
64
+ event_time: ColumnExpr
65
+ bucket: EventTimeBucket | None
66
+ partition_by: tuple[ColumnExpr, ...]
67
+
68
+ def __post_init__(self) -> None:
69
+ if not isinstance(self.event_time, ColumnExpr):
70
+ raise namespace_error(
71
+ "cross_section_group",
72
+ "event_time",
73
+ "ColumnExpr",
74
+ self.event_time,
75
+ )
76
+ if self.bucket is not None and not isinstance(self.bucket, EventTimeBucket):
77
+ raise namespace_error(
78
+ "cross_section_group",
79
+ "bucket",
80
+ "EventTimeBucket | None",
81
+ self.bucket,
82
+ )
83
+ object.__setattr__(self, "partition_by", tuple(self.partition_by))
84
+ for index, column in enumerate(self.partition_by):
85
+ if not isinstance(column, ColumnExpr):
86
+ raise namespace_error(
87
+ "cross_section_group",
88
+ f"partition_by[{index}]",
89
+ "ColumnExpr",
90
+ column,
91
+ )
92
+
93
+
94
+ def rows(size: int, /) -> RowFrame:
95
+ """Declare a row-count rolling frame."""
96
+
97
+ require_positive_int(size, "calc_flow.symbolic.rows.size")
98
+ return RowFrame(size)
99
+
100
+
101
+ def duration(micros: int, /) -> DurationFrame:
102
+ """Declare a duration rolling frame in exact microseconds."""
103
+
104
+ require_positive_int(micros, "calc_flow.symbolic.duration.micros")
105
+ return DurationFrame(micros)
106
+
107
+
108
+ def exact_time(
109
+ event_time: ColumnExpr,
110
+ /,
111
+ *,
112
+ partition_by: Sequence[ColumnExpr] = (),
113
+ ) -> CrossSectionGroup:
114
+ """Group by one exact event-time value plus the ordered partition key."""
115
+
116
+ if not isinstance(event_time, ColumnExpr):
117
+ raise namespace_error("exact_time", "event_time", "ColumnExpr", event_time)
118
+ columns = tuple(partition_by)
119
+ for index, column in enumerate(columns):
120
+ if not isinstance(column, ColumnExpr):
121
+ raise namespace_error(
122
+ "exact_time", f"partition_by[{index}]", "ColumnExpr", column
123
+ )
124
+ return CrossSectionGroup(event_time, None, columns)
125
+
126
+
127
+ def event_time_bucket(
128
+ event_time: ColumnExpr,
129
+ /,
130
+ *,
131
+ width_micros: int,
132
+ partition_by: Sequence[ColumnExpr] = (),
133
+ ) -> CrossSectionGroup:
134
+ """Group by fixed UTC buckets of exact positive width."""
135
+
136
+ if not isinstance(event_time, ColumnExpr):
137
+ raise namespace_error(
138
+ "event_time_bucket", "event_time", "ColumnExpr", event_time
139
+ )
140
+ require_positive_int(
141
+ width_micros, "calc_flow.symbolic.event_time_bucket.width_micros"
142
+ )
143
+ columns = tuple(partition_by)
144
+ for index, column in enumerate(columns):
145
+ if not isinstance(column, ColumnExpr):
146
+ raise namespace_error(
147
+ "event_time_bucket",
148
+ f"partition_by[{index}]",
149
+ "ColumnExpr",
150
+ column,
151
+ )
152
+ bucket = EventTimeBucket(event_time, width_micros, columns)
153
+ return CrossSectionGroup(event_time, bucket, columns)
calc_flow/udf.py ADDED
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any
5
+
6
+
7
+ def _validate_scalar_udf_registration(
8
+ input_types: Sequence[str], function: Any
9
+ ) -> tuple[str, ...]:
10
+ if isinstance(input_types, (str, bytes)) or not isinstance(input_types, Sequence):
11
+ raise TypeError("input_types must be a sequence of Arrow type names")
12
+ copied_types = tuple(input_types)
13
+ if len(copied_types) > 64:
14
+ raise ValueError("input_types must contain at most 64 entries")
15
+ if not all(isinstance(value, str) for value in copied_types):
16
+ raise TypeError("input_types must contain only Arrow type names")
17
+ if not callable(function):
18
+ raise TypeError("function must be callable")
19
+ return copied_types