maybeai-sheet-cli 0.13.2__tar.gz → 0.14.0__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.13.2 → maybeai_sheet_cli-0.14.0}/PKG-INFO +7 -3
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/README.md +6 -2
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/pyproject.toml +1 -1
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/__init__.py +1 -1
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/db_table.py +317 -1
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/excel_worksheet.py +12 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/worksheet.py +42 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/endpoints.py +16 -1
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_endpoints.py +1 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_phase2_cli.py +47 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_two_engine_cli.py +182 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/.gitignore +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/chart_config.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/chart_style_summary.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/cli.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/client.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/__init__.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/_phase2.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/cell.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/chart.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/column.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/dashboard.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/excel_table.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/file.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/formula.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/history.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/image.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/range.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/raw.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/row.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/share.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/sheet.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/style.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/table.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/workbook.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/config.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/dashboard_interactions.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/dashboard_plan.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/errors.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/formatters.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/models/__init__.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/models/share.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/models/sheet.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/models/workbook.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/options.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/protocol.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/resolver.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/table_resolver.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/update.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/build_fixtures.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/flat-records.xlsx +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/mixed-engine.xlsx +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/multi-table-layout.xlsx +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/rows.json +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/simple-grid.xlsx +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/simple-values.json +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_benchmark_runner.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_cli.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_dashboard_plan.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_formatters.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_resolver.py +0 -0
- {maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/test_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maybeai-sheet-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
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
|
|
@@ -293,6 +293,7 @@ mbs
|
|
|
293
293
|
├── db_worksheet(AKA db_table)
|
|
294
294
|
│ ├── create API Create a PG-backed table from JSON row data.
|
|
295
295
|
│ ├── create-from-query API Calculate SQL and create a PG-backed table from tabular results.
|
|
296
|
+
│ ├── create-from-range API Read a source worksheet range and create a PG-backed table on a target workbook.
|
|
296
297
|
│ ├── update PLAN Update PG-backed rows from JSON row data.
|
|
297
298
|
│ ├── delete PLAN Delete PG-backed rows/table; requires --yes unless --dry-run.
|
|
298
299
|
│ ├── schema API Infer table schema from a bounded PG read.
|
|
@@ -410,6 +411,7 @@ mbs db-table read --doc-id abc123 --name orders-large --limit 100 --offset 200
|
|
|
410
411
|
mbs db-table insert --doc-id abc123 --name orders-large --rows rows.json # API
|
|
411
412
|
mbs db-table create --doc-id abc123 --name Orders --rows orders.json # API
|
|
412
413
|
mbs db-table create-from-query --doc-id abc123 --name OrderSummary --sql-file order_summary.sql # API
|
|
414
|
+
mbs db-table create-from-range --doc-id target123 --name R_OrderLines_Store1 --source-doc-id source123 --worksheet-name "1店" --range A2:AR423 --header-row 0 --use-header-names --if-exists adopt --verify # API
|
|
413
415
|
mbs db-table update --doc-id abc123 --name Orders --key order_id --rows orders_patch.json # PLAN
|
|
414
416
|
mbs db-table update --doc-id abc123 --name Orders --row-index 42 --rows row_patch.json # PLAN
|
|
415
417
|
mbs db-table update --doc-id abc123 --name Orders --where '{"status":"open"}' --rows status_patch.json # PLAN
|
|
@@ -502,8 +504,10 @@ mbs share create --doc-id abc123 --email teammate@example.com --role viewer # A
|
|
|
502
504
|
limit is 100 rows, and `--limit` / `--offset` are capped at 10,000 to avoid
|
|
503
505
|
accidental large PG scans. `db-table create-from-query` is CLI-composed: it
|
|
504
506
|
calculates a tabular `=SQL(...)` result through `calc-formula`, then creates a
|
|
505
|
-
PG/SheetTable-backed table through `db_table/create`.
|
|
506
|
-
|
|
507
|
+
PG/SheetTable-backed table through `db_table/create`. `db-table create-from-range`
|
|
508
|
+
is also CLI-composed: it reads a source worksheet range, reshapes header/data
|
|
509
|
+
rows, then creates a PG/SheetTable on the target workbook. Use a keyset-style
|
|
510
|
+
backend contract for deeper pagination when it becomes available.
|
|
507
511
|
|
|
508
512
|
Refactor planning is tracked separately from this README:
|
|
509
513
|
`docs/superpowers/plans/2026-07-01-mbs-two-engine-command-refactor.md`.
|
|
@@ -269,6 +269,7 @@ mbs
|
|
|
269
269
|
├── db_worksheet(AKA db_table)
|
|
270
270
|
│ ├── create API Create a PG-backed table from JSON row data.
|
|
271
271
|
│ ├── create-from-query API Calculate SQL and create a PG-backed table from tabular results.
|
|
272
|
+
│ ├── create-from-range API Read a source worksheet range and create a PG-backed table on a target workbook.
|
|
272
273
|
│ ├── update PLAN Update PG-backed rows from JSON row data.
|
|
273
274
|
│ ├── delete PLAN Delete PG-backed rows/table; requires --yes unless --dry-run.
|
|
274
275
|
│ ├── schema API Infer table schema from a bounded PG read.
|
|
@@ -386,6 +387,7 @@ mbs db-table read --doc-id abc123 --name orders-large --limit 100 --offset 200
|
|
|
386
387
|
mbs db-table insert --doc-id abc123 --name orders-large --rows rows.json # API
|
|
387
388
|
mbs db-table create --doc-id abc123 --name Orders --rows orders.json # API
|
|
388
389
|
mbs db-table create-from-query --doc-id abc123 --name OrderSummary --sql-file order_summary.sql # API
|
|
390
|
+
mbs db-table create-from-range --doc-id target123 --name R_OrderLines_Store1 --source-doc-id source123 --worksheet-name "1店" --range A2:AR423 --header-row 0 --use-header-names --if-exists adopt --verify # API
|
|
389
391
|
mbs db-table update --doc-id abc123 --name Orders --key order_id --rows orders_patch.json # PLAN
|
|
390
392
|
mbs db-table update --doc-id abc123 --name Orders --row-index 42 --rows row_patch.json # PLAN
|
|
391
393
|
mbs db-table update --doc-id abc123 --name Orders --where '{"status":"open"}' --rows status_patch.json # PLAN
|
|
@@ -478,8 +480,10 @@ mbs share create --doc-id abc123 --email teammate@example.com --role viewer # A
|
|
|
478
480
|
limit is 100 rows, and `--limit` / `--offset` are capped at 10,000 to avoid
|
|
479
481
|
accidental large PG scans. `db-table create-from-query` is CLI-composed: it
|
|
480
482
|
calculates a tabular `=SQL(...)` result through `calc-formula`, then creates a
|
|
481
|
-
PG/SheetTable-backed table through `db_table/create`.
|
|
482
|
-
|
|
483
|
+
PG/SheetTable-backed table through `db_table/create`. `db-table create-from-range`
|
|
484
|
+
is also CLI-composed: it reads a source worksheet range, reshapes header/data
|
|
485
|
+
rows, then creates a PG/SheetTable on the target workbook. Use a keyset-style
|
|
486
|
+
backend contract for deeper pagination when it becomes available.
|
|
483
487
|
|
|
484
488
|
Refactor planning is tracked separately from this README:
|
|
485
489
|
`docs/superpowers/plans/2026-07-01-mbs-two-engine-command-refactor.md`.
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/db_table.py
RENAMED
|
@@ -12,7 +12,13 @@ from ..endpoints import get_endpoint_metadata
|
|
|
12
12
|
from ..errors import APIRequestError, UsageError
|
|
13
13
|
from ..formatters import build_command_output, handle_cli_error, load_json_file, render_output
|
|
14
14
|
from ..options import output_option, resolve_command_state, timeout_option, verbose_option
|
|
15
|
-
from ..resolver import
|
|
15
|
+
from ..resolver import (
|
|
16
|
+
ResolvedTarget,
|
|
17
|
+
append_gid,
|
|
18
|
+
build_document_url,
|
|
19
|
+
parse_document_target,
|
|
20
|
+
resolve_target,
|
|
21
|
+
)
|
|
16
22
|
from ..table_resolver import (
|
|
17
23
|
ResolvedTableTarget,
|
|
18
24
|
bounded_a1_range,
|
|
@@ -176,6 +182,148 @@ def _load_query(sql: str | None, sql_file: Path | None) -> str:
|
|
|
176
182
|
return sql if sql is not None else sql_file.read_text()
|
|
177
183
|
|
|
178
184
|
|
|
185
|
+
def _positional_column_name(index: int) -> str:
|
|
186
|
+
return f"raw_col_{index + 1:03d}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _header_fallback_column_name(index: int) -> str:
|
|
190
|
+
return f"col_{index + 1:03d}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _values_matrix_from_read_response(response: dict[str, Any]) -> list[list[Any]]:
|
|
194
|
+
for key in ("values", "data", "rows"):
|
|
195
|
+
candidate = response.get(key)
|
|
196
|
+
if isinstance(candidate, list) and (
|
|
197
|
+
not candidate or all(isinstance(row, list) for row in candidate)
|
|
198
|
+
):
|
|
199
|
+
return [list(row) if isinstance(row, list) else [] for row in candidate]
|
|
200
|
+
raise UsageError("Source range readback did not contain a values matrix.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _unique_column_names(names: list[str]) -> list[str]:
|
|
204
|
+
seen: dict[str, int] = {}
|
|
205
|
+
unique: list[str] = []
|
|
206
|
+
for name in names:
|
|
207
|
+
count = seen.get(name, 0)
|
|
208
|
+
seen[name] = count + 1
|
|
209
|
+
unique.append(name if count == 0 else f"{name}_{count + 1}")
|
|
210
|
+
return unique
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_blank_matrix_row(row: list[Any]) -> bool:
|
|
214
|
+
if not row:
|
|
215
|
+
return True
|
|
216
|
+
return all(value is None or str(value).strip() == "" for value in row)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _surface_from_values_matrix(
|
|
220
|
+
values: list[list[Any]],
|
|
221
|
+
*,
|
|
222
|
+
header_row: int = 0,
|
|
223
|
+
use_header_names: bool = False,
|
|
224
|
+
data_start: int | None = None,
|
|
225
|
+
data_end: int | None = None,
|
|
226
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
|
|
227
|
+
if not values:
|
|
228
|
+
raise UsageError("Source range readback is empty.")
|
|
229
|
+
if header_row < 0:
|
|
230
|
+
raise UsageError("--header-row must be at least 0.")
|
|
231
|
+
if header_row >= len(values):
|
|
232
|
+
raise UsageError(f"--header-row {header_row} is out of range (total={len(values)}).")
|
|
233
|
+
|
|
234
|
+
resolved_data_start = header_row + 1 if data_start is None else data_start
|
|
235
|
+
resolved_data_end = len(values) if data_end is None else data_end
|
|
236
|
+
if resolved_data_start < 0:
|
|
237
|
+
raise UsageError("--data-start must be at least 0.")
|
|
238
|
+
if resolved_data_end < resolved_data_start:
|
|
239
|
+
raise UsageError("--data-end must be greater than or equal to --data-start.")
|
|
240
|
+
if resolved_data_end > len(values):
|
|
241
|
+
raise UsageError(f"--data-end {resolved_data_end} is out of range (total={len(values)}).")
|
|
242
|
+
|
|
243
|
+
header_cells = list(values[header_row] or [])
|
|
244
|
+
width = len(header_cells)
|
|
245
|
+
for row in values[resolved_data_start:resolved_data_end]:
|
|
246
|
+
if isinstance(row, list):
|
|
247
|
+
width = max(width, len(row))
|
|
248
|
+
if width == 0:
|
|
249
|
+
raise UsageError("Source range header row did not contain any columns.")
|
|
250
|
+
|
|
251
|
+
while len(header_cells) < width:
|
|
252
|
+
header_cells.append(None)
|
|
253
|
+
|
|
254
|
+
if use_header_names:
|
|
255
|
+
raw_names = [
|
|
256
|
+
str(cell or "").strip() or _header_fallback_column_name(index)
|
|
257
|
+
for index, cell in enumerate(header_cells)
|
|
258
|
+
]
|
|
259
|
+
column_names = _unique_column_names(raw_names)
|
|
260
|
+
else:
|
|
261
|
+
column_names = [_positional_column_name(index) for index in range(width)]
|
|
262
|
+
|
|
263
|
+
rows: list[dict[str, Any]] = []
|
|
264
|
+
dropped_blank = 0
|
|
265
|
+
for raw_row in values[resolved_data_start:resolved_data_end]:
|
|
266
|
+
cells = list(raw_row) if isinstance(raw_row, list) else []
|
|
267
|
+
if _is_blank_matrix_row(cells):
|
|
268
|
+
dropped_blank += 1
|
|
269
|
+
continue
|
|
270
|
+
row: dict[str, Any] = {}
|
|
271
|
+
for index, name in enumerate(column_names):
|
|
272
|
+
value = cells[index] if index < len(cells) else None
|
|
273
|
+
row[name] = "" if value is None else value
|
|
274
|
+
rows.append(row)
|
|
275
|
+
|
|
276
|
+
columns = [
|
|
277
|
+
{
|
|
278
|
+
"name": name,
|
|
279
|
+
"logical_type": "text",
|
|
280
|
+
"index": index,
|
|
281
|
+
}
|
|
282
|
+
for index, name in enumerate(column_names)
|
|
283
|
+
]
|
|
284
|
+
summary = {
|
|
285
|
+
"header_row": header_row,
|
|
286
|
+
"use_header_names": use_header_names,
|
|
287
|
+
"data_start": resolved_data_start,
|
|
288
|
+
"data_end": resolved_data_end,
|
|
289
|
+
"column_count": len(columns),
|
|
290
|
+
"row_count": len(rows),
|
|
291
|
+
"rows_dropped_blank": dropped_blank,
|
|
292
|
+
"column_names": column_names,
|
|
293
|
+
}
|
|
294
|
+
return columns, rows, summary
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _resolve_source_workbook_target(
|
|
298
|
+
*,
|
|
299
|
+
source_doc_id: str | None,
|
|
300
|
+
source_url: str | None,
|
|
301
|
+
worksheet_name: str,
|
|
302
|
+
) -> ResolvedTarget:
|
|
303
|
+
if bool(source_doc_id) == bool(source_url):
|
|
304
|
+
raise UsageError("Provide exactly one of --source-doc-id or --source-url.")
|
|
305
|
+
if source_url:
|
|
306
|
+
document_id, _gid = parse_document_target(source_url)
|
|
307
|
+
if not document_id:
|
|
308
|
+
raise UsageError("--source-url must include a MaybeAI document id.")
|
|
309
|
+
return ResolvedTarget(
|
|
310
|
+
document_id=document_id,
|
|
311
|
+
url=source_url,
|
|
312
|
+
uri=source_url,
|
|
313
|
+
gid=None,
|
|
314
|
+
worksheet_name=worksheet_name,
|
|
315
|
+
)
|
|
316
|
+
assert source_doc_id is not None
|
|
317
|
+
uri = build_document_url(source_doc_id)
|
|
318
|
+
return ResolvedTarget(
|
|
319
|
+
document_id=source_doc_id,
|
|
320
|
+
url=uri,
|
|
321
|
+
uri=uri,
|
|
322
|
+
gid=None,
|
|
323
|
+
worksheet_name=worksheet_name,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
179
327
|
def _first_tabular_value(value: Any) -> Any:
|
|
180
328
|
if isinstance(value, list):
|
|
181
329
|
if not value:
|
|
@@ -885,6 +1033,174 @@ def create_from_query(
|
|
|
885
1033
|
handle_cli_error(error)
|
|
886
1034
|
|
|
887
1035
|
|
|
1036
|
+
DB_TABLE_CREATE_FROM_RANGE_EXAMPLE = """Example:
|
|
1037
|
+
|
|
1038
|
+
mbs db-table create-from-range \\
|
|
1039
|
+
--doc-id <target_doc_id> \\
|
|
1040
|
+
--name R_OrderLines_Store1 \\
|
|
1041
|
+
--source-doc-id <source_doc_id> \\
|
|
1042
|
+
--worksheet-name "1店" \\
|
|
1043
|
+
--range A2:AR423 \\
|
|
1044
|
+
--header-row 0 \\
|
|
1045
|
+
--use-header-names \\
|
|
1046
|
+
--if-exists adopt \\
|
|
1047
|
+
--verify \\
|
|
1048
|
+
--output json
|
|
1049
|
+
|
|
1050
|
+
Reads a source worksheet range, treats --header-row as a 0-based index inside
|
|
1051
|
+
the returned values matrix, drops blank rows, and creates a PG/SheetTable on
|
|
1052
|
+
the target --doc-id. Use --use-header-names for semantic headers (merged-title
|
|
1053
|
+
sheets); omit it for positional raw_col_NNN columns.
|
|
1054
|
+
"""
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
@app.command(
|
|
1058
|
+
"create-from-range",
|
|
1059
|
+
help="Create a PG/SheetTable-backed table from a source worksheet range readback.",
|
|
1060
|
+
epilog=DB_TABLE_CREATE_FROM_RANGE_EXAMPLE,
|
|
1061
|
+
)
|
|
1062
|
+
def create_from_range(
|
|
1063
|
+
ctx: typer.Context,
|
|
1064
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="Target MaybeAI document ID."),
|
|
1065
|
+
url: Optional[str] = typer.Option(None, "--url", help="Target MaybeAI workbook URL."),
|
|
1066
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved target workbook URI."),
|
|
1067
|
+
name: str = typer.Option(..., "--name", help="New PG-backed table name on the target workbook."),
|
|
1068
|
+
source_doc_id: Optional[str] = typer.Option(
|
|
1069
|
+
None,
|
|
1070
|
+
"--source-doc-id",
|
|
1071
|
+
help="Source MaybeAI document ID to read the range from.",
|
|
1072
|
+
),
|
|
1073
|
+
source_url: Optional[str] = typer.Option(
|
|
1074
|
+
None,
|
|
1075
|
+
"--source-url",
|
|
1076
|
+
help="Source MaybeAI workbook URL to read the range from.",
|
|
1077
|
+
),
|
|
1078
|
+
worksheet_name: str = typer.Option(
|
|
1079
|
+
...,
|
|
1080
|
+
"--worksheet-name",
|
|
1081
|
+
help="Source worksheet name to read.",
|
|
1082
|
+
),
|
|
1083
|
+
cell_range: str = typer.Option(
|
|
1084
|
+
...,
|
|
1085
|
+
"--range",
|
|
1086
|
+
help="Source A1-style range to read (include the semantic header row).",
|
|
1087
|
+
),
|
|
1088
|
+
header_row: int = typer.Option(
|
|
1089
|
+
0,
|
|
1090
|
+
"--header-row",
|
|
1091
|
+
min=0,
|
|
1092
|
+
help="0-based header row index inside the returned values matrix (not an Excel row number).",
|
|
1093
|
+
),
|
|
1094
|
+
use_header_names: bool = typer.Option(
|
|
1095
|
+
False,
|
|
1096
|
+
"--use-header-names",
|
|
1097
|
+
help="Use header cell text as column names; blank cells become col_NNN. Default uses raw_col_NNN.",
|
|
1098
|
+
),
|
|
1099
|
+
data_start: Optional[int] = typer.Option(
|
|
1100
|
+
None,
|
|
1101
|
+
"--data-start",
|
|
1102
|
+
min=0,
|
|
1103
|
+
help="Optional 0-based data start index inside the values matrix; defaults to header-row+1.",
|
|
1104
|
+
),
|
|
1105
|
+
data_end: Optional[int] = typer.Option(
|
|
1106
|
+
None,
|
|
1107
|
+
"--data-end",
|
|
1108
|
+
min=0,
|
|
1109
|
+
help="Optional 0-based exclusive data end index inside the values matrix; defaults to end.",
|
|
1110
|
+
),
|
|
1111
|
+
if_exists: str = typer.Option("error", "--if-exists", help="Existing table policy: error or adopt."),
|
|
1112
|
+
adopt_existing: bool = typer.Option(False, "--adopt-existing", help="Alias for --if-exists adopt."),
|
|
1113
|
+
verify: bool = typer.Option(False, "--verify", help="Verify PG registry state after create/adopt."),
|
|
1114
|
+
output: Optional[str] = output_option(),
|
|
1115
|
+
verbose: Optional[bool] = verbose_option(),
|
|
1116
|
+
timeout: Optional[float] = timeout_option(),
|
|
1117
|
+
) -> None:
|
|
1118
|
+
try:
|
|
1119
|
+
policy = "adopt" if adopt_existing else if_exists.strip().lower()
|
|
1120
|
+
if policy not in {"error", "adopt"}:
|
|
1121
|
+
raise UsageError("--if-exists must be 'error' or 'adopt'.")
|
|
1122
|
+
state = _state(
|
|
1123
|
+
ctx,
|
|
1124
|
+
doc_id=doc_id,
|
|
1125
|
+
url=url,
|
|
1126
|
+
uri=uri,
|
|
1127
|
+
output=output,
|
|
1128
|
+
verbose=verbose,
|
|
1129
|
+
timeout=timeout,
|
|
1130
|
+
)
|
|
1131
|
+
target_workbook = resolve_target(state)
|
|
1132
|
+
source_workbook = _resolve_source_workbook_target(
|
|
1133
|
+
source_doc_id=source_doc_id,
|
|
1134
|
+
source_url=source_url,
|
|
1135
|
+
worksheet_name=worksheet_name,
|
|
1136
|
+
)
|
|
1137
|
+
client = MaybeAIClient(state)
|
|
1138
|
+
read_endpoint = "/api/v1/excel/read_sheet"
|
|
1139
|
+
read_payload = {
|
|
1140
|
+
"uri": source_workbook.uri,
|
|
1141
|
+
"worksheet_name": worksheet_name,
|
|
1142
|
+
"range_address": cell_range,
|
|
1143
|
+
"value_render_option": "FORMATTED_VALUE",
|
|
1144
|
+
}
|
|
1145
|
+
read_response = client.post_json(read_endpoint, read_payload)
|
|
1146
|
+
values = _values_matrix_from_read_response(read_response)
|
|
1147
|
+
loaded_columns, loaded_rows, surface_summary = _surface_from_values_matrix(
|
|
1148
|
+
values,
|
|
1149
|
+
header_row=header_row,
|
|
1150
|
+
use_header_names=use_header_names,
|
|
1151
|
+
data_start=data_start,
|
|
1152
|
+
data_end=data_end,
|
|
1153
|
+
)
|
|
1154
|
+
create_endpoint = "/api/v1/excel/db_table/create"
|
|
1155
|
+
create_payload = {
|
|
1156
|
+
"document_id": target_workbook.document_id,
|
|
1157
|
+
"uri": target_workbook.uri,
|
|
1158
|
+
"name": name,
|
|
1159
|
+
"columns": loaded_columns,
|
|
1160
|
+
"rows": loaded_rows,
|
|
1161
|
+
"if_exists": policy,
|
|
1162
|
+
}
|
|
1163
|
+
try:
|
|
1164
|
+
response = client.post_json(create_endpoint, create_payload)
|
|
1165
|
+
except APIRequestError as error:
|
|
1166
|
+
response = (
|
|
1167
|
+
_adopt_existing_create_response(error, name=name, columns=loaded_columns)
|
|
1168
|
+
if policy == "adopt"
|
|
1169
|
+
else None
|
|
1170
|
+
)
|
|
1171
|
+
if response is None:
|
|
1172
|
+
raise
|
|
1173
|
+
if verify:
|
|
1174
|
+
resolved = _verify_created_table(client, target_workbook, name)
|
|
1175
|
+
response = {
|
|
1176
|
+
**response,
|
|
1177
|
+
"verified": True,
|
|
1178
|
+
"verified_gid": resolved.gid,
|
|
1179
|
+
"verified_engine": resolved.engine,
|
|
1180
|
+
}
|
|
1181
|
+
context = {
|
|
1182
|
+
"source_command": "db-table.create-from-range",
|
|
1183
|
+
"read_endpoint": read_endpoint,
|
|
1184
|
+
"create_endpoint": create_endpoint,
|
|
1185
|
+
"source_document_id": source_workbook.document_id,
|
|
1186
|
+
"source_worksheet_name": worksheet_name,
|
|
1187
|
+
"source_range": cell_range,
|
|
1188
|
+
**surface_summary,
|
|
1189
|
+
}
|
|
1190
|
+
render_output(
|
|
1191
|
+
build_command_output(
|
|
1192
|
+
endpoint=f"{read_endpoint} + {create_endpoint}",
|
|
1193
|
+
target=_created_table_target(target_workbook, response),
|
|
1194
|
+
result=response,
|
|
1195
|
+
context=context,
|
|
1196
|
+
metadata=get_endpoint_metadata("db-table.create-from-range") if state.verbose else None,
|
|
1197
|
+
),
|
|
1198
|
+
state.output,
|
|
1199
|
+
)
|
|
1200
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
1201
|
+
handle_cli_error(error)
|
|
1202
|
+
|
|
1203
|
+
|
|
888
1204
|
@app.command(
|
|
889
1205
|
"update",
|
|
890
1206
|
help="Planned: update PG/SheetTable-backed rows from JSON row data.",
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/excel_worksheet.py
RENAMED
|
@@ -454,6 +454,16 @@ def copy(
|
|
|
454
454
|
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
455
455
|
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name to copy."),
|
|
456
456
|
new_name: str = typer.Option(..., "--new-name", help="Name for copied worksheet."),
|
|
457
|
+
target_doc_id: Optional[str] = typer.Option(
|
|
458
|
+
None,
|
|
459
|
+
"--target-doc-id",
|
|
460
|
+
help="Optional target document ID. Cross-document copy is currently unsupported.",
|
|
461
|
+
),
|
|
462
|
+
target_url: Optional[str] = typer.Option(
|
|
463
|
+
None,
|
|
464
|
+
"--target-url",
|
|
465
|
+
help="Optional target workbook URL. Cross-document copy is currently unsupported.",
|
|
466
|
+
),
|
|
457
467
|
dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
|
|
458
468
|
output: Optional[str] = output_option(),
|
|
459
469
|
verbose: Optional[bool] = verbose_option(),
|
|
@@ -467,6 +477,8 @@ def copy(
|
|
|
467
477
|
gid=gid,
|
|
468
478
|
worksheet_name=worksheet_name,
|
|
469
479
|
new_name=new_name,
|
|
480
|
+
target_doc_id=target_doc_id,
|
|
481
|
+
target_url=target_url,
|
|
470
482
|
dry_run=dry_run,
|
|
471
483
|
output=output,
|
|
472
484
|
verbose=verbose,
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/worksheet.py
RENAMED
|
@@ -6,8 +6,10 @@ from typing import Optional
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
8
|
from ..client import MaybeAIClient
|
|
9
|
+
from ..errors import UsageError
|
|
9
10
|
from ..formatters import handle_cli_error
|
|
10
11
|
from ..options import output_option, timeout_option, verbose_option
|
|
12
|
+
from ..resolver import parse_document_target
|
|
11
13
|
from ._phase2 import (
|
|
12
14
|
command_target,
|
|
13
15
|
common_options,
|
|
@@ -163,6 +165,31 @@ def delete(
|
|
|
163
165
|
handle_cli_error(error)
|
|
164
166
|
|
|
165
167
|
|
|
168
|
+
def _reject_cross_document_worksheet_copy(
|
|
169
|
+
*,
|
|
170
|
+
source_document_id: str | None,
|
|
171
|
+
target_doc_id: str | None,
|
|
172
|
+
target_url: str | None,
|
|
173
|
+
) -> None:
|
|
174
|
+
if not target_doc_id and not target_url:
|
|
175
|
+
return
|
|
176
|
+
resolved_target_doc_id = target_doc_id
|
|
177
|
+
if target_url:
|
|
178
|
+
parsed_doc_id, _gid = parse_document_target(target_url)
|
|
179
|
+
resolved_target_doc_id = resolved_target_doc_id or parsed_doc_id
|
|
180
|
+
if not resolved_target_doc_id:
|
|
181
|
+
raise UsageError("--target-url must include a MaybeAI document id.")
|
|
182
|
+
if source_document_id and resolved_target_doc_id == source_document_id:
|
|
183
|
+
return
|
|
184
|
+
raise UsageError(
|
|
185
|
+
"Cross-document excel-worksheet copy is unsupported: "
|
|
186
|
+
"/api/v1/excel/copy_worksheet only copies within one workbook "
|
|
187
|
+
"(uri, sheet, new_name). Use `mbs db-table create-from-range` to "
|
|
188
|
+
"materialize a PG/SheetTable raw surface from a source worksheet "
|
|
189
|
+
"range into the target workbook."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
166
193
|
@app.command("copy")
|
|
167
194
|
def copy(
|
|
168
195
|
ctx: typer.Context,
|
|
@@ -172,6 +199,16 @@ def copy(
|
|
|
172
199
|
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
173
200
|
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name to copy."),
|
|
174
201
|
new_name: str = typer.Option(..., "--new-name", help="Name for copied worksheet."),
|
|
202
|
+
target_doc_id: Optional[str] = typer.Option(
|
|
203
|
+
None,
|
|
204
|
+
"--target-doc-id",
|
|
205
|
+
help="Optional target document ID. Cross-document copy is currently unsupported.",
|
|
206
|
+
),
|
|
207
|
+
target_url: Optional[str] = typer.Option(
|
|
208
|
+
None,
|
|
209
|
+
"--target-url",
|
|
210
|
+
help="Optional target workbook URL. Cross-document copy is currently unsupported.",
|
|
211
|
+
),
|
|
175
212
|
dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
|
|
176
213
|
output: Optional[str] = output_option(),
|
|
177
214
|
verbose: Optional[bool] = verbose_option(),
|
|
@@ -190,6 +227,11 @@ def copy(
|
|
|
190
227
|
timeout=timeout,
|
|
191
228
|
)
|
|
192
229
|
target = command_target(state)
|
|
230
|
+
_reject_cross_document_worksheet_copy(
|
|
231
|
+
source_document_id=target.document_id,
|
|
232
|
+
target_doc_id=target_doc_id,
|
|
233
|
+
target_url=target_url,
|
|
234
|
+
)
|
|
193
235
|
endpoint = "/api/v1/excel/copy_worksheet"
|
|
194
236
|
payload = {
|
|
195
237
|
"uri": target.uri,
|
|
@@ -70,6 +70,14 @@ COMMAND_ENDPOINTS: dict[str, EndpointMetadata] = {
|
|
|
70
70
|
"db-table.insert": _meta("db-table.insert", "API", "POST", "/api/v1/excel/append_rows", verify_supported=True),
|
|
71
71
|
"db-table.create": _meta("db-table.create", "API", "POST", "/api/v1/excel/db_table/create", verify_supported=True, notes="Creates a PG/SheetTable-backed table from JSON row data; --if-exists adopt verifies existing PG registry state."),
|
|
72
72
|
"db-table.create-from-query": _meta("db-table.create-from-query", "API", "POST", "/api/v1/excel/calc-formula + /api/v1/excel/db_table/create", verify_supported=True, notes="CLI-composed SQL materialization: calculate =SQL(...) then create a PG/SheetTable table from the tabular result."),
|
|
73
|
+
"db-table.create-from-range": _meta(
|
|
74
|
+
"db-table.create-from-range",
|
|
75
|
+
"API",
|
|
76
|
+
"POST",
|
|
77
|
+
"/api/v1/excel/read_sheet + /api/v1/excel/db_table/create",
|
|
78
|
+
verify_supported=True,
|
|
79
|
+
notes="CLI-composed raw-surface import: read a source worksheet range, reshape header/data rows, then create a PG/SheetTable on the target workbook.",
|
|
80
|
+
),
|
|
73
81
|
"db-table.update": _meta("db-table.update", "PLAN", None, None, notes="Planned native db-table update route; CLI help documents keyed JSON row patches."),
|
|
74
82
|
"db-table.delete": _meta("db-table.delete", "PLAN", None, None, destructive=True, confirmation_required=True, notes="Planned native db-table delete route."),
|
|
75
83
|
"worksheet.list": _meta("worksheet.list", "NOW", "POST", "/api/v1/excel/list_worksheets"),
|
|
@@ -100,7 +108,14 @@ COMMAND_ENDPOINTS: dict[str, EndpointMetadata] = {
|
|
|
100
108
|
dry_run_supported=True,
|
|
101
109
|
confirmation_required=True,
|
|
102
110
|
),
|
|
103
|
-
"worksheet.copy": _meta(
|
|
111
|
+
"worksheet.copy": _meta(
|
|
112
|
+
"worksheet.copy",
|
|
113
|
+
"API",
|
|
114
|
+
"POST",
|
|
115
|
+
"/api/v1/excel/copy_worksheet",
|
|
116
|
+
dry_run_supported=True,
|
|
117
|
+
notes="Same-document copy only. --target-doc-id/--target-url fail fast; use db-table create-from-range for cross-document raw surfaces.",
|
|
118
|
+
),
|
|
104
119
|
"worksheet.move": _meta("worksheet.move", "API", "POST", "/api/v1/excel/move_worksheet", dry_run_supported=True),
|
|
105
120
|
"row.insert": _meta("row.insert", "API", "POST", "/api/v1/excel/insert_rows", dry_run_supported=True),
|
|
106
121
|
"row.delete": _meta(
|
|
@@ -90,6 +90,7 @@ class EndpointRegistryTests(unittest.TestCase):
|
|
|
90
90
|
"db-table.insert": ("API", "POST", "/api/v1/excel/append_rows"),
|
|
91
91
|
"db-table.create": ("API", "POST", "/api/v1/excel/db_table/create"),
|
|
92
92
|
"db-table.create-from-query": ("API", "POST", "/api/v1/excel/calc-formula + /api/v1/excel/db_table/create"),
|
|
93
|
+
"db-table.create-from-range": ("API", "POST", "/api/v1/excel/read_sheet + /api/v1/excel/db_table/create"),
|
|
93
94
|
"db-table.update": ("PLAN", None, None),
|
|
94
95
|
"db-table.delete": ("PLAN", None, None),
|
|
95
96
|
}
|
|
@@ -125,6 +125,53 @@ class Phase2CLITests(unittest.TestCase):
|
|
|
125
125
|
self.assertEqual(move_payload["sheet"], "Copy")
|
|
126
126
|
self.assertEqual(move_payload["index"], 1)
|
|
127
127
|
|
|
128
|
+
def test_excel_worksheet_copy_rejects_cross_document_target(self) -> None:
|
|
129
|
+
with patch("maybeai_sheet.commands.worksheet.MaybeAIClient.post_json") as post_json:
|
|
130
|
+
result = self.runner.invoke(
|
|
131
|
+
app,
|
|
132
|
+
[
|
|
133
|
+
"excel-worksheet",
|
|
134
|
+
"copy",
|
|
135
|
+
"--doc-id",
|
|
136
|
+
"source123",
|
|
137
|
+
"--worksheet-name",
|
|
138
|
+
"1店",
|
|
139
|
+
"--target-doc-id",
|
|
140
|
+
"target123",
|
|
141
|
+
"--new-name",
|
|
142
|
+
"R_OrderLines_Store1",
|
|
143
|
+
],
|
|
144
|
+
env=self.env,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
148
|
+
self.assertIn("Cross-document excel-worksheet copy is unsupported", result.output)
|
|
149
|
+
self.assertIn("db-table create-from-range", result.output)
|
|
150
|
+
post_json.assert_not_called()
|
|
151
|
+
|
|
152
|
+
def test_excel_worksheet_copy_allows_same_document_target_doc_id(self) -> None:
|
|
153
|
+
with patch("maybeai_sheet.commands.worksheet.MaybeAIClient.post_json") as post_json:
|
|
154
|
+
post_json.return_value = {"success": True}
|
|
155
|
+
result = self.runner.invoke(
|
|
156
|
+
app,
|
|
157
|
+
[
|
|
158
|
+
"excel-worksheet",
|
|
159
|
+
"copy",
|
|
160
|
+
"--doc-id",
|
|
161
|
+
"abc123",
|
|
162
|
+
"--worksheet-name",
|
|
163
|
+
"Source",
|
|
164
|
+
"--target-doc-id",
|
|
165
|
+
"abc123",
|
|
166
|
+
"--new-name",
|
|
167
|
+
"Copy",
|
|
168
|
+
],
|
|
169
|
+
env=self.env,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
173
|
+
self.assertEqual(post_json.call_args.args[0], "/api/v1/excel/copy_worksheet")
|
|
174
|
+
|
|
128
175
|
def test_row_commands_use_verified_endpoints(self) -> None:
|
|
129
176
|
with patch("maybeai_sheet.commands.row.MaybeAIClient.post_json") as post_json:
|
|
130
177
|
post_json.return_value = {"success": True}
|
|
@@ -15,6 +15,7 @@ if str(SRC) not in sys.path:
|
|
|
15
15
|
sys.path.insert(0, str(SRC))
|
|
16
16
|
|
|
17
17
|
from maybeai_sheet.cli import app
|
|
18
|
+
from maybeai_sheet.commands.db_table import _surface_from_values_matrix
|
|
18
19
|
from maybeai_sheet.errors import APIRequestError, UsageError
|
|
19
20
|
from maybeai_sheet.protocol import cli_group_for_model_label
|
|
20
21
|
from maybeai_sheet.table_resolver import (
|
|
@@ -1135,6 +1136,187 @@ class TwoEngineCLITests(unittest.TestCase):
|
|
|
1135
1136
|
self.assertEqual(trace["error"]["status_code"], 409)
|
|
1136
1137
|
self.assertEqual(trace["error"]["body"], {"detail": "cannot persist formula in A1"})
|
|
1137
1138
|
|
|
1139
|
+
def test_surface_from_values_matrix_header_names_and_blank_drop(self) -> None:
|
|
1140
|
+
values = [
|
|
1141
|
+
["Merged Title", "", ""],
|
|
1142
|
+
["order_id", "amount", ""],
|
|
1143
|
+
["A1", 10, ""],
|
|
1144
|
+
["", "", ""],
|
|
1145
|
+
["A2", 20, "x"],
|
|
1146
|
+
]
|
|
1147
|
+
columns, rows, summary = _surface_from_values_matrix(
|
|
1148
|
+
values,
|
|
1149
|
+
header_row=1,
|
|
1150
|
+
use_header_names=True,
|
|
1151
|
+
)
|
|
1152
|
+
self.assertEqual([column["name"] for column in columns], ["order_id", "amount", "col_003"])
|
|
1153
|
+
self.assertEqual(rows, [{"order_id": "A1", "amount": 10, "col_003": ""}, {"order_id": "A2", "amount": 20, "col_003": "x"}])
|
|
1154
|
+
self.assertEqual(summary["rows_dropped_blank"], 1)
|
|
1155
|
+
self.assertEqual(summary["data_start"], 2)
|
|
1156
|
+
|
|
1157
|
+
positional_columns, positional_rows, _summary = _surface_from_values_matrix(
|
|
1158
|
+
values,
|
|
1159
|
+
header_row=1,
|
|
1160
|
+
use_header_names=False,
|
|
1161
|
+
)
|
|
1162
|
+
self.assertEqual(
|
|
1163
|
+
[column["name"] for column in positional_columns],
|
|
1164
|
+
["raw_col_001", "raw_col_002", "raw_col_003"],
|
|
1165
|
+
)
|
|
1166
|
+
self.assertEqual(positional_rows[0]["raw_col_001"], "A1")
|
|
1167
|
+
|
|
1168
|
+
def test_db_table_create_from_range_reads_source_and_creates_on_target(self) -> None:
|
|
1169
|
+
with patch("maybeai_sheet.commands.db_table.MaybeAIClient.post_json") as post_json:
|
|
1170
|
+
post_json.side_effect = [
|
|
1171
|
+
{
|
|
1172
|
+
"success": True,
|
|
1173
|
+
"values": [
|
|
1174
|
+
["order_id", "amount", ""],
|
|
1175
|
+
["A1", 10, ""],
|
|
1176
|
+
["", "", ""],
|
|
1177
|
+
["A2", 20, "note"],
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
"success": True,
|
|
1182
|
+
"document_id": "target123",
|
|
1183
|
+
"worksheet_name": "R_OrderLines_Store1",
|
|
1184
|
+
"gid": 11,
|
|
1185
|
+
"engine": "pg",
|
|
1186
|
+
"created_rows": 2,
|
|
1187
|
+
"spreadsheet_url": "https://www.maybe.ai/docs/spreadsheets/d/target123?gid=11",
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
"success": True,
|
|
1191
|
+
"worksheets": [
|
|
1192
|
+
{
|
|
1193
|
+
"gid": 11,
|
|
1194
|
+
"worksheet_name": "R_OrderLines_Store1",
|
|
1195
|
+
"data_engine": "pg",
|
|
1196
|
+
}
|
|
1197
|
+
],
|
|
1198
|
+
},
|
|
1199
|
+
]
|
|
1200
|
+
result = self.runner.invoke(
|
|
1201
|
+
app,
|
|
1202
|
+
[
|
|
1203
|
+
"db-table",
|
|
1204
|
+
"create-from-range",
|
|
1205
|
+
"--doc-id",
|
|
1206
|
+
"target123",
|
|
1207
|
+
"--name",
|
|
1208
|
+
"R_OrderLines_Store1",
|
|
1209
|
+
"--source-doc-id",
|
|
1210
|
+
"source123",
|
|
1211
|
+
"--worksheet-name",
|
|
1212
|
+
"1店",
|
|
1213
|
+
"--range",
|
|
1214
|
+
"A2:C5",
|
|
1215
|
+
"--header-row",
|
|
1216
|
+
"0",
|
|
1217
|
+
"--use-header-names",
|
|
1218
|
+
"--if-exists",
|
|
1219
|
+
"adopt",
|
|
1220
|
+
"--verify",
|
|
1221
|
+
"--output",
|
|
1222
|
+
"json",
|
|
1223
|
+
],
|
|
1224
|
+
env=self.env,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1228
|
+
self.assertEqual(post_json.call_args_list[0].args[0], "/api/v1/excel/read_sheet")
|
|
1229
|
+
read_payload = post_json.call_args_list[0].args[1]
|
|
1230
|
+
self.assertEqual(read_payload["uri"], "https://www.maybe.ai/docs/spreadsheets/d/source123")
|
|
1231
|
+
self.assertEqual(read_payload["worksheet_name"], "1店")
|
|
1232
|
+
self.assertEqual(read_payload["range_address"], "A2:C5")
|
|
1233
|
+
self.assertEqual(post_json.call_args_list[1].args[0], "/api/v1/excel/db_table/create")
|
|
1234
|
+
create_payload = post_json.call_args_list[1].args[1]
|
|
1235
|
+
self.assertEqual(create_payload["document_id"], "target123")
|
|
1236
|
+
self.assertEqual(create_payload["uri"], "https://www.maybe.ai/docs/spreadsheets/d/target123")
|
|
1237
|
+
self.assertEqual(create_payload["name"], "R_OrderLines_Store1")
|
|
1238
|
+
self.assertEqual(create_payload["if_exists"], "adopt")
|
|
1239
|
+
self.assertEqual(
|
|
1240
|
+
create_payload["columns"],
|
|
1241
|
+
[
|
|
1242
|
+
{"name": "order_id", "logical_type": "text", "index": 0},
|
|
1243
|
+
{"name": "amount", "logical_type": "text", "index": 1},
|
|
1244
|
+
{"name": "col_003", "logical_type": "text", "index": 2},
|
|
1245
|
+
],
|
|
1246
|
+
)
|
|
1247
|
+
self.assertEqual(
|
|
1248
|
+
create_payload["rows"],
|
|
1249
|
+
[
|
|
1250
|
+
{"order_id": "A1", "amount": 10, "col_003": ""},
|
|
1251
|
+
{"order_id": "A2", "amount": 20, "col_003": "note"},
|
|
1252
|
+
],
|
|
1253
|
+
)
|
|
1254
|
+
data = json.loads(result.output)
|
|
1255
|
+
self.assertEqual(data["endpoint"], "/api/v1/excel/read_sheet + /api/v1/excel/db_table/create")
|
|
1256
|
+
self.assertEqual(data["target"]["document_id"], "target123")
|
|
1257
|
+
self.assertEqual(data["context"]["source_document_id"], "source123")
|
|
1258
|
+
self.assertEqual(data["context"]["source_worksheet_name"], "1店")
|
|
1259
|
+
self.assertEqual(data["context"]["source_range"], "A2:C5")
|
|
1260
|
+
self.assertEqual(data["context"]["header_row"], 0)
|
|
1261
|
+
self.assertTrue(data["context"]["use_header_names"])
|
|
1262
|
+
self.assertEqual(data["context"]["row_count"], 2)
|
|
1263
|
+
self.assertEqual(data["context"]["rows_dropped_blank"], 1)
|
|
1264
|
+
self.assertTrue(data["result"]["verified"])
|
|
1265
|
+
|
|
1266
|
+
def test_db_table_create_from_range_uses_positional_columns_by_default(self) -> None:
|
|
1267
|
+
with patch("maybeai_sheet.commands.db_table.MaybeAIClient.post_json") as post_json:
|
|
1268
|
+
post_json.side_effect = [
|
|
1269
|
+
{"success": True, "values": [["h1", "h2"], ["v1", "v2"]]},
|
|
1270
|
+
{
|
|
1271
|
+
"success": True,
|
|
1272
|
+
"document_id": "target123",
|
|
1273
|
+
"worksheet_name": "R_Positional",
|
|
1274
|
+
"gid": 12,
|
|
1275
|
+
"engine": "pg",
|
|
1276
|
+
"created_rows": 1,
|
|
1277
|
+
},
|
|
1278
|
+
]
|
|
1279
|
+
result = self.runner.invoke(
|
|
1280
|
+
app,
|
|
1281
|
+
[
|
|
1282
|
+
"db-table",
|
|
1283
|
+
"create-from-range",
|
|
1284
|
+
"--doc-id",
|
|
1285
|
+
"target123",
|
|
1286
|
+
"--name",
|
|
1287
|
+
"R_Positional",
|
|
1288
|
+
"--source-doc-id",
|
|
1289
|
+
"source123",
|
|
1290
|
+
"--worksheet-name",
|
|
1291
|
+
"Sheet1",
|
|
1292
|
+
"--range",
|
|
1293
|
+
"A1:B2",
|
|
1294
|
+
"--output",
|
|
1295
|
+
"json",
|
|
1296
|
+
],
|
|
1297
|
+
env=self.env,
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1301
|
+
create_payload = post_json.call_args_list[1].args[1]
|
|
1302
|
+
self.assertEqual(
|
|
1303
|
+
[column["name"] for column in create_payload["columns"]],
|
|
1304
|
+
["raw_col_001", "raw_col_002"],
|
|
1305
|
+
)
|
|
1306
|
+
self.assertEqual(create_payload["rows"], [{"raw_col_001": "v1", "raw_col_002": "v2"}])
|
|
1307
|
+
data = json.loads(result.output)
|
|
1308
|
+
self.assertFalse(data["context"]["use_header_names"])
|
|
1309
|
+
|
|
1310
|
+
def test_db_table_create_from_range_help_lists_options(self) -> None:
|
|
1311
|
+
result = self.runner.invoke(app, ["db-table", "create-from-range", "--help"], env=self.env)
|
|
1312
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
1313
|
+
self.assertIn("--source-doc-id", result.output)
|
|
1314
|
+
self.assertIn("--header-row", result.output)
|
|
1315
|
+
self.assertIn("--use-header-names", result.output)
|
|
1316
|
+
self.assertIn("--range", result.output)
|
|
1317
|
+
self.assertIn("--if-exists", result.output)
|
|
1318
|
+
self.assertIn("--verify", result.output)
|
|
1319
|
+
|
|
1138
1320
|
def test_db_table_create_adopts_existing_pg_table_with_verify(self) -> None:
|
|
1139
1321
|
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json") as rows_file, tempfile.NamedTemporaryFile(mode="w+", suffix=".json") as columns_file:
|
|
1140
1322
|
json.dump([], rows_file)
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/chart_style_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/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.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/dashboard.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/excel_table.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
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/src/maybeai_sheet/commands/workbook.py
RENAMED
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/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.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/build_fixtures.py
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/flat-records.xlsx
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/mixed-engine.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/tests/fixtures/benchmarks/simple-grid.xlsx
RENAMED
|
File without changes
|
{maybeai_sheet_cli-0.13.2 → maybeai_sheet_cli-0.14.0}/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
|