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.
- calc_flow/__init__.py +169 -0
- calc_flow/_native.pyd +0 -0
- calc_flow/_native.pyi +366 -0
- calc_flow/array.py +1324 -0
- calc_flow/capabilities.py +775 -0
- calc_flow/config.py +219 -0
- calc_flow/errors.py +25 -0
- calc_flow/join_spec.py +156 -0
- calc_flow/pipeline.py +1123 -0
- calc_flow/py.typed +0 -0
- calc_flow/runtime.py +979 -0
- calc_flow/store.py +138 -0
- calc_flow/symbolic/__init__.py +65 -0
- calc_flow/symbolic/_generated_rolling_kernels.py +23 -0
- calc_flow/symbolic/analyzer.py +2280 -0
- calc_flow/symbolic/domains.py +77 -0
- calc_flow/symbolic/errors.py +58 -0
- calc_flow/symbolic/expr.py +662 -0
- calc_flow/symbolic/lower/__init__.py +31 -0
- calc_flow/symbolic/lower/planners.py +1270 -0
- calc_flow/symbolic/lower/program.py +840 -0
- calc_flow/symbolic/lower/segments.py +836 -0
- calc_flow/symbolic/lower/strategies.py +1472 -0
- calc_flow/symbolic/nodes.py +603 -0
- calc_flow/symbolic/ops.py +1155 -0
- calc_flow/symbolic/optimizer.py +600 -0
- calc_flow/symbolic/program.py +377 -0
- calc_flow/symbolic/types.py +110 -0
- calc_flow/symbolic/windows.py +153 -0
- calc_flow/udf.py +19 -0
- calc_flow_python-4.0.0.dist-info/METADATA +376 -0
- calc_flow_python-4.0.0.dist-info/RECORD +35 -0
- calc_flow_python-4.0.0.dist-info/WHEEL +4 -0
- calc_flow_python-4.0.0.dist-info/licenses/LICENSE +202 -0
- calc_flow_python-4.0.0.dist-info/sboms/calc-flow-python.cyclonedx.json +10081 -0
calc_flow/config.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import GetJsonSchemaHandler, RootModel, ValidationError, model_validator
|
|
8
|
+
from pydantic_core import CoreSchema, PydanticCustomError
|
|
9
|
+
|
|
10
|
+
from calc_flow import _native
|
|
11
|
+
|
|
12
|
+
type JSONValue = (
|
|
13
|
+
None | bool | int | float | str | list[JSONValue] | dict[str, JSONValue]
|
|
14
|
+
)
|
|
15
|
+
_MAX_JSON_DEPTH = 32
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _json_error(message: str) -> ValueError:
|
|
19
|
+
return ValueError(message)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_issue_path(path: str) -> tuple[str | int, ...]:
|
|
23
|
+
dotted = path.replace("[", ".").replace("]", "")
|
|
24
|
+
parts: list[str | int] = []
|
|
25
|
+
for segment in dotted.split("."):
|
|
26
|
+
if segment == "":
|
|
27
|
+
continue
|
|
28
|
+
parts.append(int(segment) if segment.isdigit() else segment)
|
|
29
|
+
return tuple(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _native_issues(error: Exception) -> tuple[dict[str, str], ...]:
|
|
33
|
+
raw = getattr(error, "issues", ())
|
|
34
|
+
return tuple(
|
|
35
|
+
issue
|
|
36
|
+
for issue in raw
|
|
37
|
+
if isinstance(issue, dict)
|
|
38
|
+
and isinstance(issue.get("path"), str)
|
|
39
|
+
and isinstance(issue.get("code"), str)
|
|
40
|
+
and isinstance(issue.get("message"), str)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _structured_project_error(issues: tuple[dict[str, str], ...]) -> ValidationError:
|
|
45
|
+
return ValidationError.from_exception_data(
|
|
46
|
+
"ProjectDocument",
|
|
47
|
+
[
|
|
48
|
+
{
|
|
49
|
+
"type": PydanticCustomError(issue["code"], issue["message"]),
|
|
50
|
+
"loc": _parse_issue_path(issue["path"]),
|
|
51
|
+
"input": None,
|
|
52
|
+
}
|
|
53
|
+
for issue in issues
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_json_value(value: object) -> None:
|
|
59
|
+
pending: list[tuple[object, int, frozenset[int]]] = [(value, 0, frozenset())]
|
|
60
|
+
while pending:
|
|
61
|
+
current, depth, ancestors = pending.pop()
|
|
62
|
+
if depth > _MAX_JSON_DEPTH:
|
|
63
|
+
raise _json_error(
|
|
64
|
+
f"project exceeds the maximum JSON depth of {_MAX_JSON_DEPTH}"
|
|
65
|
+
)
|
|
66
|
+
if current is None or isinstance(current, (bool, str)):
|
|
67
|
+
continue
|
|
68
|
+
if isinstance(current, int):
|
|
69
|
+
if not -(2**63) <= current <= 2**64 - 1:
|
|
70
|
+
raise _json_error("project integer is outside the portable JSON range")
|
|
71
|
+
continue
|
|
72
|
+
if isinstance(current, float):
|
|
73
|
+
if not math.isfinite(current):
|
|
74
|
+
raise _json_error("project JSON numbers must be finite")
|
|
75
|
+
continue
|
|
76
|
+
if type(current) is dict:
|
|
77
|
+
identity = id(current)
|
|
78
|
+
if identity in ancestors:
|
|
79
|
+
raise _json_error("project contains a cycle")
|
|
80
|
+
nested_ancestors = ancestors | {identity}
|
|
81
|
+
for key, child in current.items():
|
|
82
|
+
if not isinstance(key, str):
|
|
83
|
+
raise _json_error("project JSON object keys must be strings")
|
|
84
|
+
pending.append((child, depth + 1, nested_ancestors))
|
|
85
|
+
continue
|
|
86
|
+
if type(current) is list:
|
|
87
|
+
identity = id(current)
|
|
88
|
+
if identity in ancestors:
|
|
89
|
+
raise _json_error("project contains a cycle")
|
|
90
|
+
nested_ancestors = ancestors | {identity}
|
|
91
|
+
pending.extend((child, depth + 1, nested_ancestors) for child in current)
|
|
92
|
+
continue
|
|
93
|
+
raise _json_error(
|
|
94
|
+
f"project contains a non-JSON value of type {type(current).__name__}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _canonicalize(value: object) -> dict[str, JSONValue]:
|
|
99
|
+
_validate_json_value(value)
|
|
100
|
+
try:
|
|
101
|
+
encoded = json.dumps(
|
|
102
|
+
value, allow_nan=False, separators=(",", ":"), sort_keys=True
|
|
103
|
+
)
|
|
104
|
+
canonical = _native.validate_project_json(encoded)
|
|
105
|
+
except _native.ConfigError as error:
|
|
106
|
+
issues = _native_issues(error)
|
|
107
|
+
if issues:
|
|
108
|
+
raise _structured_project_error(issues) from error
|
|
109
|
+
raise _json_error(str(error)) from error
|
|
110
|
+
except Exception as error:
|
|
111
|
+
raise _json_error(str(error)) from error
|
|
112
|
+
parsed = json.loads(canonical)
|
|
113
|
+
if not isinstance(parsed, dict):
|
|
114
|
+
raise _json_error("project root must be a JSON object")
|
|
115
|
+
return parsed
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _reject_duplicate_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
119
|
+
result: dict[str, object] = {}
|
|
120
|
+
for key, value in pairs:
|
|
121
|
+
if key in result:
|
|
122
|
+
raise _json_error(f"duplicate JSON object key {key!r}")
|
|
123
|
+
result[key] = value
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _openapi_project_schema(component_name: str) -> dict[str, Any]:
|
|
128
|
+
reference_prefix = f"#/components/schemas/{component_name}/$defs/"
|
|
129
|
+
|
|
130
|
+
def rewrite_references(value: object) -> object:
|
|
131
|
+
if type(value) is dict:
|
|
132
|
+
rewritten = {key: rewrite_references(item) for key, item in value.items()}
|
|
133
|
+
reference = rewritten.get("$ref")
|
|
134
|
+
if isinstance(reference, str) and reference.startswith("#/$defs/"):
|
|
135
|
+
rewritten["$ref"] = reference_prefix + reference.removeprefix(
|
|
136
|
+
"#/$defs/"
|
|
137
|
+
)
|
|
138
|
+
return rewritten
|
|
139
|
+
if type(value) is list:
|
|
140
|
+
return [rewrite_references(item) for item in value]
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
schema = rewrite_references(json.loads(_native.project_json_schema()))
|
|
144
|
+
assert isinstance(schema, dict)
|
|
145
|
+
return schema
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ProjectDocument(RootModel[dict[str, JSONValue]]):
|
|
149
|
+
@model_validator(mode="before")
|
|
150
|
+
@classmethod
|
|
151
|
+
def _validate_with_rust(cls, value: object) -> dict[str, JSONValue]:
|
|
152
|
+
return _canonicalize(value)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def model_validate_json(
|
|
156
|
+
cls,
|
|
157
|
+
json_data: str | bytes | bytearray,
|
|
158
|
+
*,
|
|
159
|
+
strict: bool | None = None,
|
|
160
|
+
extra: str | None = None,
|
|
161
|
+
context: Any | None = None,
|
|
162
|
+
by_alias: bool | None = None,
|
|
163
|
+
by_name: bool | None = None,
|
|
164
|
+
) -> ProjectDocument:
|
|
165
|
+
del extra
|
|
166
|
+
try:
|
|
167
|
+
parsed = json.loads(json_data, object_pairs_hook=_reject_duplicate_pairs)
|
|
168
|
+
except RecursionError as cause:
|
|
169
|
+
validation_error = _json_error(
|
|
170
|
+
"project JSON exceeds the maximum nesting depth"
|
|
171
|
+
)
|
|
172
|
+
raise ValidationError.from_exception_data(
|
|
173
|
+
cls.__name__,
|
|
174
|
+
[
|
|
175
|
+
{
|
|
176
|
+
"type": "value_error",
|
|
177
|
+
"loc": (),
|
|
178
|
+
"input": json_data,
|
|
179
|
+
"ctx": {"error": validation_error},
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
) from cause
|
|
183
|
+
except (TypeError, ValueError, UnicodeDecodeError) as error:
|
|
184
|
+
raise ValidationError.from_exception_data(
|
|
185
|
+
cls.__name__,
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
"type": "value_error",
|
|
189
|
+
"loc": (),
|
|
190
|
+
"input": json_data,
|
|
191
|
+
"ctx": {"error": ValueError(str(error))},
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
) from error
|
|
195
|
+
return cls.model_validate(
|
|
196
|
+
parsed,
|
|
197
|
+
strict=strict,
|
|
198
|
+
context=context,
|
|
199
|
+
by_alias=by_alias,
|
|
200
|
+
by_name=by_name,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def __get_pydantic_json_schema__(
|
|
205
|
+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
|
206
|
+
) -> dict[str, Any]:
|
|
207
|
+
del core_schema, handler
|
|
208
|
+
return _openapi_project_schema(cls.__name__)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def model_json_schema(cls, *args: object, **kwargs: object) -> dict[str, Any]:
|
|
212
|
+
del args, kwargs
|
|
213
|
+
return json.loads(_native.project_json_schema())
|
|
214
|
+
|
|
215
|
+
def canonical_json(self) -> str:
|
|
216
|
+
encoded = json.dumps(
|
|
217
|
+
self.root, allow_nan=False, separators=(",", ":"), sort_keys=True
|
|
218
|
+
)
|
|
219
|
+
return _native.validate_project_json(encoded)
|
calc_flow/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from calc_flow._native import (
|
|
4
|
+
CalcFlowError,
|
|
5
|
+
CancelledError,
|
|
6
|
+
CheckpointError,
|
|
7
|
+
CheckpointPublicationUnknownError,
|
|
8
|
+
CompileError,
|
|
9
|
+
ConfigError,
|
|
10
|
+
ExecutionError,
|
|
11
|
+
ProviderError,
|
|
12
|
+
StreamingRuntimeError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CalcFlowError",
|
|
17
|
+
"CancelledError",
|
|
18
|
+
"CheckpointPublicationUnknownError",
|
|
19
|
+
"CheckpointError",
|
|
20
|
+
"CompileError",
|
|
21
|
+
"ConfigError",
|
|
22
|
+
"ExecutionError",
|
|
23
|
+
"ProviderError",
|
|
24
|
+
"StreamingRuntimeError",
|
|
25
|
+
]
|
calc_flow/join_spec.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""The stream-join wire specification shared by every surface.
|
|
2
|
+
|
|
3
|
+
``calc_flow.pipeline`` (project JSON), ``calc_flow.symbolic`` (node
|
|
4
|
+
attrs), and the lowering path each encode the same bounded inner join
|
|
5
|
+
contract. The value containers, validators, and the single wire-spec
|
|
6
|
+
builder live here so the contract is defined once and the symbolic layer
|
|
7
|
+
no longer imports runtime-side helpers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import timedelta
|
|
15
|
+
|
|
16
|
+
STREAM_JOIN_MAX_SAFE_JSON_INTEGER = 9_007_199_254_740_991
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class JoinTimeBounds:
|
|
21
|
+
"""Inclusive non-negative event-time distances around a left row."""
|
|
22
|
+
|
|
23
|
+
before: timedelta
|
|
24
|
+
after: timedelta
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
timedelta_micros(self.before, "before")
|
|
28
|
+
timedelta_micros(self.after, "after")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class JoinStateLimits:
|
|
33
|
+
"""Required logical state and per-input match limits."""
|
|
34
|
+
|
|
35
|
+
max_state_rows_per_side: int
|
|
36
|
+
max_state_bytes_per_side: int
|
|
37
|
+
max_matches_per_input_batch: int
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
for field_name in (
|
|
41
|
+
"max_state_rows_per_side",
|
|
42
|
+
"max_state_bytes_per_side",
|
|
43
|
+
"max_matches_per_input_batch",
|
|
44
|
+
):
|
|
45
|
+
value = getattr(self, field_name)
|
|
46
|
+
if value.__class__ is not int:
|
|
47
|
+
raise TypeError(f"{field_name} must be an exact int")
|
|
48
|
+
if not 1 <= value <= STREAM_JOIN_MAX_SAFE_JSON_INTEGER:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"{field_name} must be in 1..={STREAM_JOIN_MAX_SAFE_JSON_INTEGER}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class JoinSideWire:
|
|
56
|
+
"""One join side's wire-level keys, event time, and output prefix."""
|
|
57
|
+
|
|
58
|
+
keys: tuple[str, ...]
|
|
59
|
+
event_time: str
|
|
60
|
+
prefix: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def require_equal_key_counts(
|
|
64
|
+
left_keys: Sequence[str], right_keys: Sequence[str]
|
|
65
|
+
) -> None:
|
|
66
|
+
if len(left_keys) != len(right_keys):
|
|
67
|
+
raise ValueError("left_keys and right_keys must have equal length")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def require_event_time_columns(left_event_time: str, right_event_time: str) -> None:
|
|
71
|
+
for field_name, value in (
|
|
72
|
+
("left_event_time", left_event_time),
|
|
73
|
+
("right_event_time", right_event_time),
|
|
74
|
+
):
|
|
75
|
+
if not isinstance(value, str) or not value:
|
|
76
|
+
raise TypeError(f"{field_name} must be a non-empty string")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def require_join_bounds(bounds: JoinTimeBounds) -> None:
|
|
80
|
+
if not isinstance(bounds, JoinTimeBounds):
|
|
81
|
+
raise TypeError("bounds must be a calc_flow.JoinTimeBounds")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def require_join_limits(limits: JoinStateLimits) -> None:
|
|
85
|
+
if not isinstance(limits, JoinStateLimits):
|
|
86
|
+
raise TypeError("limits must be a calc_flow.JoinStateLimits")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _portable_identifier(value: object) -> bool:
|
|
90
|
+
return isinstance(value, str) and value.isidentifier() and value.isascii()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def require_distinct_prefixes(left_prefix: str, right_prefix: str) -> None:
|
|
94
|
+
if not _portable_identifier(left_prefix) or not _portable_identifier(right_prefix):
|
|
95
|
+
raise ValueError("prefixes must be distinct portable identifiers")
|
|
96
|
+
if left_prefix == right_prefix:
|
|
97
|
+
raise ValueError("prefixes must be distinct portable identifiers")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def timedelta_micros(value: timedelta, field_name: str) -> int:
|
|
101
|
+
# Exact-type checks: bool is an int subclass and timedelta subclasses
|
|
102
|
+
# must not satisfy the wire contract, so compare classes directly.
|
|
103
|
+
if value.__class__ is not timedelta:
|
|
104
|
+
raise TypeError(f"{field_name} must be an exact datetime.timedelta")
|
|
105
|
+
micros = (
|
|
106
|
+
value.days * 86_400_000_000 + value.seconds * 1_000_000 + value.microseconds
|
|
107
|
+
)
|
|
108
|
+
if not 0 <= micros <= STREAM_JOIN_MAX_SAFE_JSON_INTEGER:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"{field_name} must resolve to "
|
|
111
|
+
f"0..={STREAM_JOIN_MAX_SAFE_JSON_INTEGER} microseconds"
|
|
112
|
+
)
|
|
113
|
+
return micros
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def bounds_wire(before_micros: int, after_micros: int) -> dict[str, int]:
|
|
117
|
+
"""Build the wire ``bounds`` object from resolved microsecond distances."""
|
|
118
|
+
|
|
119
|
+
return {"before_micros": before_micros, "after_micros": after_micros}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def limits_wire(
|
|
123
|
+
max_state_rows_per_side: int,
|
|
124
|
+
max_state_bytes_per_side: int,
|
|
125
|
+
max_matches_per_input_batch: int,
|
|
126
|
+
) -> dict[str, int]:
|
|
127
|
+
"""Build the wire ``limits`` object from the three resolved limits."""
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"max_state_rows_per_side": max_state_rows_per_side,
|
|
131
|
+
"max_state_bytes_per_side": max_state_bytes_per_side,
|
|
132
|
+
"max_matches_per_input_batch": max_matches_per_input_batch,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def join_wire_spec(
|
|
137
|
+
left: JoinSideWire,
|
|
138
|
+
right: JoinSideWire,
|
|
139
|
+
bounds: Mapping[str, int],
|
|
140
|
+
limits: Mapping[str, int],
|
|
141
|
+
*,
|
|
142
|
+
join_type: str = "inner",
|
|
143
|
+
) -> dict[str, object]:
|
|
144
|
+
"""Build the canonical ``stream_join`` operator wire specification."""
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"join_type": join_type,
|
|
148
|
+
"left_keys": list(left.keys),
|
|
149
|
+
"right_keys": list(right.keys),
|
|
150
|
+
"left_event_time": left.event_time,
|
|
151
|
+
"right_event_time": right.event_time,
|
|
152
|
+
"bounds": dict(bounds),
|
|
153
|
+
"limits": dict(limits),
|
|
154
|
+
"left_prefix": left.prefix,
|
|
155
|
+
"right_prefix": right.prefix,
|
|
156
|
+
}
|