maybeai-sheet-cli 0.19.2__tar.gz → 0.19.3__tar.gz
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.
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/PKG-INFO +1 -1
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/pyproject.toml +1 -1
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/style.py +252 -36
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/endpoints.py +2 -1
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_endpoints.py +6 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_phase2_cli.py +277 -7
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/.gitignore +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/README.md +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/__init__.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/chart_config.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/chart_style_summary.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/cli.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/client.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/__init__.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/_phase2.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/cell.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/chart.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/column.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/dashboard.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/db_table.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/excel_table.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/excel_worksheet.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/file.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/formula.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/history.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/image.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/media.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/pivot.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/range.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/raw.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/row.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/share.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/sheet.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/table.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/workbook.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/worksheet.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/config.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/dashboard_interactions.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/dashboard_plan.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/errors.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/formatters.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/models/__init__.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/models/share.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/models/sheet.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/models/workbook.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/options.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/protocol.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/resolver.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/table_resolver.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/update.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/build_fixtures.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/flat-records.xlsx +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/mixed-engine.xlsx +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/multi-table-layout.xlsx +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/rows.json +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/simple-grid.xlsx +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/simple-values.json +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_benchmark_runner.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_cli.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_dashboard_plan.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_formatters.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_resolver.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_two_engine_cli.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_update.py +0 -0
- {maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maybeai-sheet-cli
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.3
|
|
4
4
|
Summary: CLI for common MaybeAI spreadsheet operations
|
|
5
5
|
Project-URL: Homepage, https://github.com/OmniMCP-AI/maybeai-sheet-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/OmniMCP-AI/maybeai-sheet-cli
|
|
@@ -51,6 +51,28 @@ _BEAUTIFY_DEFAULTS: dict[str, object] = {
|
|
|
51
51
|
"preset": "clean_data_table",
|
|
52
52
|
"hideGridlines": True,
|
|
53
53
|
}
|
|
54
|
+
_BEAUTIFY_BODY_BANDING: dict[str, object] = {
|
|
55
|
+
"enabled": True,
|
|
56
|
+
"oddRowBackground": "#FFFFFF",
|
|
57
|
+
"evenRowBackground": "#F8FAFC",
|
|
58
|
+
}
|
|
59
|
+
_BEAUTIFY_COLUMN_POLICY: dict[str, object] = {
|
|
60
|
+
"backgroundBySemanticType": False,
|
|
61
|
+
"allowFontColorBySemanticType": True,
|
|
62
|
+
"allowFormatBySemanticType": True,
|
|
63
|
+
"allowAlignmentBySemanticType": True,
|
|
64
|
+
}
|
|
65
|
+
_BEAUTIFY_CONFIG_KEYS = frozenset(
|
|
66
|
+
{
|
|
67
|
+
"defaults",
|
|
68
|
+
"layout",
|
|
69
|
+
"bodyBanding",
|
|
70
|
+
"columnPolicy",
|
|
71
|
+
"headerStyle",
|
|
72
|
+
"categoryStyles",
|
|
73
|
+
"sampling",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
54
76
|
_BEAUTIFY_SAMPLE_RANGE = "A1:ZZ20"
|
|
55
77
|
_BEAUTIFY_HEADER_STYLE: dict[str, object] = {
|
|
56
78
|
"bold": True,
|
|
@@ -78,47 +100,40 @@ _BEAUTIFY_TEXT_STYLE: dict[str, object] = {
|
|
|
78
100
|
"format": "text",
|
|
79
101
|
"horizontal": "left",
|
|
80
102
|
"vertical": "middle",
|
|
81
|
-
"bg_color": "#FFFFFF",
|
|
82
103
|
}
|
|
83
104
|
_BEAUTIFY_DATE_STYLE: dict[str, object] = {
|
|
84
105
|
"format": "date",
|
|
85
106
|
"format_code": "yyyy-mm-dd",
|
|
86
107
|
"horizontal": "center",
|
|
87
|
-
"bg_color": "#F8FAFC",
|
|
88
108
|
}
|
|
89
109
|
_BEAUTIFY_CURRENCY_STYLE: dict[str, object] = {
|
|
90
110
|
"format": "currency",
|
|
91
111
|
"format_code": "$#,##0.00",
|
|
92
112
|
"horizontal": "right",
|
|
93
113
|
"font_color": "#065F46",
|
|
94
|
-
"bg_color": "#ECFDF5",
|
|
95
114
|
}
|
|
96
115
|
_BEAUTIFY_PERCENT_STYLE: dict[str, object] = {
|
|
97
116
|
"format": "percent",
|
|
98
117
|
"format_code": "0.00%",
|
|
99
118
|
"horizontal": "right",
|
|
100
119
|
"font_color": "#1D4ED8",
|
|
101
|
-
"bg_color": "#EFF6FF",
|
|
102
120
|
}
|
|
103
121
|
_BEAUTIFY_INTEGER_STYLE: dict[str, object] = {
|
|
104
122
|
"format": "integer",
|
|
105
123
|
"format_code": "#,##0",
|
|
106
124
|
"horizontal": "right",
|
|
107
125
|
"font_color": "#581C87",
|
|
108
|
-
"bg_color": "#F5F3FF",
|
|
109
126
|
}
|
|
110
127
|
_BEAUTIFY_NUMBER_STYLE: dict[str, object] = {
|
|
111
128
|
"format": "number",
|
|
112
129
|
"format_code": "0.00",
|
|
113
130
|
"horizontal": "right",
|
|
114
131
|
"font_color": "#374151",
|
|
115
|
-
"bg_color": "#F9FAFB",
|
|
116
132
|
}
|
|
117
133
|
_BEAUTIFY_STATUS_STYLE: dict[str, object] = {
|
|
118
134
|
"format": "text",
|
|
119
135
|
"horizontal": "left",
|
|
120
136
|
"font_color": "#92400E",
|
|
121
|
-
"bg_color": "#FEF3C7",
|
|
122
137
|
}
|
|
123
138
|
_BEAUTIFY_STYLE_BY_CATEGORY: dict[str, dict[str, object]] = {
|
|
124
139
|
"text": _BEAUTIFY_TEXT_STYLE,
|
|
@@ -129,6 +144,47 @@ _BEAUTIFY_STYLE_BY_CATEGORY: dict[str, dict[str, object]] = {
|
|
|
129
144
|
"number": _BEAUTIFY_NUMBER_STYLE,
|
|
130
145
|
"status": _BEAUTIFY_STATUS_STYLE,
|
|
131
146
|
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _default_beautify_style_policy() -> dict[str, object]:
|
|
150
|
+
return {
|
|
151
|
+
"defaults": dict(_BEAUTIFY_DEFAULTS),
|
|
152
|
+
"bodyBanding": dict(_BEAUTIFY_BODY_BANDING),
|
|
153
|
+
"columnPolicy": dict(_BEAUTIFY_COLUMN_POLICY),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _merge_dict(base: dict[str, object], override: object, *, option: str) -> dict[str, object]:
|
|
158
|
+
if override is None:
|
|
159
|
+
return dict(base)
|
|
160
|
+
if not isinstance(override, dict):
|
|
161
|
+
raise UsageError(f"{option} must be a JSON object.")
|
|
162
|
+
merged = dict(base)
|
|
163
|
+
merged.update(override)
|
|
164
|
+
return merged
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _beautify_config(config_path: Path | None) -> tuple[dict[str, object] | None, str | None]:
|
|
168
|
+
if config_path is None:
|
|
169
|
+
return None, None
|
|
170
|
+
config = load_json_object_argument(config_path, option="--config")
|
|
171
|
+
unsupported = sorted(set(config) - _BEAUTIFY_CONFIG_KEYS)
|
|
172
|
+
if unsupported:
|
|
173
|
+
raise UsageError(f"--config contains unsupported keys: {', '.join(unsupported)}.")
|
|
174
|
+
return config, str(config_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _beautify_style_policy(config: dict[str, object] | None = None) -> dict[str, object]:
|
|
178
|
+
policy = _default_beautify_style_policy()
|
|
179
|
+
if not config:
|
|
180
|
+
return policy
|
|
181
|
+
if "defaults" in config:
|
|
182
|
+
policy["defaults"] = _merge_dict(policy["defaults"], config["defaults"], option="--config defaults") # type: ignore[arg-type]
|
|
183
|
+
if "bodyBanding" in config:
|
|
184
|
+
policy["bodyBanding"] = _merge_dict(policy["bodyBanding"], config["bodyBanding"], option="--config bodyBanding") # type: ignore[arg-type]
|
|
185
|
+
if "columnPolicy" in config:
|
|
186
|
+
policy["columnPolicy"] = _merge_dict(policy["columnPolicy"], config["columnPolicy"], option="--config columnPolicy") # type: ignore[arg-type]
|
|
187
|
+
return policy
|
|
132
188
|
_BEAUTIFY_KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
133
189
|
"date": (
|
|
134
190
|
"date",
|
|
@@ -580,6 +636,39 @@ def _beautify_workbook_payload(
|
|
|
580
636
|
}
|
|
581
637
|
|
|
582
638
|
|
|
639
|
+
def _is_styleable_worksheet(worksheet: dict[str, object]) -> bool:
|
|
640
|
+
worksheet_name = _worksheet_name(worksheet)
|
|
641
|
+
if not worksheet_name:
|
|
642
|
+
return False
|
|
643
|
+
row_count = _int_payload_value(worksheet, ("row_count", "rows", "total_rows", "max_row"))
|
|
644
|
+
column_count = _int_payload_value(
|
|
645
|
+
worksheet,
|
|
646
|
+
("column_count", "col_count", "columns", "total_columns", "max_column", "max_col"),
|
|
647
|
+
)
|
|
648
|
+
return not ((row_count is not None and row_count <= 0) or (column_count is not None and column_count <= 0))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _select_beautify_worksheets(target, worksheet_response: dict[str, object]) -> list[dict[str, object]]:
|
|
652
|
+
styleable = [worksheet for worksheet in _worksheet_items(worksheet_response) if _is_styleable_worksheet(worksheet)]
|
|
653
|
+
if not styleable:
|
|
654
|
+
if target.worksheet_name:
|
|
655
|
+
raise UsageError(f"No styleable worksheet found for --worksheet-name {target.worksheet_name!r}.")
|
|
656
|
+
raise UsageError("No styleable worksheets found. The workbook may be empty or missing dimensions.")
|
|
657
|
+
if target.worksheet_name:
|
|
658
|
+
selected = [worksheet for worksheet in styleable if _worksheet_name(worksheet) == target.worksheet_name]
|
|
659
|
+
if not selected:
|
|
660
|
+
raise UsageError(f"No styleable worksheet found for --worksheet-name {target.worksheet_name!r}.")
|
|
661
|
+
return selected[:1]
|
|
662
|
+
if target.gid is not None:
|
|
663
|
+
selected = [worksheet for worksheet in styleable if _worksheet_gid(worksheet) == target.gid]
|
|
664
|
+
if not selected:
|
|
665
|
+
raise UsageError(f"No styleable worksheet found for gid {target.gid}.")
|
|
666
|
+
return selected[:1]
|
|
667
|
+
if len(styleable) == 1:
|
|
668
|
+
return styleable
|
|
669
|
+
raise UsageError("Workbook has multiple styleable worksheets. Provide --gid or --worksheet-name.")
|
|
670
|
+
|
|
671
|
+
|
|
583
672
|
def _bounded_table_range_from_shape(column_count: int, row_count: int) -> str:
|
|
584
673
|
if column_count <= 0 or row_count <= 0:
|
|
585
674
|
raise UsageError("Cannot beautify an empty worksheet.")
|
|
@@ -867,6 +956,7 @@ def _excel_column_style_operations(
|
|
|
867
956
|
continue
|
|
868
957
|
operations.append(
|
|
869
958
|
{
|
|
959
|
+
"kind": "semantic_column_style",
|
|
870
960
|
"range_addresses": [
|
|
871
961
|
f"{_column_letters_from_index(run_start)}2:{_column_letters_from_index(one_based_index - 1)}{data_last_row}"
|
|
872
962
|
],
|
|
@@ -877,6 +967,7 @@ def _excel_column_style_operations(
|
|
|
877
967
|
run_style = style
|
|
878
968
|
operations.append(
|
|
879
969
|
{
|
|
970
|
+
"kind": "semantic_column_style",
|
|
880
971
|
"range_addresses": [
|
|
881
972
|
f"{_column_letters_from_index(run_start)}2:{_column_letters_from_index(len(headers))}{data_last_row}"
|
|
882
973
|
],
|
|
@@ -886,7 +977,51 @@ def _excel_column_style_operations(
|
|
|
886
977
|
return operations
|
|
887
978
|
|
|
888
979
|
|
|
889
|
-
def
|
|
980
|
+
def _excel_body_banding_operations(
|
|
981
|
+
*,
|
|
982
|
+
column_count: int,
|
|
983
|
+
last_row: int,
|
|
984
|
+
style_policy: dict[str, object],
|
|
985
|
+
) -> list[dict[str, object]]:
|
|
986
|
+
banding = style_policy.get("bodyBanding")
|
|
987
|
+
if not isinstance(banding, dict) or banding.get("enabled") is False:
|
|
988
|
+
return []
|
|
989
|
+
if column_count <= 0 or last_row < 2:
|
|
990
|
+
return []
|
|
991
|
+
odd_background = str(banding.get("oddRowBackground", "#FFFFFF"))
|
|
992
|
+
even_background = str(banding.get("evenRowBackground", "#F8FAFC"))
|
|
993
|
+
last_column = _column_letters_from_index(column_count)
|
|
994
|
+
odd_ranges: list[str] = []
|
|
995
|
+
even_ranges: list[str] = []
|
|
996
|
+
for row_number in range(2, last_row + 1):
|
|
997
|
+
target = odd_ranges if (row_number - 2) % 2 == 0 else even_ranges
|
|
998
|
+
target.append(f"A{row_number}:{last_column}{row_number}")
|
|
999
|
+
operations: list[dict[str, object]] = []
|
|
1000
|
+
if odd_ranges:
|
|
1001
|
+
operations.append(
|
|
1002
|
+
{
|
|
1003
|
+
"kind": "body_banding",
|
|
1004
|
+
"range_addresses": odd_ranges,
|
|
1005
|
+
"style": {"bg_color": odd_background},
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
if even_ranges:
|
|
1009
|
+
operations.append(
|
|
1010
|
+
{
|
|
1011
|
+
"kind": "body_banding",
|
|
1012
|
+
"range_addresses": even_ranges,
|
|
1013
|
+
"style": {"bg_color": even_background},
|
|
1014
|
+
}
|
|
1015
|
+
)
|
|
1016
|
+
return operations
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _excel_beautify_plan(
|
|
1020
|
+
worksheet: dict[str, object],
|
|
1021
|
+
read_response: dict[str, object] | None = None,
|
|
1022
|
+
*,
|
|
1023
|
+
style_policy: dict[str, object] | None = None,
|
|
1024
|
+
) -> dict[str, object]:
|
|
890
1025
|
headers = _header_values_from_read_response(read_response or {})
|
|
891
1026
|
if not headers:
|
|
892
1027
|
column_count = _int_payload_value(
|
|
@@ -902,10 +1037,12 @@ def _excel_beautify_plan(worksheet: dict[str, object], read_response: dict[str,
|
|
|
902
1037
|
classifications = _excel_column_classifications(headers, read_response)
|
|
903
1038
|
last_row = _range_data_last_row(table_range)
|
|
904
1039
|
header_range = f"A1:{_column_letters_from_index(column_count)}1"
|
|
1040
|
+
resolved_style_policy = style_policy or _default_beautify_style_policy()
|
|
905
1041
|
return {
|
|
906
1042
|
"worksheet_name": _worksheet_name(worksheet),
|
|
907
1043
|
"engine": "excelize",
|
|
908
1044
|
"table_range": table_range,
|
|
1045
|
+
"style_policy": resolved_style_policy,
|
|
909
1046
|
"headers": headers,
|
|
910
1047
|
"columns": [
|
|
911
1048
|
_column_classification_plan(
|
|
@@ -917,14 +1054,17 @@ def _excel_beautify_plan(worksheet: dict[str, object], read_response: dict[str,
|
|
|
917
1054
|
],
|
|
918
1055
|
"operations": [
|
|
919
1056
|
{
|
|
1057
|
+
"kind": "freeze_header",
|
|
920
1058
|
"endpoint": "/api/v1/excel/freeze_panes",
|
|
921
1059
|
"payload": {"freeze_rows": 1, "freeze_columns": 0},
|
|
922
1060
|
},
|
|
923
1061
|
{
|
|
1062
|
+
"kind": "filter",
|
|
924
1063
|
"endpoint": "/api/v1/excel/set_auto_filter",
|
|
925
1064
|
"payload": {"auto_filter": {"ref": table_range}},
|
|
926
1065
|
},
|
|
927
1066
|
{
|
|
1067
|
+
"kind": "header_style",
|
|
928
1068
|
"endpoint": "/api/v1/excel/batch_set_cell_style",
|
|
929
1069
|
"payload": {
|
|
930
1070
|
"range_addresses": [header_range],
|
|
@@ -933,8 +1073,21 @@ def _excel_beautify_plan(worksheet: dict[str, object], read_response: dict[str,
|
|
|
933
1073
|
},
|
|
934
1074
|
*[
|
|
935
1075
|
{
|
|
1076
|
+
"kind": operation.get("kind", "body_banding"),
|
|
1077
|
+
"endpoint": "/api/v1/excel/batch_set_cell_style",
|
|
1078
|
+
"payload": {key: value for key, value in operation.items() if key != "kind"},
|
|
1079
|
+
}
|
|
1080
|
+
for operation in _excel_body_banding_operations(
|
|
1081
|
+
column_count=column_count,
|
|
1082
|
+
last_row=last_row,
|
|
1083
|
+
style_policy=resolved_style_policy,
|
|
1084
|
+
)
|
|
1085
|
+
],
|
|
1086
|
+
*[
|
|
1087
|
+
{
|
|
1088
|
+
"kind": operation.get("kind", "semantic_column_style"),
|
|
936
1089
|
"endpoint": "/api/v1/excel/batch_set_cell_style",
|
|
937
|
-
"payload": operation,
|
|
1090
|
+
"payload": {key: value for key, value in operation.items() if key != "kind"},
|
|
938
1091
|
}
|
|
939
1092
|
for operation in _excel_column_style_operations(headers, classifications, last_row)
|
|
940
1093
|
],
|
|
@@ -942,6 +1095,65 @@ def _excel_beautify_plan(worksheet: dict[str, object], read_response: dict[str,
|
|
|
942
1095
|
}
|
|
943
1096
|
|
|
944
1097
|
|
|
1098
|
+
def _bounded_verify_range(table_range: object) -> str:
|
|
1099
|
+
if not isinstance(table_range, str):
|
|
1100
|
+
return "A1:Z20"
|
|
1101
|
+
end = _range_end(table_range)
|
|
1102
|
+
if end is None:
|
|
1103
|
+
return "A1:Z20"
|
|
1104
|
+
column_count = min(column_index_from_letters(end[0]), column_index_from_letters("Z"))
|
|
1105
|
+
row_count = min(end[1], 20)
|
|
1106
|
+
return f"A1:{_column_letters_from_index(column_count)}{row_count}"
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _excel_beautify_verify(client: MaybeAIClient, *, target, plan: dict[str, object]) -> dict[str, object]:
|
|
1110
|
+
worksheet_name = plan.get("worksheet_name")
|
|
1111
|
+
try:
|
|
1112
|
+
read_payload = base_payload(
|
|
1113
|
+
SimpleNamespace(uri=target.uri, worksheet_name=worksheet_name if isinstance(worksheet_name, str) else None)
|
|
1114
|
+
) | {
|
|
1115
|
+
"range_address": _bounded_verify_range(plan.get("table_range")),
|
|
1116
|
+
"value_render_option": "FORMATTED_VALUE",
|
|
1117
|
+
}
|
|
1118
|
+
read_response = client.post_json("/api/v1/excel/read_sheet", read_payload)
|
|
1119
|
+
return {
|
|
1120
|
+
"success": bool(read_response.get("success", True)),
|
|
1121
|
+
"scope": "worksheet_sample",
|
|
1122
|
+
"endpoint": "/api/v1/excel/read_sheet",
|
|
1123
|
+
"range_address": read_payload["range_address"],
|
|
1124
|
+
"worksheet_name": worksheet_name,
|
|
1125
|
+
"response": read_response,
|
|
1126
|
+
}
|
|
1127
|
+
except Exception as error: # verify is best-effort after a successful apply
|
|
1128
|
+
return {
|
|
1129
|
+
"success": False,
|
|
1130
|
+
"scope": "worksheet_sample",
|
|
1131
|
+
"endpoint": "/api/v1/excel/read_sheet",
|
|
1132
|
+
"worksheet_name": worksheet_name,
|
|
1133
|
+
"error": str(error),
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _pg_beautify_verify(client: MaybeAIClient, *, target, gid: int) -> dict[str, object]:
|
|
1138
|
+
try:
|
|
1139
|
+
metadata_response = client.post_json("/api/v1/excel/table/metadata", _pg_table_metadata_payload(target, gid))
|
|
1140
|
+
return {
|
|
1141
|
+
"success": bool(metadata_response.get("success", True)),
|
|
1142
|
+
"scope": "field_metadata",
|
|
1143
|
+
"endpoint": "/api/v1/excel/table/metadata",
|
|
1144
|
+
"gid": gid,
|
|
1145
|
+
"response": metadata_response,
|
|
1146
|
+
}
|
|
1147
|
+
except Exception as error:
|
|
1148
|
+
return {
|
|
1149
|
+
"success": False,
|
|
1150
|
+
"scope": "field_metadata",
|
|
1151
|
+
"endpoint": "/api/v1/excel/table/metadata",
|
|
1152
|
+
"gid": gid,
|
|
1153
|
+
"error": str(error),
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
|
|
945
1157
|
def _pg_fields_from_metadata(metadata_response: dict[str, object], *, gid: int, worksheet_name: str | None) -> list[dict[str, object]]:
|
|
946
1158
|
tables = metadata_response.get("tables")
|
|
947
1159
|
if not isinstance(tables, list):
|
|
@@ -1021,11 +1233,17 @@ def _pg_beautify_field_update(
|
|
|
1021
1233
|
style = classification.style
|
|
1022
1234
|
existing_property = field.get("property")
|
|
1023
1235
|
property_payload: dict[str, object] = dict(existing_property) if isinstance(existing_property, dict) else {}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1236
|
+
formatter = str(style["format_code"]) if "format_code" in style else None
|
|
1237
|
+
font_color = str(style["font_color"]) if "font_color" in style else None
|
|
1238
|
+
beautify_property = (
|
|
1239
|
+
_field_property_payload(
|
|
1240
|
+
formatter=formatter,
|
|
1241
|
+
color=font_color,
|
|
1242
|
+
background_color=None,
|
|
1243
|
+
width=None,
|
|
1244
|
+
)
|
|
1245
|
+
if formatter is not None or font_color is not None
|
|
1246
|
+
else {}
|
|
1029
1247
|
)
|
|
1030
1248
|
beautify_style = beautify_property.pop("style", None)
|
|
1031
1249
|
property_payload.update(beautify_property)
|
|
@@ -1909,20 +2127,27 @@ def beautify(
|
|
|
1909
2127
|
doc_id: Optional[str] = typer.Option(None, "--doc-id", "--docs-id", help="MaybeAI document ID."),
|
|
1910
2128
|
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
1911
2129
|
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
2130
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
1912
2131
|
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Optional worksheet name to beautify."),
|
|
1913
2132
|
mode: Optional[str] = typer.Option(None, "--mode", help="single_table, multi_table, or auto_detect."),
|
|
1914
2133
|
dry_run: bool = typer.Option(False, "--dry-run", help="Preview the generated beautify plan without applying styles."),
|
|
2134
|
+
verify: bool = typer.Option(False, "--verify", help="Read back a bounded sample after applying styles."),
|
|
2135
|
+
config: Optional[Path] = typer.Option(None, "--config", exists=True, dir_okay=False, help="Optional JSON beautify style policy."),
|
|
1915
2136
|
output: Optional[str] = output_option(),
|
|
1916
2137
|
verbose: Optional[bool] = verbose_option(),
|
|
1917
2138
|
timeout: Optional[float] = timeout_option(),
|
|
1918
2139
|
) -> None:
|
|
1919
2140
|
try:
|
|
2141
|
+
if dry_run and verify:
|
|
2142
|
+
raise UsageError("--dry-run and --verify cannot be combined.")
|
|
2143
|
+
config_payload, config_source = _beautify_config(config)
|
|
2144
|
+
style_policy = _beautify_style_policy(config_payload)
|
|
1920
2145
|
state = common_options(
|
|
1921
2146
|
ctx,
|
|
1922
2147
|
doc_id=doc_id,
|
|
1923
2148
|
url=url,
|
|
1924
2149
|
uri=uri,
|
|
1925
|
-
gid=
|
|
2150
|
+
gid=gid,
|
|
1926
2151
|
worksheet_name=worksheet_name,
|
|
1927
2152
|
output=output,
|
|
1928
2153
|
verbose=verbose,
|
|
@@ -1939,28 +2164,11 @@ def beautify(
|
|
|
1939
2164
|
},
|
|
1940
2165
|
)
|
|
1941
2166
|
_worksheet_style_mode(mode)
|
|
1942
|
-
selected_worksheets
|
|
1943
|
-
for worksheet in _worksheet_items(worksheet_response):
|
|
1944
|
-
worksheet_item_name = _worksheet_name(worksheet)
|
|
1945
|
-
if not worksheet_item_name:
|
|
1946
|
-
continue
|
|
1947
|
-
if worksheet_name and worksheet_item_name != worksheet_name:
|
|
1948
|
-
continue
|
|
1949
|
-
row_count = _int_payload_value(worksheet, ("row_count", "rows", "total_rows", "max_row"))
|
|
1950
|
-
column_count = _int_payload_value(
|
|
1951
|
-
worksheet,
|
|
1952
|
-
("column_count", "col_count", "columns", "total_columns", "max_column", "max_col"),
|
|
1953
|
-
)
|
|
1954
|
-
if (row_count is not None and row_count <= 0) or (column_count is not None and column_count <= 0):
|
|
1955
|
-
continue
|
|
1956
|
-
selected_worksheets.append(worksheet)
|
|
1957
|
-
if not selected_worksheets:
|
|
1958
|
-
if worksheet_name:
|
|
1959
|
-
raise UsageError(f"No styleable worksheet found for --worksheet-name {worksheet_name!r}.")
|
|
1960
|
-
raise UsageError("No styleable worksheets found. The workbook may be empty or missing dimensions.")
|
|
2167
|
+
selected_worksheets = _select_beautify_worksheets(target, worksheet_response)
|
|
1961
2168
|
|
|
1962
2169
|
plans: list[dict[str, object]] = []
|
|
1963
2170
|
responses: list[dict[str, object]] = []
|
|
2171
|
+
verify_results: list[dict[str, object]] = []
|
|
1964
2172
|
if dry_run:
|
|
1965
2173
|
for worksheet in selected_worksheets:
|
|
1966
2174
|
engine = _normalize_engine(worksheet.get("data_engine") or worksheet.get("engine"))
|
|
@@ -1993,6 +2201,7 @@ def beautify(
|
|
|
1993
2201
|
"gid": gid,
|
|
1994
2202
|
"metadata_endpoint": "/api/v1/excel/table/metadata",
|
|
1995
2203
|
"operation": "field formatter/style updates with header layout defaults",
|
|
2204
|
+
"style_policy": style_policy,
|
|
1996
2205
|
"layout": _pg_beautify_layout_plan(len(fields)),
|
|
1997
2206
|
"columns": columns,
|
|
1998
2207
|
"fields": columns,
|
|
@@ -2007,7 +2216,7 @@ def beautify(
|
|
|
2007
2216
|
"value_render_option": "FORMATTED_VALUE",
|
|
2008
2217
|
}
|
|
2009
2218
|
read_response = client.post_json("/api/v1/excel/read_sheet", read_payload)
|
|
2010
|
-
plans.append(_excel_beautify_plan(worksheet, read_response))
|
|
2219
|
+
plans.append(_excel_beautify_plan(worksheet, read_response, style_policy=style_policy))
|
|
2011
2220
|
response: dict[str, object] = {
|
|
2012
2221
|
"success": True,
|
|
2013
2222
|
"dry_run": True,
|
|
@@ -2076,6 +2285,8 @@ def beautify(
|
|
|
2076
2285
|
)
|
|
2077
2286
|
if fallback_reason:
|
|
2078
2287
|
responses[-1]["batch_update_fallback_reason"] = fallback_reason
|
|
2288
|
+
if verify:
|
|
2289
|
+
verify_results.append(_pg_beautify_verify(client, target=target, gid=gid))
|
|
2079
2290
|
continue
|
|
2080
2291
|
|
|
2081
2292
|
read_payload = base_payload(
|
|
@@ -2085,7 +2296,7 @@ def beautify(
|
|
|
2085
2296
|
"value_render_option": "FORMATTED_VALUE",
|
|
2086
2297
|
}
|
|
2087
2298
|
read_response = client.post_json("/api/v1/excel/read_sheet", read_payload)
|
|
2088
|
-
plan = _excel_beautify_plan(worksheet, read_response)
|
|
2299
|
+
plan = _excel_beautify_plan(worksheet, read_response, style_policy=style_policy)
|
|
2089
2300
|
plans.append(plan)
|
|
2090
2301
|
operation_responses: list[dict[str, object]] = []
|
|
2091
2302
|
for operation in plan["operations"]:
|
|
@@ -2109,6 +2320,8 @@ def beautify(
|
|
|
2109
2320
|
"operations": operation_responses,
|
|
2110
2321
|
}
|
|
2111
2322
|
)
|
|
2323
|
+
if verify:
|
|
2324
|
+
verify_results.append(_excel_beautify_verify(client, target=target, plan=plan))
|
|
2112
2325
|
response = {
|
|
2113
2326
|
"success": all(bool(item.get("success", True)) for item in responses),
|
|
2114
2327
|
"styled_worksheet_count": len(responses),
|
|
@@ -2142,7 +2355,10 @@ def beautify(
|
|
|
2142
2355
|
"list_endpoint": list_endpoint,
|
|
2143
2356
|
"worksheet_count": len(plans),
|
|
2144
2357
|
"engine": plans[0].get("engine") if len(plans) == 1 else "mixed",
|
|
2358
|
+
"config_applied": config_payload is not None,
|
|
2359
|
+
**({"config_source": config_source} if config_source else {}),
|
|
2145
2360
|
},
|
|
2361
|
+
verify=verify_results[0] if len(verify_results) == 1 else (verify_results or None),
|
|
2146
2362
|
metadata=get_endpoint_metadata("style.beautify") if state.verbose else None,
|
|
2147
2363
|
),
|
|
2148
2364
|
state.output,
|
|
@@ -181,7 +181,8 @@ COMMAND_ENDPOINTS: dict[str, EndpointMetadata] = {
|
|
|
181
181
|
"POST",
|
|
182
182
|
"/api/v1/excel/list_worksheets + /api/v1/excel/read_sheet + /api/v1/excel/freeze_panes + /api/v1/excel/set_auto_filter + /api/v1/excel/batch_set_cell_style | /api/v1/excel/table/metadata + /api/v1/excel/db_table/field/batch_update",
|
|
183
183
|
dry_run_supported=True,
|
|
184
|
-
|
|
184
|
+
verify_supported=True,
|
|
185
|
+
notes="CLI-composed agent-friendly worksheet beautifier. Excelize worksheets get header freeze, auto-filter, header styling, zebra body policy, and semantic column format/alignment without default column backgrounds. PG/SheetTable worksheets get batched field formatter/style metadata updates with single-field fallback.",
|
|
185
186
|
),
|
|
186
187
|
"style.columns-width": _meta("style.columns-width", "API", "POST", "/api/v1/excel/set_columns_width", dry_run_supported=True),
|
|
187
188
|
"style.rows-height": _meta("style.rows-height", "API", "POST", "/api/v1/excel/set_rows_height", dry_run_supported=True),
|
|
@@ -140,6 +140,12 @@ class EndpointRegistryTests(unittest.TestCase):
|
|
|
140
140
|
self.assertTrue(metadata.large_output)
|
|
141
141
|
self.assertIsNotNone(metadata.default_limit)
|
|
142
142
|
|
|
143
|
+
def test_style_beautify_metadata_supports_optional_verify(self) -> None:
|
|
144
|
+
metadata = get_endpoint_metadata("style.beautify")
|
|
145
|
+
|
|
146
|
+
self.assertTrue(metadata.dry_run_supported)
|
|
147
|
+
self.assertTrue(metadata.verify_supported)
|
|
148
|
+
|
|
143
149
|
def test_missing_phase_2_commands_are_recorded_but_not_implemented(self) -> None:
|
|
144
150
|
for command_id in (
|
|
145
151
|
"range.used-range",
|
|
@@ -25,6 +25,15 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
25
25
|
self.runner = CliRunner()
|
|
26
26
|
self.env = {"MAYBEAI_API_TOKEN": "test-token"}
|
|
27
27
|
|
|
28
|
+
def _assert_no_background_keys(self, payload: object) -> None:
|
|
29
|
+
if isinstance(payload, dict):
|
|
30
|
+
for key, value in payload.items():
|
|
31
|
+
self.assertNotIn(key, {"bg_color", "backgroundColor", "fillColor"})
|
|
32
|
+
self._assert_no_background_keys(value)
|
|
33
|
+
elif isinstance(payload, list):
|
|
34
|
+
for item in payload:
|
|
35
|
+
self._assert_no_background_keys(item)
|
|
36
|
+
|
|
28
37
|
def _write_two_chart_dashboard_spec(self, directory: str, *, worksheet_name: str = "Sales_Dashboard") -> Path:
|
|
29
38
|
spec_path = Path(directory) / "dashboard.json"
|
|
30
39
|
spec_path.write_text(
|
|
@@ -1455,6 +1464,12 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1455
1464
|
{"success": True},
|
|
1456
1465
|
{"success": True},
|
|
1457
1466
|
{"success": True},
|
|
1467
|
+
{"success": True},
|
|
1468
|
+
{"success": True},
|
|
1469
|
+
{"success": True},
|
|
1470
|
+
{"success": True},
|
|
1471
|
+
{"success": True},
|
|
1472
|
+
{"success": True},
|
|
1458
1473
|
]
|
|
1459
1474
|
result = self.runner.invoke(
|
|
1460
1475
|
app,
|
|
@@ -1463,6 +1478,8 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1463
1478
|
"beautify",
|
|
1464
1479
|
"--docs-id",
|
|
1465
1480
|
"abc123",
|
|
1481
|
+
"--worksheet-name",
|
|
1482
|
+
"Summary",
|
|
1466
1483
|
],
|
|
1467
1484
|
env=self.env,
|
|
1468
1485
|
)
|
|
@@ -1475,8 +1492,8 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1475
1492
|
data = json.loads(result.output)
|
|
1476
1493
|
self.assertEqual(data["endpoint"], "/api/v1/excel/beautify_worksheet")
|
|
1477
1494
|
self.assertEqual(data["context"]["source_command"], "style.beautify")
|
|
1478
|
-
self.assertEqual(data["context"]["worksheet_count"],
|
|
1479
|
-
self.assertEqual(data["result"]["styled_worksheet_count"],
|
|
1495
|
+
self.assertEqual(data["context"]["worksheet_count"], 1)
|
|
1496
|
+
self.assertEqual(data["result"]["styled_worksheet_count"], 1)
|
|
1480
1497
|
|
|
1481
1498
|
def test_style_beautify_dry_run_previews_classification_plan_without_applying_styles(self) -> None:
|
|
1482
1499
|
worksheet_response = {
|
|
@@ -1514,6 +1531,255 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1514
1531
|
self.assertGreaterEqual(worksheet_plan["columns"][0]["confidence"], 0.8)
|
|
1515
1532
|
self.assertTrue(worksheet_plan["columns"][0]["reasons"])
|
|
1516
1533
|
|
|
1534
|
+
def test_style_beautify_default_policy_uses_zebra_rows_not_column_backgrounds(self) -> None:
|
|
1535
|
+
worksheet_response = {
|
|
1536
|
+
"success": True,
|
|
1537
|
+
"worksheets": [{"worksheet_name": "Report", "row_count": 4, "column_count": 4, "data_engine": "excelize"}],
|
|
1538
|
+
}
|
|
1539
|
+
read_response = {
|
|
1540
|
+
"success": True,
|
|
1541
|
+
"values": [
|
|
1542
|
+
["Date", "Revenue", "Margin %", "Status"],
|
|
1543
|
+
["2026-01-01", "$100.00", "12.50%", "Active"],
|
|
1544
|
+
["2026-01-02", "$120.00", "9.80%", "Closed"],
|
|
1545
|
+
],
|
|
1546
|
+
"range_address": "A1:D3",
|
|
1547
|
+
}
|
|
1548
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1549
|
+
post_json.side_effect = [worksheet_response, read_response]
|
|
1550
|
+
result = self.runner.invoke(
|
|
1551
|
+
app,
|
|
1552
|
+
[
|
|
1553
|
+
"style",
|
|
1554
|
+
"beautify",
|
|
1555
|
+
"--doc-id",
|
|
1556
|
+
"abc123",
|
|
1557
|
+
"--dry-run",
|
|
1558
|
+
"--output",
|
|
1559
|
+
"json",
|
|
1560
|
+
],
|
|
1561
|
+
env=self.env,
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1565
|
+
data = json.loads(result.output)
|
|
1566
|
+
worksheet_plan = data["result"]["plan"]["worksheets"][0]
|
|
1567
|
+
style_policy = worksheet_plan["style_policy"]
|
|
1568
|
+
self.assertEqual(style_policy["defaults"]["preset"], "clean_data_table")
|
|
1569
|
+
self.assertTrue(style_policy["bodyBanding"]["enabled"])
|
|
1570
|
+
self.assertEqual(style_policy["bodyBanding"]["oddRowBackground"], "#FFFFFF")
|
|
1571
|
+
self.assertEqual(style_policy["bodyBanding"]["evenRowBackground"], "#F8FAFC")
|
|
1572
|
+
self.assertFalse(style_policy["columnPolicy"]["backgroundBySemanticType"])
|
|
1573
|
+
column_operations = [
|
|
1574
|
+
operation
|
|
1575
|
+
for operation in worksheet_plan["operations"]
|
|
1576
|
+
if operation.get("kind") == "semantic_column_style"
|
|
1577
|
+
]
|
|
1578
|
+
self.assertTrue(column_operations)
|
|
1579
|
+
self._assert_no_background_keys(column_operations)
|
|
1580
|
+
banding_operations = [
|
|
1581
|
+
operation
|
|
1582
|
+
for operation in worksheet_plan["operations"]
|
|
1583
|
+
if operation.get("kind") == "body_banding"
|
|
1584
|
+
]
|
|
1585
|
+
self.assertEqual(len(banding_operations), 2)
|
|
1586
|
+
banding_styles = [operation["payload"]["style"]["bg_color"] for operation in banding_operations]
|
|
1587
|
+
self.assertEqual(banding_styles, ["#FFFFFF", "#F8FAFC"])
|
|
1588
|
+
|
|
1589
|
+
def test_style_beautify_url_gid_selects_only_matching_worksheet(self) -> None:
|
|
1590
|
+
worksheet_metadata = {
|
|
1591
|
+
"success": True,
|
|
1592
|
+
"worksheets": [
|
|
1593
|
+
{"gid": 0, "worksheet_name": "Summary", "row_count": 12, "column_count": 2, "data_engine": "excelize"},
|
|
1594
|
+
{"gid": 7, "worksheet_name": "Details", "row_count": 8, "column_count": 2, "data_engine": "excelize"},
|
|
1595
|
+
],
|
|
1596
|
+
}
|
|
1597
|
+
details_read = {"success": True, "values": [["SKU", "Cost"], ["A1", 20]], "range_address": "A1:B2"}
|
|
1598
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1599
|
+
post_json.side_effect = [
|
|
1600
|
+
worksheet_metadata,
|
|
1601
|
+
details_read,
|
|
1602
|
+
{"success": True},
|
|
1603
|
+
{"success": True},
|
|
1604
|
+
{"success": True},
|
|
1605
|
+
{"success": True},
|
|
1606
|
+
{"success": True},
|
|
1607
|
+
{"success": True},
|
|
1608
|
+
{"success": True},
|
|
1609
|
+
]
|
|
1610
|
+
result = self.runner.invoke(
|
|
1611
|
+
app,
|
|
1612
|
+
[
|
|
1613
|
+
"style",
|
|
1614
|
+
"beautify",
|
|
1615
|
+
"--url",
|
|
1616
|
+
"https://www.maybe.ai/docs/spreadsheets/d/abc123?gid=7",
|
|
1617
|
+
"--output",
|
|
1618
|
+
"json",
|
|
1619
|
+
],
|
|
1620
|
+
env=self.env,
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1624
|
+
read_payload = post_json.call_args_list[1].args[1]
|
|
1625
|
+
self.assertEqual(read_payload["worksheet_name"], "Details")
|
|
1626
|
+
data = json.loads(result.output)
|
|
1627
|
+
self.assertEqual(data["context"]["worksheet_count"], 1)
|
|
1628
|
+
self.assertEqual(data["result"]["styled_worksheet_count"], 1)
|
|
1629
|
+
self.assertEqual(data["result"]["worksheet_name"], "Details")
|
|
1630
|
+
|
|
1631
|
+
def test_style_beautify_url_without_gid_rejects_multiple_worksheets(self) -> None:
|
|
1632
|
+
worksheet_metadata = {
|
|
1633
|
+
"success": True,
|
|
1634
|
+
"worksheets": [
|
|
1635
|
+
{"gid": 0, "worksheet_name": "Summary", "row_count": 12, "column_count": 2, "data_engine": "excelize"},
|
|
1636
|
+
{"gid": 7, "worksheet_name": "Details", "row_count": 8, "column_count": 2, "data_engine": "excelize"},
|
|
1637
|
+
],
|
|
1638
|
+
}
|
|
1639
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1640
|
+
post_json.side_effect = [worksheet_metadata]
|
|
1641
|
+
result = self.runner.invoke(
|
|
1642
|
+
app,
|
|
1643
|
+
[
|
|
1644
|
+
"style",
|
|
1645
|
+
"beautify",
|
|
1646
|
+
"--url",
|
|
1647
|
+
"https://www.maybe.ai/docs/spreadsheets/d/abc123",
|
|
1648
|
+
"--output",
|
|
1649
|
+
"json",
|
|
1650
|
+
],
|
|
1651
|
+
env=self.env,
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
self.assertNotEqual(result.exit_code, 0, result.output)
|
|
1655
|
+
self.assertIn("--gid", result.output)
|
|
1656
|
+
self.assertIn("--worksheet-name", result.output)
|
|
1657
|
+
self.assertEqual(len(post_json.call_args_list), 1)
|
|
1658
|
+
|
|
1659
|
+
def test_style_beautify_rejects_dry_run_verify_before_backend_calls(self) -> None:
|
|
1660
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1661
|
+
result = self.runner.invoke(
|
|
1662
|
+
app,
|
|
1663
|
+
[
|
|
1664
|
+
"style",
|
|
1665
|
+
"beautify",
|
|
1666
|
+
"--doc-id",
|
|
1667
|
+
"abc123",
|
|
1668
|
+
"--dry-run",
|
|
1669
|
+
"--verify",
|
|
1670
|
+
"--output",
|
|
1671
|
+
"json",
|
|
1672
|
+
],
|
|
1673
|
+
env=self.env,
|
|
1674
|
+
)
|
|
1675
|
+
|
|
1676
|
+
self.assertNotEqual(result.exit_code, 0, result.output)
|
|
1677
|
+
self.assertIn("--dry-run", result.output)
|
|
1678
|
+
self.assertIn("--verify", result.output)
|
|
1679
|
+
post_json.assert_not_called()
|
|
1680
|
+
|
|
1681
|
+
def test_style_beautify_config_updates_policy_and_context(self) -> None:
|
|
1682
|
+
worksheet_response = {
|
|
1683
|
+
"success": True,
|
|
1684
|
+
"worksheets": [{"worksheet_name": "Report", "row_count": 4, "column_count": 2, "data_engine": "excelize"}],
|
|
1685
|
+
}
|
|
1686
|
+
read_response = {
|
|
1687
|
+
"success": True,
|
|
1688
|
+
"values": [["Name", "Revenue"], ["A", "$100.00"]],
|
|
1689
|
+
"range_address": "A1:B2",
|
|
1690
|
+
}
|
|
1691
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
1692
|
+
config_path = Path(directory) / "beautify-config.json"
|
|
1693
|
+
config_path.write_text(
|
|
1694
|
+
json.dumps(
|
|
1695
|
+
{
|
|
1696
|
+
"bodyBanding": {
|
|
1697
|
+
"enabled": True,
|
|
1698
|
+
"oddRowBackground": "#FFFFFF",
|
|
1699
|
+
"evenRowBackground": "#EEF2FF",
|
|
1700
|
+
},
|
|
1701
|
+
"columnPolicy": {
|
|
1702
|
+
"backgroundBySemanticType": False,
|
|
1703
|
+
"allowFontColorBySemanticType": True,
|
|
1704
|
+
"allowFormatBySemanticType": True,
|
|
1705
|
+
"allowAlignmentBySemanticType": True,
|
|
1706
|
+
},
|
|
1707
|
+
}
|
|
1708
|
+
)
|
|
1709
|
+
)
|
|
1710
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1711
|
+
post_json.side_effect = [worksheet_response, read_response]
|
|
1712
|
+
result = self.runner.invoke(
|
|
1713
|
+
app,
|
|
1714
|
+
[
|
|
1715
|
+
"style",
|
|
1716
|
+
"beautify",
|
|
1717
|
+
"--doc-id",
|
|
1718
|
+
"abc123",
|
|
1719
|
+
"--dry-run",
|
|
1720
|
+
"--config",
|
|
1721
|
+
str(config_path),
|
|
1722
|
+
"--output",
|
|
1723
|
+
"json",
|
|
1724
|
+
],
|
|
1725
|
+
env=self.env,
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1729
|
+
data = json.loads(result.output)
|
|
1730
|
+
self.assertTrue(data["context"]["config_applied"])
|
|
1731
|
+
self.assertTrue(data["context"]["config_source"].endswith("beautify-config.json"))
|
|
1732
|
+
style_policy = data["result"]["plan"]["worksheets"][0]["style_policy"]
|
|
1733
|
+
self.assertEqual(style_policy["bodyBanding"]["evenRowBackground"], "#EEF2FF")
|
|
1734
|
+
self.assertFalse(style_policy["columnPolicy"]["backgroundBySemanticType"])
|
|
1735
|
+
|
|
1736
|
+
def test_style_beautify_verify_reads_back_after_apply(self) -> None:
|
|
1737
|
+
worksheet_metadata = {
|
|
1738
|
+
"success": True,
|
|
1739
|
+
"worksheets": [{"gid": 0, "worksheet_name": "Report", "row_count": 4, "column_count": 2, "data_engine": "excelize"}],
|
|
1740
|
+
}
|
|
1741
|
+
read_response = {
|
|
1742
|
+
"success": True,
|
|
1743
|
+
"values": [["Name", "Revenue"], ["A", "$100.00"]],
|
|
1744
|
+
"range_address": "A1:B2",
|
|
1745
|
+
}
|
|
1746
|
+
verify_response = {"success": True, "values": [["Name", "Revenue"], ["A", "$100.00"]], "range_address": "A1:B4"}
|
|
1747
|
+
with patch("maybeai_sheet.commands.style.MaybeAIClient.post_json") as post_json:
|
|
1748
|
+
post_json.side_effect = [
|
|
1749
|
+
worksheet_metadata,
|
|
1750
|
+
read_response,
|
|
1751
|
+
{"success": True},
|
|
1752
|
+
{"success": True},
|
|
1753
|
+
{"success": True},
|
|
1754
|
+
{"success": True},
|
|
1755
|
+
{"success": True},
|
|
1756
|
+
{"success": True},
|
|
1757
|
+
{"success": True},
|
|
1758
|
+
verify_response,
|
|
1759
|
+
]
|
|
1760
|
+
result = self.runner.invoke(
|
|
1761
|
+
app,
|
|
1762
|
+
[
|
|
1763
|
+
"style",
|
|
1764
|
+
"beautify",
|
|
1765
|
+
"--url",
|
|
1766
|
+
"https://www.maybe.ai/docs/spreadsheets/d/abc123?gid=0",
|
|
1767
|
+
"--verify",
|
|
1768
|
+
"--output",
|
|
1769
|
+
"json",
|
|
1770
|
+
],
|
|
1771
|
+
env=self.env,
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1775
|
+
self.assertEqual(post_json.call_args_list[-1].args[0], "/api/v1/excel/read_sheet")
|
|
1776
|
+
verify_payload = post_json.call_args_list[-1].args[1]
|
|
1777
|
+
self.assertEqual(verify_payload["worksheet_name"], "Report")
|
|
1778
|
+
self.assertEqual(verify_payload["range_address"], "A1:B4")
|
|
1779
|
+
data = json.loads(result.output)
|
|
1780
|
+
self.assertTrue(data["verify"]["success"])
|
|
1781
|
+
self.assertEqual(data["verify"]["scope"], "worksheet_sample")
|
|
1782
|
+
|
|
1517
1783
|
def test_style_beautify_excel_worksheet_applies_header_filter_and_semantic_column_styles(self) -> None:
|
|
1518
1784
|
worksheet_metadata = {
|
|
1519
1785
|
"success": True,
|
|
@@ -1536,6 +1802,8 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1536
1802
|
{"success": True, "operation": "freeze"},
|
|
1537
1803
|
{"success": True, "operation": "filter"},
|
|
1538
1804
|
{"success": True, "operation": "header_style"},
|
|
1805
|
+
{"success": True, "operation": "odd_row_banding"},
|
|
1806
|
+
{"success": True, "operation": "even_row_banding"},
|
|
1539
1807
|
{"success": True, "operation": "date_style"},
|
|
1540
1808
|
{"success": True, "operation": "currency_style"},
|
|
1541
1809
|
{"success": True, "operation": "percent_style"},
|
|
@@ -1616,6 +1884,8 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1616
1884
|
{"success": True, "operation": "freeze"},
|
|
1617
1885
|
{"success": True, "operation": "filter"},
|
|
1618
1886
|
{"success": True, "operation": "header_style"},
|
|
1887
|
+
{"success": True, "operation": "odd_row_banding"},
|
|
1888
|
+
{"success": True, "operation": "even_row_banding"},
|
|
1619
1889
|
{"success": True, "operation": "currency_style"},
|
|
1620
1890
|
{"success": True, "operation": "percent_style"},
|
|
1621
1891
|
{"success": True, "operation": "integer_style"},
|
|
@@ -1642,7 +1912,7 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1642
1912
|
category_by_range = {
|
|
1643
1913
|
payload["range_addresses"][0]: payload["style"]["format"]
|
|
1644
1914
|
for payload in style_payloads
|
|
1645
|
-
if payload["range_addresses"][0] != "A1:E1"
|
|
1915
|
+
if payload["range_addresses"][0] != "A1:E1" and "format" in payload["style"]
|
|
1646
1916
|
}
|
|
1647
1917
|
self.assertEqual(category_by_range["A2:A3"], "currency")
|
|
1648
1918
|
self.assertEqual(category_by_range["B2:B3"], "percent")
|
|
@@ -1808,12 +2078,12 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1808
2078
|
margin_payload = next(payload for payload in update_payloads if payload["name"] == "margin_rate")
|
|
1809
2079
|
self.assertNotIn("formatter", order_id_payload["property"])
|
|
1810
2080
|
self.assertEqual(revenue_payload["property"]["formatter"], "$#,##0.00")
|
|
1811
|
-
self.
|
|
2081
|
+
self.assertNotIn("backgroundColor", revenue_payload["property"]["style"])
|
|
1812
2082
|
self.assertEqual(revenue_payload["property"]["customWidget"], {"type": "sparkline"})
|
|
1813
2083
|
self.assertEqual(revenue_payload["property"]["style"]["borderColor"], "#111111")
|
|
1814
2084
|
self.assertEqual(revenue_payload["property"]["style"]["width"], 188)
|
|
1815
2085
|
self.assertEqual(margin_payload["property"]["formatter"], "0.00%")
|
|
1816
|
-
self.
|
|
2086
|
+
self.assertNotIn("backgroundColor", margin_payload["property"]["style"])
|
|
1817
2087
|
|
|
1818
2088
|
data = json.loads(result.output)
|
|
1819
2089
|
self.assertEqual(data["endpoint"], "/api/v1/excel/db_table/field/batch_update")
|
|
@@ -1893,7 +2163,7 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
1893
2163
|
self.assertEqual(batch_payload["updates"][0]["name"], "event_time")
|
|
1894
2164
|
self.assertEqual(batch_payload["updates"][0]["property"]["formatter"], "yyyy-mm-dd")
|
|
1895
2165
|
self.assertEqual(batch_payload["updates"][1]["property"]["formatter"], "$#,##0.00")
|
|
1896
|
-
self.
|
|
2166
|
+
self.assertNotIn("backgroundColor", batch_payload["updates"][2]["property"]["style"])
|
|
1897
2167
|
|
|
1898
2168
|
data = json.loads(result.output)
|
|
1899
2169
|
self.assertEqual(data["endpoint"], "/api/v1/excel/db_table/field/batch_update")
|
|
@@ -2154,7 +2424,7 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
2154
2424
|
self.assertEqual(result.exit_code, 0, result.output)
|
|
2155
2425
|
update_payload = post_json.call_args_list[2].args[1]["updates"][0]
|
|
2156
2426
|
self.assertNotIn("formatter", update_payload["property"])
|
|
2157
|
-
self.
|
|
2427
|
+
self.assertNotIn("backgroundColor", update_payload["property"]["style"])
|
|
2158
2428
|
|
|
2159
2429
|
def test_style_beautify_pg_rejects_unmatched_table_metadata(self) -> None:
|
|
2160
2430
|
worksheets = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/chart_style_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/dashboard.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/db_table.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/excel_table.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/excel_worksheet.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/workbook.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/commands/worksheet.py
RENAMED
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/src/maybeai_sheet/dashboard_interactions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/build_fixtures.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/flat-records.xlsx
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/mixed-engine.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/simple-grid.xlsx
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.19.2 → maybeai_sheet_cli-0.19.3}/tests/fixtures/benchmarks/simple-values.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|