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/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
+ }