calc-flow-python 2026.9.25__cp310-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.
- calc_flow/__init__.py +248 -0
- calc_flow/_compat.py +103 -0
- calc_flow/_native.pyd +4 -0
- calc_flow/_native.pyi +387 -0
- calc_flow/_stream_inputs.py +233 -0
- calc_flow/array.py +1337 -0
- calc_flow/asof_join_spec.py +126 -0
- calc_flow/capabilities.py +829 -0
- calc_flow/compute.py +278 -0
- calc_flow/config.py +221 -0
- calc_flow/errors.py +25 -0
- calc_flow/join_spec.py +157 -0
- calc_flow/pipeline.py +1288 -0
- calc_flow/py.typed +0 -0
- calc_flow/runtime.py +1075 -0
- calc_flow/store.py +140 -0
- calc_flow/stream.py +390 -0
- calc_flow/symbolic/__init__.py +69 -0
- calc_flow/symbolic/_generated_rolling_kernels.py +23 -0
- calc_flow/symbolic/analyzer.py +2788 -0
- calc_flow/symbolic/asof.py +212 -0
- calc_flow/symbolic/asof_analysis.py +215 -0
- calc_flow/symbolic/domains.py +77 -0
- calc_flow/symbolic/errors.py +67 -0
- calc_flow/symbolic/expr.py +850 -0
- calc_flow/symbolic/late_output.py +256 -0
- calc_flow/symbolic/lower/__init__.py +31 -0
- calc_flow/symbolic/lower/asof.py +86 -0
- calc_flow/symbolic/lower/bindings.py +69 -0
- calc_flow/symbolic/lower/event_windows.py +477 -0
- calc_flow/symbolic/lower/late_output.py +118 -0
- calc_flow/symbolic/lower/planners.py +1278 -0
- calc_flow/symbolic/lower/program.py +840 -0
- calc_flow/symbolic/lower/schema.py +170 -0
- calc_flow/symbolic/lower/segments.py +871 -0
- calc_flow/symbolic/lower/sql.py +423 -0
- calc_flow/symbolic/lower/strategies.py +1522 -0
- calc_flow/symbolic/nodes.py +626 -0
- calc_flow/symbolic/ops.py +1305 -0
- calc_flow/symbolic/optimizer.py +692 -0
- calc_flow/symbolic/program.py +494 -0
- calc_flow/symbolic/sql.py +36 -0
- calc_flow/symbolic/types.py +110 -0
- calc_flow/symbolic/windows.py +177 -0
- calc_flow/udf.py +19 -0
- calc_flow_python-2026.9.25.dist-info/METADATA +318 -0
- calc_flow_python-2026.9.25.dist-info/RECORD +50 -0
- calc_flow_python-2026.9.25.dist-info/WHEEL +5 -0
- calc_flow_python-2026.9.25.dist-info/licenses/LICENSE +202 -0
- calc_flow_python-2026.9.25.dist-info/sboms/calc-flow-python.cyclonedx.json +10082 -0
calc_flow/_native.pyi
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal, Protocol, TypedDict, final
|
|
7
|
+
|
|
8
|
+
import pyarrow as pa
|
|
9
|
+
from typing_extensions import Never, TypeAlias
|
|
10
|
+
|
|
11
|
+
JSONValue: TypeAlias = (
|
|
12
|
+
None | bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"]
|
|
13
|
+
)
|
|
14
|
+
_JSONInput: TypeAlias = (
|
|
15
|
+
None | bool | int | float | str | list["_JSONInput"] | Mapping[str, "_JSONInput"]
|
|
16
|
+
)
|
|
17
|
+
StreamingFailureReasonCode: TypeAlias = Literal[
|
|
18
|
+
"join_state_limit_exceeded",
|
|
19
|
+
"join_match_limit_exceeded",
|
|
20
|
+
"join_counter_overflow",
|
|
21
|
+
"join_time_conversion_failed",
|
|
22
|
+
"asof_invalid_input",
|
|
23
|
+
"asof_duplicate_identity",
|
|
24
|
+
"asof_late_row",
|
|
25
|
+
"asof_state_limit_exceeded",
|
|
26
|
+
"asof_workspace_limit_exceeded",
|
|
27
|
+
"asof_output_limit_exceeded",
|
|
28
|
+
"asof_counter_overflow",
|
|
29
|
+
"asof_protocol_error",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
class _ArrowCStream(Protocol):
|
|
33
|
+
def __arrow_c_stream__(
|
|
34
|
+
self, requested_schema: object | None = None, /
|
|
35
|
+
) -> object: ...
|
|
36
|
+
|
|
37
|
+
class CalcFlowError(Exception): ...
|
|
38
|
+
|
|
39
|
+
class ConfigError(CalcFlowError):
|
|
40
|
+
@property
|
|
41
|
+
def issues(self) -> tuple[dict[str, str], ...]: ...
|
|
42
|
+
|
|
43
|
+
class CompileError(CalcFlowError): ...
|
|
44
|
+
class ExecutionError(CalcFlowError): ...
|
|
45
|
+
class ProviderError(ExecutionError): ...
|
|
46
|
+
class CheckpointError(CalcFlowError): ...
|
|
47
|
+
class CancelledError(ExecutionError): ...
|
|
48
|
+
|
|
49
|
+
class StreamingRuntimeError(CalcFlowError):
|
|
50
|
+
@property
|
|
51
|
+
def category(self) -> str: ...
|
|
52
|
+
@property
|
|
53
|
+
def reason_code(self) -> StreamingFailureReasonCode | None: ...
|
|
54
|
+
@property
|
|
55
|
+
def message(self) -> str: ...
|
|
56
|
+
@property
|
|
57
|
+
def job_id(self) -> int | None: ...
|
|
58
|
+
@property
|
|
59
|
+
def epoch(self) -> int | None: ...
|
|
60
|
+
@property
|
|
61
|
+
def checkpoint_phase(self) -> str | None: ...
|
|
62
|
+
@property
|
|
63
|
+
def component_kind(self) -> str | None: ...
|
|
64
|
+
@property
|
|
65
|
+
def component_id(self) -> str | None: ...
|
|
66
|
+
@property
|
|
67
|
+
def diagnostic_id(self) -> int | None: ...
|
|
68
|
+
@property
|
|
69
|
+
def position(self) -> int: ...
|
|
70
|
+
|
|
71
|
+
class CheckpointPublicationUnknownError(StreamingRuntimeError): ...
|
|
72
|
+
|
|
73
|
+
@final
|
|
74
|
+
class ExecutionOptions:
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
settings: Mapping[str, _JSONInput] | None = ...,
|
|
78
|
+
deadline: datetime | None = ...,
|
|
79
|
+
) -> None: ...
|
|
80
|
+
@property
|
|
81
|
+
def settings(self) -> dict[str, JSONValue]: ...
|
|
82
|
+
@property
|
|
83
|
+
def deadline(self) -> datetime | None: ...
|
|
84
|
+
|
|
85
|
+
@final
|
|
86
|
+
class ProviderContext:
|
|
87
|
+
def __new__(cls) -> Never:
|
|
88
|
+
"""Engine-created; application construction is not public API."""
|
|
89
|
+
...
|
|
90
|
+
@property
|
|
91
|
+
def settings(self) -> dict[str, JSONValue]: ...
|
|
92
|
+
@property
|
|
93
|
+
def deadline(self) -> datetime | None: ...
|
|
94
|
+
|
|
95
|
+
class _ExecutionCancellation(Protocol):
|
|
96
|
+
def cancel(self) -> None: ...
|
|
97
|
+
|
|
98
|
+
class Batch:
|
|
99
|
+
@staticmethod
|
|
100
|
+
def from_pyarrow(
|
|
101
|
+
table: _ArrowCStream, metadata: Mapping[str, object] | None = None
|
|
102
|
+
) -> Batch: ...
|
|
103
|
+
@staticmethod
|
|
104
|
+
def from_array(
|
|
105
|
+
array: object,
|
|
106
|
+
*,
|
|
107
|
+
backend: str,
|
|
108
|
+
metadata: Mapping[str, object] | None = None,
|
|
109
|
+
) -> Batch: ...
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _from_external(
|
|
112
|
+
object: object, backend: str, len: int, metadata: Mapping[str, object]
|
|
113
|
+
) -> Batch:
|
|
114
|
+
"""Build an internal Python-provider batch; this is not a stable public API."""
|
|
115
|
+
...
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _new_owned_numpy(shape: Sequence[int], dtype: str) -> tuple[object, object]:
|
|
118
|
+
"""Allocate private Rust-owned NumPy storage."""
|
|
119
|
+
...
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _from_owned_array(
|
|
122
|
+
array: object,
|
|
123
|
+
*,
|
|
124
|
+
backend: str,
|
|
125
|
+
token: object | None,
|
|
126
|
+
metadata: Mapping[str, object],
|
|
127
|
+
) -> Batch:
|
|
128
|
+
"""Adopt a trusted provider result; this is not a stable public API."""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
def to_pyarrow(self) -> pa.Table: ...
|
|
132
|
+
@property
|
|
133
|
+
def array(self) -> object: ...
|
|
134
|
+
@property
|
|
135
|
+
def backend(self) -> str: ...
|
|
136
|
+
@property
|
|
137
|
+
def kind(self) -> str: ...
|
|
138
|
+
@property
|
|
139
|
+
def num_rows(self) -> int: ...
|
|
140
|
+
@property
|
|
141
|
+
def metadata(self) -> dict[str, object]: ...
|
|
142
|
+
|
|
143
|
+
class Runtime:
|
|
144
|
+
def __init__(self) -> None: ...
|
|
145
|
+
def register_provider(
|
|
146
|
+
self,
|
|
147
|
+
provider: str,
|
|
148
|
+
name: str,
|
|
149
|
+
version: str,
|
|
150
|
+
callback: object,
|
|
151
|
+
*,
|
|
152
|
+
accepts_context: bool = False,
|
|
153
|
+
) -> None: ...
|
|
154
|
+
def _register_mapping_provider(
|
|
155
|
+
self,
|
|
156
|
+
provider: str,
|
|
157
|
+
name: str,
|
|
158
|
+
version: str,
|
|
159
|
+
callback: object,
|
|
160
|
+
*,
|
|
161
|
+
input_ports: Sequence[tuple[str, str]],
|
|
162
|
+
output_ports: Sequence[tuple[str, str]],
|
|
163
|
+
accepts_context: bool = False,
|
|
164
|
+
) -> None: ...
|
|
165
|
+
def _register_stateless_stream_provider(
|
|
166
|
+
self,
|
|
167
|
+
provider: str,
|
|
168
|
+
name: str,
|
|
169
|
+
version: str,
|
|
170
|
+
callback: object,
|
|
171
|
+
*,
|
|
172
|
+
microbatch_invariant: bool,
|
|
173
|
+
deterministic: bool,
|
|
174
|
+
replay_safe: bool,
|
|
175
|
+
) -> None: ...
|
|
176
|
+
def _register_stateless_stream_mapping_provider(
|
|
177
|
+
self,
|
|
178
|
+
provider: str,
|
|
179
|
+
name: str,
|
|
180
|
+
version: str,
|
|
181
|
+
callback: object,
|
|
182
|
+
*,
|
|
183
|
+
input_ports: Sequence[tuple[str, str]],
|
|
184
|
+
output_ports: Sequence[tuple[str, str]],
|
|
185
|
+
microbatch_invariant: bool,
|
|
186
|
+
deterministic: bool,
|
|
187
|
+
replay_safe: bool,
|
|
188
|
+
) -> None: ...
|
|
189
|
+
def register_scalar_udf(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
provider: str,
|
|
193
|
+
name: str,
|
|
194
|
+
version: str,
|
|
195
|
+
input_types: Sequence[str],
|
|
196
|
+
return_type: str,
|
|
197
|
+
volatility: str,
|
|
198
|
+
function: Callable[..., object],
|
|
199
|
+
) -> None: ...
|
|
200
|
+
def register_kafka_decoder(
|
|
201
|
+
self,
|
|
202
|
+
*,
|
|
203
|
+
name: str,
|
|
204
|
+
version: str,
|
|
205
|
+
function: Callable[[bytes], object],
|
|
206
|
+
) -> None: ...
|
|
207
|
+
def catalog(self) -> list[dict[str, object]]: ...
|
|
208
|
+
def _infer_sql_schema(
|
|
209
|
+
self, query: str, schemas: Mapping[str, pa.Schema]
|
|
210
|
+
) -> pa.Schema: ...
|
|
211
|
+
def validation_report(self, project_json: str) -> dict[str, object]: ...
|
|
212
|
+
def compile_project(self, project_json: str) -> ExecutionPlan: ...
|
|
213
|
+
def compile_stream_project(
|
|
214
|
+
self, project_json: str, delivery: Mapping[str, str]
|
|
215
|
+
) -> StreamExecutionPlan: ...
|
|
216
|
+
def _compile_stream_graph_project(
|
|
217
|
+
self, project_json: str, delivery: Mapping[str, str]
|
|
218
|
+
) -> StreamExecutionPlan: ...
|
|
219
|
+
|
|
220
|
+
class ExecutionPlan:
|
|
221
|
+
@property
|
|
222
|
+
def name(self) -> str: ...
|
|
223
|
+
@property
|
|
224
|
+
def fingerprint(self) -> str: ...
|
|
225
|
+
def execute(
|
|
226
|
+
self,
|
|
227
|
+
inputs: dict[str, Batch],
|
|
228
|
+
*,
|
|
229
|
+
options: ExecutionOptions | None = None,
|
|
230
|
+
) -> RunResult: ...
|
|
231
|
+
def execute_async(
|
|
232
|
+
self,
|
|
233
|
+
inputs: dict[str, Batch],
|
|
234
|
+
*,
|
|
235
|
+
options: ExecutionOptions | None = None,
|
|
236
|
+
) -> Awaitable[RunResult]: ...
|
|
237
|
+
def _execute_async_cancellable(
|
|
238
|
+
self,
|
|
239
|
+
inputs: dict[str, Batch],
|
|
240
|
+
*,
|
|
241
|
+
options: ExecutionOptions | None = None,
|
|
242
|
+
) -> tuple[asyncio.Future[RunResult], _ExecutionCancellation]: ...
|
|
243
|
+
def snapshot_async(self) -> Awaitable[dict[str, object]]: ...
|
|
244
|
+
def restore_async(self, state_json: str) -> Awaitable[None]: ...
|
|
245
|
+
def reset_async(self) -> Awaitable[None]: ...
|
|
246
|
+
|
|
247
|
+
class StreamExecutionPlan:
|
|
248
|
+
@property
|
|
249
|
+
def name(self) -> str: ...
|
|
250
|
+
@property
|
|
251
|
+
def fingerprint(self) -> str: ...
|
|
252
|
+
@property
|
|
253
|
+
def source_binding_ids(self) -> tuple[str, ...]: ...
|
|
254
|
+
@property
|
|
255
|
+
def static_input_ids(self) -> tuple[str, ...]: ...
|
|
256
|
+
@property
|
|
257
|
+
def sink_binding_ids(self) -> tuple[str, ...]: ...
|
|
258
|
+
@property
|
|
259
|
+
def requirements(self) -> dict[str, str]: ...
|
|
260
|
+
|
|
261
|
+
@final
|
|
262
|
+
class _ManagedCheckpointRuntime:
|
|
263
|
+
def __init__(self, directory: str, /) -> None: ...
|
|
264
|
+
|
|
265
|
+
@final
|
|
266
|
+
class _StreamingRunner:
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
plan: StreamExecutionPlan,
|
|
270
|
+
sources: Mapping[str, object],
|
|
271
|
+
sinks: Mapping[str, Sequence[object]],
|
|
272
|
+
checkpoints: _ManagedCheckpointRuntime,
|
|
273
|
+
config: Mapping[str, int],
|
|
274
|
+
static_inputs: Mapping[str, object],
|
|
275
|
+
) -> None: ...
|
|
276
|
+
def start_async(self) -> Awaitable[_StreamingJob]: ...
|
|
277
|
+
def _wait_start_cleanup_async(self) -> Awaitable[None]: ...
|
|
278
|
+
def _release_roots(self) -> None: ...
|
|
279
|
+
|
|
280
|
+
@final
|
|
281
|
+
class _StreamingJob:
|
|
282
|
+
@property
|
|
283
|
+
def id(self) -> int: ...
|
|
284
|
+
def status(self) -> dict[str, object]: ...
|
|
285
|
+
def trigger_checkpoint_async(self) -> Awaitable[int]: ...
|
|
286
|
+
def shutdown_async(self) -> Awaitable[dict[str, object]]: ...
|
|
287
|
+
def cancel_async(self) -> Awaitable[dict[str, object]]: ...
|
|
288
|
+
def wait_async(self) -> Awaitable[dict[str, object]]: ...
|
|
289
|
+
def _release_roots(self) -> None: ...
|
|
290
|
+
def _enable_callback_profiling(self) -> None: ...
|
|
291
|
+
def _take_callback_profile(self) -> str: ...
|
|
292
|
+
|
|
293
|
+
class _FileProjectStore:
|
|
294
|
+
def __init__(self, directory: str) -> None: ...
|
|
295
|
+
def create(self, project_json: str) -> Awaitable[None]: ...
|
|
296
|
+
def put(self, project_json: str) -> Awaitable[None]: ...
|
|
297
|
+
def get(self, project_id: str) -> Awaitable[str]: ...
|
|
298
|
+
def list(self) -> Awaitable[list[str]]: ...
|
|
299
|
+
def delete(self, project_id: str) -> Awaitable[None]: ...
|
|
300
|
+
|
|
301
|
+
def import_project_json(document: bytes) -> str: ...
|
|
302
|
+
def import_project_yaml(document: bytes) -> str: ...
|
|
303
|
+
def export_project_json(project_json: str) -> str: ...
|
|
304
|
+
def export_project_yaml(project_json: str) -> str: ...
|
|
305
|
+
|
|
306
|
+
class _NodeTiming(TypedDict):
|
|
307
|
+
duration_ns: int
|
|
308
|
+
input_rows: dict[str, int]
|
|
309
|
+
output_rows: dict[str, int]
|
|
310
|
+
|
|
311
|
+
class _DataFusionMetric(TypedDict):
|
|
312
|
+
query_id: int
|
|
313
|
+
node_id: str | None
|
|
314
|
+
runtime_acquire_ns: int
|
|
315
|
+
session_state_create_ns: int
|
|
316
|
+
input_adapter_ns: int
|
|
317
|
+
table_register_ns: int
|
|
318
|
+
sql_parse_ns: int
|
|
319
|
+
logical_planning_ns: int
|
|
320
|
+
physical_planning_ns: int
|
|
321
|
+
physical_planning_count: int
|
|
322
|
+
planning_ns: int
|
|
323
|
+
stream_open_ns: int
|
|
324
|
+
execution_to_first_batch_ns: int
|
|
325
|
+
execution_remaining_ns: int
|
|
326
|
+
execution_ns: int
|
|
327
|
+
collect_ns: int
|
|
328
|
+
output_arrow_wrap_ns: int
|
|
329
|
+
audit_ns: int
|
|
330
|
+
metrics_traversal_ns: int
|
|
331
|
+
logical_plan_string_ns: int
|
|
332
|
+
physical_plan_string_ns: int
|
|
333
|
+
batch_envelope_ns: int
|
|
334
|
+
run_result_ns: int
|
|
335
|
+
physical_metric_count: int
|
|
336
|
+
output_partition_count: int
|
|
337
|
+
output_partition_rows: list[int]
|
|
338
|
+
window_partition_count: int
|
|
339
|
+
window_partition_rows: list[int]
|
|
340
|
+
spill_bytes: int
|
|
341
|
+
elapsed_compute_ns: int
|
|
342
|
+
window_compute_ns: int
|
|
343
|
+
repartition_sort_compute_ns: int
|
|
344
|
+
window_operator_count: int
|
|
345
|
+
repartition_operator_count: int
|
|
346
|
+
sort_operator_count: int
|
|
347
|
+
coalesce_operator_count: int
|
|
348
|
+
output_rows: int
|
|
349
|
+
configured_batch_size: int
|
|
350
|
+
parallelism_mode: Literal["fixed", "auto"]
|
|
351
|
+
configured_target_partitions: int
|
|
352
|
+
requested_target_partitions: int
|
|
353
|
+
effective_target_partitions: int
|
|
354
|
+
available_parallelism: int
|
|
355
|
+
max_partitions: int
|
|
356
|
+
min_rows_per_partition: int
|
|
357
|
+
small_rows_threshold: int
|
|
358
|
+
parallelism_decision_reused: bool
|
|
359
|
+
decision_input_rows: int
|
|
360
|
+
decision_active_entities: int | None
|
|
361
|
+
decision_active_entities_source: str
|
|
362
|
+
input_rows: int
|
|
363
|
+
active_entities: int | None
|
|
364
|
+
active_entities_source: str
|
|
365
|
+
partition_limit_reason: str
|
|
366
|
+
rolling_rewrite_enabled: bool
|
|
367
|
+
diagnostics_collected: bool
|
|
368
|
+
rolling_candidate_windows: int
|
|
369
|
+
rolling_rewritten_windows: int
|
|
370
|
+
rolling_fallback_reasons: list[str]
|
|
371
|
+
logical_plan: str
|
|
372
|
+
physical_plan: str
|
|
373
|
+
|
|
374
|
+
class RunResult:
|
|
375
|
+
@property
|
|
376
|
+
def outputs(self) -> dict[str, Batch]: ...
|
|
377
|
+
@property
|
|
378
|
+
def metadata(self) -> dict[str, str]: ...
|
|
379
|
+
@property
|
|
380
|
+
def node_timings(self) -> dict[str, _NodeTiming]: ...
|
|
381
|
+
@property
|
|
382
|
+
def datafusion_metrics(self) -> list[_DataFusionMetric]: ...
|
|
383
|
+
|
|
384
|
+
def project_json_schema() -> str: ...
|
|
385
|
+
def validate_project_json(project_json: str) -> str: ...
|
|
386
|
+
def version() -> str: ...
|
|
387
|
+
def registered_connectors() -> list[dict[str, object]]: ...
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Logical input policies and owned iterable adapters for expression streams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator, Mapping
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import NoReturn, Union
|
|
8
|
+
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
import pyarrow.compute as pc
|
|
11
|
+
|
|
12
|
+
from calc_flow._compat import TypeAliasType, dataclass
|
|
13
|
+
from calc_flow.compute import TableData, _arrow_table, _table_batch
|
|
14
|
+
from calc_flow.runtime import (
|
|
15
|
+
BoundedOutOfOrderness,
|
|
16
|
+
Cursor,
|
|
17
|
+
Data,
|
|
18
|
+
DisabledWatermarks,
|
|
19
|
+
EdgeBudget,
|
|
20
|
+
NativeWatermarkCapability,
|
|
21
|
+
ReplayPositioning,
|
|
22
|
+
SourceBinding,
|
|
23
|
+
SourceCapabilities,
|
|
24
|
+
SourceDeliveryCapability,
|
|
25
|
+
SourceProvidedWatermarks,
|
|
26
|
+
Watermark,
|
|
27
|
+
WatermarkPolicy,
|
|
28
|
+
)
|
|
29
|
+
from calc_flow.symbolic.analyzer import _schema_fields
|
|
30
|
+
from calc_flow.symbolic.expr import Parameter, TableExpr
|
|
31
|
+
from calc_flow.symbolic.lower.schema import _arrow_schema
|
|
32
|
+
from calc_flow.symbolic.nodes import CStr
|
|
33
|
+
|
|
34
|
+
StreamInput = TypeAliasType(
|
|
35
|
+
"StreamInput", Union[AsyncIterable[Union[TableData, Watermark]], SourceBinding]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_POLICIES = (SourceProvidedWatermarks, BoundedOutOfOrderness, DisabledWatermarks)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _capture_watermarks(
|
|
42
|
+
watermarks: WatermarkPolicy | Mapping[str, WatermarkPolicy] | None,
|
|
43
|
+
) -> WatermarkPolicy | dict[str, WatermarkPolicy] | None:
|
|
44
|
+
return dict(watermarks) if isinstance(watermarks, Mapping) else watermarks
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _policy_mapping(
|
|
48
|
+
watermarks: WatermarkPolicy | Mapping[str, WatermarkPolicy] | None,
|
|
49
|
+
dynamic: set[str],
|
|
50
|
+
) -> Mapping[str, WatermarkPolicy]:
|
|
51
|
+
if watermarks is None:
|
|
52
|
+
return {}
|
|
53
|
+
if isinstance(watermarks, Mapping):
|
|
54
|
+
return watermarks
|
|
55
|
+
if not isinstance(watermarks, _POLICIES):
|
|
56
|
+
raise TypeError("stream.watermarks: expected supported watermark policy")
|
|
57
|
+
if len(dynamic) != 1:
|
|
58
|
+
raise ValueError("stream.watermarks: multiple inputs require a name mapping")
|
|
59
|
+
return {next(iter(dynamic)): watermarks}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _selected_policies(
|
|
63
|
+
inputs: Mapping[str, StreamInput | TableData],
|
|
64
|
+
expected: Mapping[str, TableExpr | Parameter],
|
|
65
|
+
watermarks: WatermarkPolicy | Mapping[str, WatermarkPolicy] | None,
|
|
66
|
+
) -> Mapping[str, WatermarkPolicy]:
|
|
67
|
+
dynamic = {name for name, value in expected.items() if isinstance(value, TableExpr)}
|
|
68
|
+
selected = _policy_mapping(watermarks, dynamic)
|
|
69
|
+
for name, policy in selected.items():
|
|
70
|
+
if name not in dynamic:
|
|
71
|
+
raise ValueError(f"stream.watermarks.{name}: expected dynamic input name")
|
|
72
|
+
if not isinstance(policy, _POLICIES):
|
|
73
|
+
raise TypeError(
|
|
74
|
+
f"stream.watermarks.{name}: expected supported watermark policy"
|
|
75
|
+
)
|
|
76
|
+
if isinstance(inputs[name], SourceBinding):
|
|
77
|
+
raise ValueError(f"stream.watermarks.{name}: cannot override SourceBinding")
|
|
78
|
+
return selected
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class _IterableConfig:
|
|
83
|
+
name: str
|
|
84
|
+
schema: pa.Schema
|
|
85
|
+
budget: EdgeBudget
|
|
86
|
+
policy: WatermarkPolicy
|
|
87
|
+
ordered_column: str | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _source_config(
|
|
91
|
+
name: str,
|
|
92
|
+
value: TableExpr,
|
|
93
|
+
budget: EdgeBudget,
|
|
94
|
+
policy: WatermarkPolicy | None,
|
|
95
|
+
) -> _IterableConfig:
|
|
96
|
+
schema = _arrow_schema(_schema_fields(value._node.attr("schema")))
|
|
97
|
+
event_time = value._node.attr("event_time")
|
|
98
|
+
ordered_column = None
|
|
99
|
+
if policy is None:
|
|
100
|
+
policy = DisabledWatermarks()
|
|
101
|
+
if isinstance(event_time, CStr):
|
|
102
|
+
ordered_column = event_time.value
|
|
103
|
+
policy = BoundedOutOfOrderness(
|
|
104
|
+
ordered_column, timedelta(microseconds=1), timedelta(milliseconds=100)
|
|
105
|
+
)
|
|
106
|
+
return _IterableConfig(name, schema, budget, policy, ordered_column)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _IterableSource:
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
source: AsyncIterable[TableData | Watermark],
|
|
113
|
+
config: _IterableConfig,
|
|
114
|
+
) -> None:
|
|
115
|
+
self._source = source
|
|
116
|
+
self._config = config
|
|
117
|
+
self._path = f"stream.inputs.{config.name}"
|
|
118
|
+
self._iterator: AsyncIterator[TableData | Watermark] | None = None
|
|
119
|
+
self._closed = False
|
|
120
|
+
self._position = 0
|
|
121
|
+
self._last_time: int | None = None
|
|
122
|
+
self.failure: str | None = None
|
|
123
|
+
|
|
124
|
+
def capabilities(self) -> SourceCapabilities:
|
|
125
|
+
watermarks = NativeWatermarkCapability.NEVER_EMITS
|
|
126
|
+
if isinstance(self._config.policy, SourceProvidedWatermarks):
|
|
127
|
+
watermarks = NativeWatermarkCapability.EMITS_NATIVE
|
|
128
|
+
return SourceCapabilities(
|
|
129
|
+
ReplayPositioning.UNSUPPORTED,
|
|
130
|
+
SourceDeliveryCapability.LOSSY,
|
|
131
|
+
max_batch_rows=self._config.budget.max_rows,
|
|
132
|
+
max_batch_bytes=self._config.budget.max_bytes,
|
|
133
|
+
schema=self._config.schema,
|
|
134
|
+
native_watermarks=watermarks,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def open(self, cursor: Cursor | None) -> None:
|
|
138
|
+
self._iterator = self._source.__aiter__()
|
|
139
|
+
|
|
140
|
+
async def next(self) -> Data | Watermark | None:
|
|
141
|
+
if self._iterator is None:
|
|
142
|
+
raise RuntimeError(f"{self._path}: source is not open")
|
|
143
|
+
try:
|
|
144
|
+
data = await self._iterator.__anext__()
|
|
145
|
+
except StopAsyncIteration:
|
|
146
|
+
return None
|
|
147
|
+
if isinstance(data, Watermark):
|
|
148
|
+
return self._watermark(data)
|
|
149
|
+
self._validate_batch(self._arrow_input(data))
|
|
150
|
+
batch = _table_batch(data, self._path)
|
|
151
|
+
self._position += 1
|
|
152
|
+
return Data(batch, Cursor(self._position.to_bytes(16, "big"), {}))
|
|
153
|
+
|
|
154
|
+
def _arrow_input(self, data: TableData) -> pa.Table | pa.RecordBatch:
|
|
155
|
+
try:
|
|
156
|
+
return _arrow_table(data, self._path)
|
|
157
|
+
except TypeError:
|
|
158
|
+
self._reject(": expected Arrow Table, RecordBatch or table Batch")
|
|
159
|
+
|
|
160
|
+
def _watermark(self, value: Watermark) -> Watermark:
|
|
161
|
+
if not isinstance(self._config.policy, SourceProvidedWatermarks):
|
|
162
|
+
self._reject(": Watermark requires SourceProvidedWatermarks")
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
def _validate_batch(self, table: pa.Table | pa.RecordBatch) -> None:
|
|
166
|
+
if not table.schema.equals(self._config.schema):
|
|
167
|
+
self._reject(".schema: expected declared Arrow schema")
|
|
168
|
+
if table.num_rows > self._config.budget.max_rows:
|
|
169
|
+
self._reject(": batch exceeds edge_budget.max_rows")
|
|
170
|
+
if table.nbytes > self._config.budget.max_bytes:
|
|
171
|
+
self._reject(": batch exceeds edge_budget.max_bytes")
|
|
172
|
+
if self._config.ordered_column is not None:
|
|
173
|
+
self._validate_time(table[self._config.ordered_column])
|
|
174
|
+
|
|
175
|
+
def _validate_time(self, column: pa.ChunkedArray | pa.Array) -> None:
|
|
176
|
+
if column.null_count:
|
|
177
|
+
self._reject(".event_time: expected non-null event time")
|
|
178
|
+
# Compare in the declared unit without datetime/range conversion. Native
|
|
179
|
+
# generation retains its checked conversion and minimum-minus-delay error.
|
|
180
|
+
values = column.cast(pa.int64())
|
|
181
|
+
if not len(values):
|
|
182
|
+
return
|
|
183
|
+
if self._is_ordered(values):
|
|
184
|
+
self._last_time = values[len(values) - 1].as_py()
|
|
185
|
+
return
|
|
186
|
+
self._reject_out_of_order(values)
|
|
187
|
+
|
|
188
|
+
def _is_ordered(self, values: pa.ChunkedArray | pa.Array) -> bool:
|
|
189
|
+
if self._last_time is not None and values[0].as_py() < self._last_time:
|
|
190
|
+
return False
|
|
191
|
+
decreases = pc.less(values.slice(1), values.slice(0, len(values) - 1))
|
|
192
|
+
return not pc.any(decreases).as_py()
|
|
193
|
+
|
|
194
|
+
def _reject_out_of_order(self, values: pa.ChunkedArray | pa.Array) -> NoReturn:
|
|
195
|
+
previous = self._last_time
|
|
196
|
+
for current in values.to_pylist():
|
|
197
|
+
if previous is not None and current < previous:
|
|
198
|
+
self._reject(
|
|
199
|
+
".event_time: expected nondecreasing event time; select an "
|
|
200
|
+
"explicit watermark policy for out-of-order input"
|
|
201
|
+
)
|
|
202
|
+
previous = current
|
|
203
|
+
raise RuntimeError(f"{self._path}.event_time: ordering violation not found")
|
|
204
|
+
|
|
205
|
+
def _reject(self, message: str) -> NoReturn:
|
|
206
|
+
self.failure = self._path + message
|
|
207
|
+
raise ValueError(self.failure)
|
|
208
|
+
|
|
209
|
+
async def close(self) -> None:
|
|
210
|
+
if self._closed or self._iterator is None:
|
|
211
|
+
return
|
|
212
|
+
self._closed = True
|
|
213
|
+
close = getattr(self._iterator, "aclose", None)
|
|
214
|
+
if close is not None:
|
|
215
|
+
await close()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _source_binding(
|
|
219
|
+
data: StreamInput,
|
|
220
|
+
name: str,
|
|
221
|
+
value: TableExpr,
|
|
222
|
+
budget: EdgeBudget,
|
|
223
|
+
policy: WatermarkPolicy | None,
|
|
224
|
+
) -> tuple[SourceBinding, _IterableSource | None]:
|
|
225
|
+
if isinstance(data, SourceBinding):
|
|
226
|
+
return data, None
|
|
227
|
+
if not isinstance(data, AsyncIterable):
|
|
228
|
+
raise TypeError(
|
|
229
|
+
f"stream.inputs.{name}: expected async iterable or SourceBinding"
|
|
230
|
+
)
|
|
231
|
+
config = _source_config(name, value, budget, policy)
|
|
232
|
+
source = _IterableSource(data, config)
|
|
233
|
+
return SourceBinding(source, watermark_policy=config.policy), source
|