caxton 0.1.0__py3-none-any.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.
- caxton/__init__.py +215 -0
- caxton/__version__.py +1 -0
- caxton/_internal/__init__.py +0 -0
- caxton/_internal/aggregation/__init__.py +11 -0
- caxton/_internal/aggregation/execution.py +315 -0
- caxton/_internal/aggregation/keys.py +80 -0
- caxton/_internal/aggregation/matrices.py +308 -0
- caxton/_internal/aggregation/models.py +69 -0
- caxton/_internal/aggregation/tables.py +233 -0
- caxton/_internal/backends/__init__.py +5 -0
- caxton/_internal/backends/_common.py +27 -0
- caxton/_internal/backends/_xlsx_formats.py +112 -0
- caxton/_internal/backends/openpyxl/__init__.py +25 -0
- caxton/_internal/backends/openpyxl/conditional_formats.py +42 -0
- caxton/_internal/backends/openpyxl/extensions.py +105 -0
- caxton/_internal/backends/openpyxl/footers.py +51 -0
- caxton/_internal/backends/openpyxl/native_tables.py +65 -0
- caxton/_internal/backends/openpyxl/package.py +223 -0
- caxton/_internal/backends/openpyxl/renderer.py +98 -0
- caxton/_internal/backends/openpyxl/rows.py +111 -0
- caxton/_internal/backends/openpyxl/styles.py +118 -0
- caxton/_internal/backends/openpyxl/tables.py +32 -0
- caxton/_internal/backends/openpyxl/template_renderer.py +108 -0
- caxton/_internal/backends/openpyxl/template_workbook.py +479 -0
- caxton/_internal/backends/openpyxl/workbook.py +74 -0
- caxton/_internal/backends/xlsxwriter/__init__.py +8 -0
- caxton/_internal/backends/xlsxwriter/conditional_formats.py +41 -0
- caxton/_internal/backends/xlsxwriter/destination.py +81 -0
- caxton/_internal/backends/xlsxwriter/drawings.py +118 -0
- caxton/_internal/backends/xlsxwriter/execution.py +93 -0
- caxton/_internal/backends/xlsxwriter/footers.py +67 -0
- caxton/_internal/backends/xlsxwriter/native_tables.py +67 -0
- caxton/_internal/backends/xlsxwriter/renderer.py +118 -0
- caxton/_internal/backends/xlsxwriter/rows.py +220 -0
- caxton/_internal/backends/xlsxwriter/styles.py +121 -0
- caxton/_internal/backends/xlsxwriter/tables.py +63 -0
- caxton/_internal/backends/xlsxwriter/workbook.py +45 -0
- caxton/_internal/block_paths.py +24 -0
- caxton/_internal/compiler/__init__.py +5 -0
- caxton/_internal/compiler/formula_resolution.py +246 -0
- caxton/_internal/compiler/spreadsheet.py +690 -0
- caxton/_internal/const.py +144 -0
- caxton/_internal/data/__init__.py +9 -0
- caxton/_internal/data/accessors.py +72 -0
- caxton/_internal/data/sources.py +187 -0
- caxton/_internal/formulas.py +140 -0
- caxton/_internal/layout/__init__.py +21 -0
- caxton/_internal/layout/spreadsheet.py +392 -0
- caxton/_internal/normalization/__init__.py +3 -0
- caxton/_internal/normalization/coordinates.py +56 -0
- caxton/_internal/operations.py +204 -0
- caxton/_internal/rendering.py +35 -0
- caxton/_internal/requirements.py +252 -0
- caxton/_internal/resolver.py +248 -0
- caxton/_internal/semantic/__init__.py +3 -0
- caxton/_internal/semantic/evaluator.py +413 -0
- caxton/_internal/shape.py +14 -0
- caxton/_internal/sinks.py +177 -0
- caxton/_internal/templates/__init__.py +17 -0
- caxton/_internal/templates/xlsx.py +406 -0
- caxton/_internal/validation/__init__.py +5 -0
- caxton/_internal/validation/expressions.py +179 -0
- caxton/_internal/validation/features.py +316 -0
- caxton/_internal/validation/formulas.py +303 -0
- caxton/_internal/validation/spreadsheet.py +31 -0
- caxton/_internal/validation/structure.py +183 -0
- caxton/api/__init__.py +155 -0
- caxton/api/columns.py +145 -0
- caxton/api/operations.py +62 -0
- caxton/api/spreadsheet.py +307 -0
- caxton/api/templates.py +76 -0
- caxton/api/xlsx/__init__.py +19 -0
- caxton/core/__init__.py +11 -0
- caxton/core/_values.py +211 -0
- caxton/core/errors/__init__.py +87 -0
- caxton/core/errors/base.py +42 -0
- caxton/core/errors/data.py +136 -0
- caxton/core/errors/rendering.py +58 -0
- caxton/core/errors/validation.py +171 -0
- caxton/core/errors/warnings.py +14 -0
- caxton/core/formatting/__init__.py +59 -0
- caxton/core/formatting/alignment.py +14 -0
- caxton/core/formatting/display.py +160 -0
- caxton/core/formatting/styles.py +364 -0
- caxton/core/ir/__init__.py +51 -0
- caxton/core/ir/base.py +22 -0
- caxton/core/ir/spreadsheet.py +329 -0
- caxton/core/models/__init__.py +140 -0
- caxton/core/models/_operators.py +98 -0
- caxton/core/models/_validation.py +64 -0
- caxton/core/models/columns.py +233 -0
- caxton/core/models/common.py +38 -0
- caxton/core/models/expressions.py +282 -0
- caxton/core/models/formulas.py +302 -0
- caxton/core/models/spreadsheet.py +527 -0
- caxton/core/models/templates.py +115 -0
- caxton/core/protocols/__init__.py +55 -0
- caxton/core/protocols/data.py +49 -0
- caxton/core/protocols/rendering.py +64 -0
- caxton/core/protocols/templates.py +40 -0
- caxton/core/rendering.py +201 -0
- caxton/core/types/__init__.py +27 -0
- caxton/core/types/base.py +37 -0
- caxton/core/types/boolean.py +11 -0
- caxton/core/types/date.py +11 -0
- caxton/core/types/datetime.py +11 -0
- caxton/core/types/decimal.py +11 -0
- caxton/core/types/duration.py +11 -0
- caxton/core/types/integer.py +11 -0
- caxton/core/types/link.py +11 -0
- caxton/core/types/money.py +22 -0
- caxton/core/types/percentage.py +13 -0
- caxton/core/types/text.py +11 -0
- caxton/core/types/time.py +11 -0
- caxton/core/values.py +22 -0
- caxton/errors.py +63 -0
- caxton/py.typed +0 -0
- caxton/testing/__init__.py +102 -0
- caxton/testing/_artifact.py +297 -0
- caxton/testing/_assertions.py +394 -0
- caxton/testing/_diff.py +40 -0
- caxton/testing/_errors.py +13 -0
- caxton/testing/_layout.py +669 -0
- caxton/testing/_snapshots.py +173 -0
- caxton/testing/_spec.py +631 -0
- caxton/testing/_xlsx.py +186 -0
- caxton/testing/strategies.py +160 -0
- caxton-0.1.0.dist-info/METADATA +145 -0
- caxton-0.1.0.dist-info/RECORD +131 -0
- caxton-0.1.0.dist-info/WHEEL +4 -0
- caxton-0.1.0.dist-info/licenses/LICENSE +21 -0
caxton/__init__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from .api import ( # noqa: WPS347
|
|
2
|
+
AggregateExpr,
|
|
3
|
+
AggregateFunction,
|
|
4
|
+
BlockDirection,
|
|
5
|
+
BorderLine,
|
|
6
|
+
BorderLineStyle,
|
|
7
|
+
Borders,
|
|
8
|
+
CellAlignment,
|
|
9
|
+
ChartKind,
|
|
10
|
+
Column,
|
|
11
|
+
CorporateTheme,
|
|
12
|
+
CustomFormat,
|
|
13
|
+
DateFormat,
|
|
14
|
+
DecimalFormat,
|
|
15
|
+
DocumentTheme,
|
|
16
|
+
Expression,
|
|
17
|
+
FieldRef,
|
|
18
|
+
FillStyle,
|
|
19
|
+
FontStyle,
|
|
20
|
+
Freeze,
|
|
21
|
+
Grouping,
|
|
22
|
+
GroupOrder,
|
|
23
|
+
Matrix,
|
|
24
|
+
MoneyFormat,
|
|
25
|
+
PercentageFormat,
|
|
26
|
+
Style,
|
|
27
|
+
StyleSheet,
|
|
28
|
+
TemplateRepeat,
|
|
29
|
+
TemplateSpecification,
|
|
30
|
+
TimeFormat,
|
|
31
|
+
Total,
|
|
32
|
+
Totals,
|
|
33
|
+
VerticalAlignment,
|
|
34
|
+
absolute,
|
|
35
|
+
boolean,
|
|
36
|
+
chart,
|
|
37
|
+
col,
|
|
38
|
+
custom_format,
|
|
39
|
+
data_source,
|
|
40
|
+
date,
|
|
41
|
+
date_format,
|
|
42
|
+
datetime,
|
|
43
|
+
decimal,
|
|
44
|
+
decimal_format,
|
|
45
|
+
duration,
|
|
46
|
+
field,
|
|
47
|
+
image,
|
|
48
|
+
integer,
|
|
49
|
+
link,
|
|
50
|
+
matrix,
|
|
51
|
+
money,
|
|
52
|
+
money_format,
|
|
53
|
+
path,
|
|
54
|
+
percentage,
|
|
55
|
+
percentage_format,
|
|
56
|
+
ref,
|
|
57
|
+
render,
|
|
58
|
+
repeat,
|
|
59
|
+
sheet,
|
|
60
|
+
sheet_ref,
|
|
61
|
+
spacer,
|
|
62
|
+
spreadsheet,
|
|
63
|
+
stack,
|
|
64
|
+
table,
|
|
65
|
+
table_ref,
|
|
66
|
+
template,
|
|
67
|
+
text,
|
|
68
|
+
time,
|
|
69
|
+
time_format,
|
|
70
|
+
title,
|
|
71
|
+
validate,
|
|
72
|
+
when,
|
|
73
|
+
write,
|
|
74
|
+
xlsx,
|
|
75
|
+
)
|
|
76
|
+
from .core.errors import (
|
|
77
|
+
AggregateEvaluationError,
|
|
78
|
+
AmbiguousTemplateRefError,
|
|
79
|
+
BackendError,
|
|
80
|
+
CaxtonError,
|
|
81
|
+
CaxtonTypeError,
|
|
82
|
+
CaxtonValueError,
|
|
83
|
+
ColumnNotFoundError,
|
|
84
|
+
CyclicColumnError,
|
|
85
|
+
DataEvaluationError,
|
|
86
|
+
DataSourceConsumedError,
|
|
87
|
+
DataSourceError,
|
|
88
|
+
DataSourceIterationError,
|
|
89
|
+
DuplicateColumnError,
|
|
90
|
+
FieldAccessError,
|
|
91
|
+
GroupingError,
|
|
92
|
+
IncompatibleTemplateRefError,
|
|
93
|
+
InvalidOperationError,
|
|
94
|
+
InvalidTemplateRefError,
|
|
95
|
+
MatrixConflictError,
|
|
96
|
+
MissingFieldError,
|
|
97
|
+
MissingTemplateRefError,
|
|
98
|
+
RenderError,
|
|
99
|
+
SchemaError,
|
|
100
|
+
SourceEvaluationError,
|
|
101
|
+
TemplateError,
|
|
102
|
+
TemplateFormatError,
|
|
103
|
+
TemplateRefError,
|
|
104
|
+
UnsupportedDataSourceError,
|
|
105
|
+
UnsupportedFeatureError,
|
|
106
|
+
ValidationError,
|
|
107
|
+
)
|
|
108
|
+
from .core.rendering import ExecutionMode
|
|
109
|
+
|
|
110
|
+
__all__ = (
|
|
111
|
+
"AggregateEvaluationError",
|
|
112
|
+
"AggregateExpr",
|
|
113
|
+
"AggregateFunction",
|
|
114
|
+
"AmbiguousTemplateRefError",
|
|
115
|
+
"BackendError",
|
|
116
|
+
"BlockDirection",
|
|
117
|
+
"BorderLine",
|
|
118
|
+
"BorderLineStyle",
|
|
119
|
+
"Borders",
|
|
120
|
+
"CaxtonError",
|
|
121
|
+
"CaxtonTypeError",
|
|
122
|
+
"CaxtonValueError",
|
|
123
|
+
"CellAlignment",
|
|
124
|
+
"ChartKind",
|
|
125
|
+
"Column",
|
|
126
|
+
"ColumnNotFoundError",
|
|
127
|
+
"CorporateTheme",
|
|
128
|
+
"CustomFormat",
|
|
129
|
+
"CyclicColumnError",
|
|
130
|
+
"DataEvaluationError",
|
|
131
|
+
"DataSourceConsumedError",
|
|
132
|
+
"DataSourceError",
|
|
133
|
+
"DataSourceIterationError",
|
|
134
|
+
"DateFormat",
|
|
135
|
+
"DecimalFormat",
|
|
136
|
+
"DocumentTheme",
|
|
137
|
+
"DuplicateColumnError",
|
|
138
|
+
"ExecutionMode",
|
|
139
|
+
"Expression",
|
|
140
|
+
"FieldAccessError",
|
|
141
|
+
"FieldRef",
|
|
142
|
+
"FillStyle",
|
|
143
|
+
"FontStyle",
|
|
144
|
+
"Freeze",
|
|
145
|
+
"GroupOrder",
|
|
146
|
+
"Grouping",
|
|
147
|
+
"GroupingError",
|
|
148
|
+
"IncompatibleTemplateRefError",
|
|
149
|
+
"InvalidOperationError",
|
|
150
|
+
"InvalidTemplateRefError",
|
|
151
|
+
"Matrix",
|
|
152
|
+
"MatrixConflictError",
|
|
153
|
+
"MissingFieldError",
|
|
154
|
+
"MissingTemplateRefError",
|
|
155
|
+
"MoneyFormat",
|
|
156
|
+
"PercentageFormat",
|
|
157
|
+
"RenderError",
|
|
158
|
+
"SchemaError",
|
|
159
|
+
"SourceEvaluationError",
|
|
160
|
+
"Style",
|
|
161
|
+
"StyleSheet",
|
|
162
|
+
"TemplateError",
|
|
163
|
+
"TemplateFormatError",
|
|
164
|
+
"TemplateRefError",
|
|
165
|
+
"TemplateRepeat",
|
|
166
|
+
"TemplateSpecification",
|
|
167
|
+
"TimeFormat",
|
|
168
|
+
"Total",
|
|
169
|
+
"Totals",
|
|
170
|
+
"UnsupportedDataSourceError",
|
|
171
|
+
"UnsupportedFeatureError",
|
|
172
|
+
"ValidationError",
|
|
173
|
+
"VerticalAlignment",
|
|
174
|
+
"absolute",
|
|
175
|
+
"boolean",
|
|
176
|
+
"chart",
|
|
177
|
+
"col",
|
|
178
|
+
"custom_format",
|
|
179
|
+
"data_source",
|
|
180
|
+
"date",
|
|
181
|
+
"date_format",
|
|
182
|
+
"datetime",
|
|
183
|
+
"decimal",
|
|
184
|
+
"decimal_format",
|
|
185
|
+
"duration",
|
|
186
|
+
"field",
|
|
187
|
+
"image",
|
|
188
|
+
"integer",
|
|
189
|
+
"link",
|
|
190
|
+
"matrix",
|
|
191
|
+
"money",
|
|
192
|
+
"money_format",
|
|
193
|
+
"path",
|
|
194
|
+
"percentage",
|
|
195
|
+
"percentage_format",
|
|
196
|
+
"ref",
|
|
197
|
+
"render",
|
|
198
|
+
"repeat",
|
|
199
|
+
"sheet",
|
|
200
|
+
"sheet_ref",
|
|
201
|
+
"spacer",
|
|
202
|
+
"spreadsheet",
|
|
203
|
+
"stack",
|
|
204
|
+
"table",
|
|
205
|
+
"table_ref",
|
|
206
|
+
"template",
|
|
207
|
+
"text",
|
|
208
|
+
"time",
|
|
209
|
+
"time_format",
|
|
210
|
+
"title",
|
|
211
|
+
"validate",
|
|
212
|
+
"when",
|
|
213
|
+
"write",
|
|
214
|
+
"xlsx",
|
|
215
|
+
)
|
caxton/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .matrices import prepare_matrix
|
|
2
|
+
from .models import PreparedColumn, PreparedTabularData, RelativeMerge
|
|
3
|
+
from .tables import prepare_table
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"PreparedColumn",
|
|
7
|
+
"PreparedTabularData",
|
|
8
|
+
"RelativeMerge",
|
|
9
|
+
"prepare_matrix",
|
|
10
|
+
"prepare_table",
|
|
11
|
+
)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from caxton._internal.semantic import SemanticRowEvaluator
|
|
10
|
+
from caxton.core._values import normalize_cell_value
|
|
11
|
+
from caxton.core.errors import AggregateEvaluationError, CaxtonError, PerformanceWarning
|
|
12
|
+
from caxton.core.models import AggregateExpr, Column, Expression
|
|
13
|
+
from caxton.core.protocols import DataSource
|
|
14
|
+
from caxton.core.values import CellValue
|
|
15
|
+
|
|
16
|
+
_BUFFERED_ROW_WARNING_THRESHOLD = 1_000_000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
20
|
+
class InputRow:
|
|
21
|
+
"""Retained values needed after the original source row is released."""
|
|
22
|
+
|
|
23
|
+
index: int
|
|
24
|
+
values: Mapping[str, CellValue]
|
|
25
|
+
expressions: Mapping[Expression, object]
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
object.__setattr__(
|
|
29
|
+
self,
|
|
30
|
+
"expressions",
|
|
31
|
+
MappingProxyType(dict(self.expressions)),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_rows(
|
|
36
|
+
source: DataSource[Any],
|
|
37
|
+
columns: Sequence[Column],
|
|
38
|
+
evaluator: SemanticRowEvaluator,
|
|
39
|
+
*,
|
|
40
|
+
aggregates: Sequence[AggregateExpr] = (),
|
|
41
|
+
path: str = "table",
|
|
42
|
+
) -> tuple[InputRow, ...]:
|
|
43
|
+
"""Read and evaluate retained row state in exactly one source pass.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Buffered semantic and aggregate-input values.
|
|
47
|
+
"""
|
|
48
|
+
rows: list[InputRow] = []
|
|
49
|
+
column_catalog = {column.id: column for column in columns}
|
|
50
|
+
for index, raw in evaluator.iter_source_rows(source):
|
|
51
|
+
rows.append(
|
|
52
|
+
evaluate_input_row(
|
|
53
|
+
source,
|
|
54
|
+
raw,
|
|
55
|
+
index,
|
|
56
|
+
columns,
|
|
57
|
+
aggregates,
|
|
58
|
+
evaluator,
|
|
59
|
+
column_catalog=column_catalog,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
output = tuple(rows)
|
|
63
|
+
warn_if_large_buffer(len(output), path=path, reason="grouping or aggregation")
|
|
64
|
+
return output
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def evaluate_input_row( # noqa: WPS211
|
|
68
|
+
source: DataSource[Any],
|
|
69
|
+
raw: object,
|
|
70
|
+
index: int,
|
|
71
|
+
columns: Sequence[Column],
|
|
72
|
+
aggregates: Sequence[AggregateExpr],
|
|
73
|
+
evaluator: SemanticRowEvaluator,
|
|
74
|
+
*,
|
|
75
|
+
column_catalog: Mapping[str, Column],
|
|
76
|
+
) -> InputRow:
|
|
77
|
+
"""Evaluate all retained state before releasing an original row.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A buffered input row without the original source object.
|
|
81
|
+
"""
|
|
82
|
+
semantic = evaluator.evaluate_row(
|
|
83
|
+
source,
|
|
84
|
+
raw,
|
|
85
|
+
columns,
|
|
86
|
+
row_index=index,
|
|
87
|
+
column_catalog=column_catalog,
|
|
88
|
+
)
|
|
89
|
+
expressions = _evaluate_aggregate_expressions(
|
|
90
|
+
aggregates,
|
|
91
|
+
source=source,
|
|
92
|
+
raw=raw,
|
|
93
|
+
index=index,
|
|
94
|
+
columns=column_catalog,
|
|
95
|
+
values=semantic.values,
|
|
96
|
+
evaluator=evaluator,
|
|
97
|
+
)
|
|
98
|
+
return InputRow(
|
|
99
|
+
index=index,
|
|
100
|
+
values=semantic.values,
|
|
101
|
+
expressions=expressions,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _evaluate_aggregate_expressions( # noqa: WPS211
|
|
106
|
+
aggregates: Sequence[AggregateExpr],
|
|
107
|
+
*,
|
|
108
|
+
source: DataSource[Any],
|
|
109
|
+
raw: object,
|
|
110
|
+
index: int,
|
|
111
|
+
columns: Mapping[str, Column],
|
|
112
|
+
values: Mapping[str, CellValue],
|
|
113
|
+
evaluator: SemanticRowEvaluator,
|
|
114
|
+
) -> Mapping[Expression, object]:
|
|
115
|
+
filters = _unique_expressions(
|
|
116
|
+
tuple(aggregate.where for aggregate in aggregates),
|
|
117
|
+
)
|
|
118
|
+
output = _evaluate_expression_mapping(
|
|
119
|
+
filters,
|
|
120
|
+
source=source,
|
|
121
|
+
raw=raw,
|
|
122
|
+
index=index,
|
|
123
|
+
columns=columns,
|
|
124
|
+
values=values,
|
|
125
|
+
evaluator=evaluator,
|
|
126
|
+
)
|
|
127
|
+
inputs = _unique_expressions(
|
|
128
|
+
tuple(
|
|
129
|
+
expression
|
|
130
|
+
for aggregate in aggregates
|
|
131
|
+
if aggregate.where is None or bool(output[aggregate.where])
|
|
132
|
+
for expression in aggregate.expressions
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
output.update(
|
|
136
|
+
_evaluate_expression_mapping(
|
|
137
|
+
inputs,
|
|
138
|
+
source=source,
|
|
139
|
+
raw=raw,
|
|
140
|
+
index=index,
|
|
141
|
+
columns=columns,
|
|
142
|
+
values=values,
|
|
143
|
+
evaluator=evaluator,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
return output
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _evaluate_expression_mapping( # noqa: WPS211
|
|
150
|
+
expressions: Sequence[Expression],
|
|
151
|
+
*,
|
|
152
|
+
source: DataSource[Any],
|
|
153
|
+
raw: object,
|
|
154
|
+
index: int,
|
|
155
|
+
columns: Mapping[str, Column],
|
|
156
|
+
values: Mapping[str, CellValue],
|
|
157
|
+
evaluator: SemanticRowEvaluator,
|
|
158
|
+
) -> dict[Expression, object]:
|
|
159
|
+
results = evaluator.evaluate_expressions(
|
|
160
|
+
source,
|
|
161
|
+
raw,
|
|
162
|
+
columns,
|
|
163
|
+
expressions,
|
|
164
|
+
row_index=index,
|
|
165
|
+
values=values,
|
|
166
|
+
)
|
|
167
|
+
return dict(zip(expressions, results, strict=True))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _unique_expressions(
|
|
171
|
+
expressions: Sequence[Expression | None],
|
|
172
|
+
) -> tuple[Expression, ...]:
|
|
173
|
+
output: dict[Expression, None] = {}
|
|
174
|
+
for expression in expressions:
|
|
175
|
+
if expression is not None:
|
|
176
|
+
output.setdefault(expression, None)
|
|
177
|
+
return tuple(output)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def warn_if_large_buffer(row_count: int, *, path: str, reason: str) -> None:
|
|
181
|
+
"""Warn once when a shape-dependent block retains a very large source."""
|
|
182
|
+
if row_count <= _BUFFERED_ROW_WARNING_THRESHOLD:
|
|
183
|
+
return
|
|
184
|
+
warnings.warn(
|
|
185
|
+
f"{path} buffered {row_count:,} rows for {reason}",
|
|
186
|
+
PerformanceWarning,
|
|
187
|
+
stacklevel=3,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def execute_aggregate(
|
|
192
|
+
expression: AggregateExpr,
|
|
193
|
+
rows: Sequence[InputRow],
|
|
194
|
+
*,
|
|
195
|
+
path: str,
|
|
196
|
+
filter_cache: MutableMapping[
|
|
197
|
+
Expression | None,
|
|
198
|
+
tuple[InputRow, ...],
|
|
199
|
+
]
|
|
200
|
+
| None = None,
|
|
201
|
+
) -> CellValue:
|
|
202
|
+
"""Execute one aggregate from captured values.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The normalized aggregate result.
|
|
206
|
+
"""
|
|
207
|
+
included = _included_rows(
|
|
208
|
+
expression.where,
|
|
209
|
+
rows,
|
|
210
|
+
filter_cache=filter_cache,
|
|
211
|
+
)
|
|
212
|
+
if not included and expression.has_default:
|
|
213
|
+
return normalize_cell_value(expression.default)
|
|
214
|
+
inputs = tuple(
|
|
215
|
+
tuple(row.expressions[item] for row in included)
|
|
216
|
+
for item in expression.expressions
|
|
217
|
+
)
|
|
218
|
+
function_name = getattr(
|
|
219
|
+
expression.function,
|
|
220
|
+
"__name__",
|
|
221
|
+
type(expression.function).__name__,
|
|
222
|
+
)
|
|
223
|
+
result = _call_aggregate(
|
|
224
|
+
expression,
|
|
225
|
+
inputs,
|
|
226
|
+
function_name=function_name,
|
|
227
|
+
scope_size=len(included),
|
|
228
|
+
path=path,
|
|
229
|
+
)
|
|
230
|
+
return _normalize_aggregate_result(
|
|
231
|
+
result,
|
|
232
|
+
function_name=function_name,
|
|
233
|
+
scope_size=len(included),
|
|
234
|
+
path=path,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _included_rows(
|
|
239
|
+
where: Expression | None,
|
|
240
|
+
rows: Sequence[InputRow],
|
|
241
|
+
*,
|
|
242
|
+
filter_cache: MutableMapping[
|
|
243
|
+
Expression | None,
|
|
244
|
+
tuple[InputRow, ...],
|
|
245
|
+
]
|
|
246
|
+
| None,
|
|
247
|
+
) -> tuple[InputRow, ...]:
|
|
248
|
+
if filter_cache is not None and where in filter_cache:
|
|
249
|
+
return filter_cache[where]
|
|
250
|
+
included = (
|
|
251
|
+
tuple(rows)
|
|
252
|
+
if where is None
|
|
253
|
+
else tuple(row for row in rows if bool(row.expressions[where]))
|
|
254
|
+
)
|
|
255
|
+
if filter_cache is not None:
|
|
256
|
+
filter_cache[where] = included
|
|
257
|
+
return included
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _call_aggregate(
|
|
261
|
+
expression: AggregateExpr,
|
|
262
|
+
inputs: Sequence[Sequence[object]],
|
|
263
|
+
*,
|
|
264
|
+
function_name: str,
|
|
265
|
+
scope_size: int,
|
|
266
|
+
path: str,
|
|
267
|
+
) -> object:
|
|
268
|
+
try:
|
|
269
|
+
return expression.function(*inputs)
|
|
270
|
+
except CaxtonError:
|
|
271
|
+
raise
|
|
272
|
+
except Exception as error:
|
|
273
|
+
message = f"Aggregate {function_name!r} failed"
|
|
274
|
+
raise AggregateEvaluationError(
|
|
275
|
+
message,
|
|
276
|
+
path=path,
|
|
277
|
+
context={
|
|
278
|
+
"exception_type": type(error).__name__,
|
|
279
|
+
"function": function_name,
|
|
280
|
+
"phase": "callable",
|
|
281
|
+
"scope_size": scope_size,
|
|
282
|
+
},
|
|
283
|
+
) from error
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _normalize_aggregate_result(
|
|
287
|
+
result: object,
|
|
288
|
+
*,
|
|
289
|
+
function_name: str,
|
|
290
|
+
scope_size: int,
|
|
291
|
+
path: str,
|
|
292
|
+
) -> CellValue:
|
|
293
|
+
try:
|
|
294
|
+
return normalize_cell_value(result)
|
|
295
|
+
except Exception as error:
|
|
296
|
+
message = f"Aggregate {function_name!r} returned an unsupported result"
|
|
297
|
+
raise AggregateEvaluationError(
|
|
298
|
+
message,
|
|
299
|
+
path=path,
|
|
300
|
+
context={
|
|
301
|
+
"exception_type": type(error).__name__,
|
|
302
|
+
"function": function_name,
|
|
303
|
+
"phase": "result_normalization",
|
|
304
|
+
"scope_size": scope_size,
|
|
305
|
+
},
|
|
306
|
+
) from error
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
__all__ = (
|
|
310
|
+
"InputRow",
|
|
311
|
+
"evaluate_input_row",
|
|
312
|
+
"execute_aggregate",
|
|
313
|
+
"read_rows",
|
|
314
|
+
"warn_if_large_buffer",
|
|
315
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import decimal
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from typing import Any, TypeAlias, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from caxton.core.errors import GroupingError
|
|
8
|
+
from caxton.core.models import Column, GroupOrder
|
|
9
|
+
from caxton.core.values import CellValue
|
|
10
|
+
|
|
11
|
+
DimensionToken: TypeAlias = tuple[str, object]
|
|
12
|
+
GroupKey: TypeAlias = tuple[CellValue, ...]
|
|
13
|
+
TokenKey: TypeAlias = tuple[DimensionToken, ...]
|
|
14
|
+
_Item = TypeVar("_Item")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dimension_token(value: object) -> DimensionToken:
|
|
18
|
+
"""Return the stable, type-sensitive identity of one dimension value."""
|
|
19
|
+
type_name = f"{type(value).__module__}.{type(value).__qualname__}"
|
|
20
|
+
if isinstance(value, decimal.Decimal):
|
|
21
|
+
return type_name, value.as_tuple()
|
|
22
|
+
if isinstance(value, float):
|
|
23
|
+
if not value:
|
|
24
|
+
return type_name, value.hex().lstrip("-")
|
|
25
|
+
return type_name, value.hex()
|
|
26
|
+
return type_name, value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def order_group_values(
|
|
30
|
+
items: Sequence[_Item],
|
|
31
|
+
column: Column,
|
|
32
|
+
value: Callable[[_Item], CellValue],
|
|
33
|
+
*,
|
|
34
|
+
path: str,
|
|
35
|
+
) -> list[_Item]:
|
|
36
|
+
"""Order dimension items with grouping's nulls-last semantics.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A new list in the declared group order.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
GroupingError: If sorted values cannot be compared.
|
|
43
|
+
"""
|
|
44
|
+
grouping = column.grouping
|
|
45
|
+
if grouping is None or grouping.order is GroupOrder.FIRST_SEEN:
|
|
46
|
+
return list(items)
|
|
47
|
+
non_null = [item for item in items if value(item) is not None]
|
|
48
|
+
nulls = [item for item in items if value(item) is None]
|
|
49
|
+
try:
|
|
50
|
+
ordered = sorted(
|
|
51
|
+
non_null,
|
|
52
|
+
key=lambda item: cast("Any", value(item)),
|
|
53
|
+
reverse=grouping.order is GroupOrder.DESCENDING,
|
|
54
|
+
)
|
|
55
|
+
except TypeError as error:
|
|
56
|
+
message = f"Group column {column.id!r} contains incomparable values"
|
|
57
|
+
raise GroupingError(
|
|
58
|
+
message,
|
|
59
|
+
path=f'{path}.column["{column.id}"].grouping',
|
|
60
|
+
context={
|
|
61
|
+
"column": column.id,
|
|
62
|
+
"order": grouping.order.value,
|
|
63
|
+
"value_types": sorted({type(value(item)).__name__ for item in items}),
|
|
64
|
+
},
|
|
65
|
+
) from error
|
|
66
|
+
return [*ordered, *nulls]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def key_token(key: GroupKey) -> TokenKey:
|
|
70
|
+
"""Return the strict identity of one compound dimension key."""
|
|
71
|
+
return tuple(dimension_token(value) for value in key)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = (
|
|
75
|
+
"GroupKey",
|
|
76
|
+
"TokenKey",
|
|
77
|
+
"dimension_token",
|
|
78
|
+
"key_token",
|
|
79
|
+
"order_group_values",
|
|
80
|
+
)
|