exstruct 0.5.2__tar.gz → 0.5.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.
- {exstruct-0.5.2 → exstruct-0.5.3}/PKG-INFO +15 -8
- {exstruct-0.5.2 → exstruct-0.5.3}/README.md +14 -7
- {exstruct-0.5.2 → exstruct-0.5.3}/pyproject.toml +4 -4
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/__init__.py +14 -0
- exstruct-0.5.3/src/exstruct/mcp/render_runner.py +124 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/server.py +104 -0
- exstruct-0.5.3/src/exstruct/mcp/shared/__init__.py +39 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/shared/a1.py +167 -82
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/shared/output_path.py +47 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/tools.py +87 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/render/__init__.py +1057 -636
- exstruct-0.5.3/src/exstruct/render/subprocess_worker.py +180 -0
- exstruct-0.5.2/src/exstruct/mcp/shared/__init__.py +0 -23
- {exstruct-0.5.2 → exstruct-0.5.3}/LICENSE +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/cli/availability.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/cli/main.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/backends/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/backends/base.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/backends/com_backend.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/backends/openpyxl_backend.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/cells.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/charts.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/integrate.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/logging_utils.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/modeling.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/pipeline.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/ranges.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/shapes.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/core/workbook.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/engine.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/errors.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/io/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/io/serialize.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/chunk_reader.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/extract_runner.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/io.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/op_schema.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/chart_types.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/engine/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/engine/base.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/engine/openpyxl_engine.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/engine/xlwings_engine.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/internal.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/models.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/normalize.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/ops/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/ops/common.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/ops/openpyxl_ops.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/ops/xlwings_ops.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/runtime.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/service.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/specs.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch/types.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/patch_runner.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/sheet_reader.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/mcp/validate_input.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/models/__init__.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/models/maps.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/models/types.py +0 -0
- {exstruct-0.5.2 → exstruct-0.5.3}/src/exstruct/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exstruct
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines
|
|
5
5
|
Keywords: excel,structure,data,exstruct
|
|
6
6
|
Author: harumiWeb
|
|
@@ -158,6 +158,7 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re
|
|
|
158
158
|
Available tools:
|
|
159
159
|
|
|
160
160
|
- `exstruct_extract`
|
|
161
|
+
- `exstruct_capture_sheet_images`
|
|
161
162
|
- `exstruct_make`
|
|
162
163
|
- `exstruct_patch`
|
|
163
164
|
- `exstruct_read_json_chunk`
|
|
@@ -169,6 +170,12 @@ Available tools:
|
|
|
169
170
|
Notes:
|
|
170
171
|
|
|
171
172
|
- In MCP, `exstruct_extract` defaults to `options.alpha_col=true` (column keys: `A`, `B`, ...). Set `options.alpha_col=false` for legacy 0-based numeric string keys.
|
|
173
|
+
- `exstruct_capture_sheet_images` is COM-only (Experimental) and supports optional `sheet` / `range` targeting (`A1:B2`, `Sheet1!A1:B2`, `'Sheet 1'!A1:B2`). When `out_dir` is omitted, it creates a unique `<workbook_stem>_images` directory under MCP `--root`.
|
|
174
|
+
- MCP server startup sets `EXSTRUCT_RENDER_SUBPROCESS=1` by default (`setdefault`). If you prefer in-process rendering, set `EXSTRUCT_RENDER_SUBPROCESS=0` before launching the server.
|
|
175
|
+
- Timeout tuning for `exstruct_capture_sheet_images`: `EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC` (tool timeout), `EXSTRUCT_RENDER_SUBPROCESS_STARTUP_TIMEOUT_SEC` (worker startup), `EXSTRUCT_RENDER_SUBPROCESS_JOIN_TIMEOUT_SEC` (primary wait budget), `EXSTRUCT_RENDER_SUBPROCESS_RESULT_TIMEOUT_SEC` (post-exit grace).
|
|
176
|
+
- Subprocess errors are stage-aware (`stage=startup|join|result|worker`) so MCP clients can distinguish bootstrap failures vs timeout vs worker-side rendering errors.
|
|
177
|
+
- Trade-off of `EXSTRUCT_RENDER_SUBPROCESS=1`: subprocess startup/coordination overhead and dependency on worker-side module resolution.
|
|
178
|
+
- Trade-off of `EXSTRUCT_RENDER_SUBPROCESS=0`: less crash isolation and higher memory pressure risk in long-running processes.
|
|
172
179
|
- Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses.
|
|
173
180
|
- On Windows with Excel, standard/verbose can use COM for richer extraction. On non-Windows, COM is unavailable and extraction uses openpyxl-based fallbacks.
|
|
174
181
|
- `exstruct_patch` supports `backend` selection:
|
|
@@ -176,13 +183,13 @@ Notes:
|
|
|
176
183
|
- `com`: force COM (rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`)
|
|
177
184
|
- `openpyxl`: force openpyxl (`.xls` is not supported)
|
|
178
185
|
- `create_chart` is COM-only (`backend="openpyxl"` is not allowed for requests that include it), and it also rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`.
|
|
179
|
-
- `create_chart` `chart_type` supports: `line`, `column`, `bar`, `area`, `pie`, `doughnut`, `scatter`, `radar` (aliases: `column_clustered`, `bar_clustered`, `xy_scatter`, `donut`).
|
|
180
|
-
- `create_chart` `data_range` accepts either one range string or `list[str]` (multi-series), and `data_range` / `category_range` both support sheet-qualified ranges (`Sheet2!A1:B10`, `'Sales Data'!A1:B10`).
|
|
181
|
-
- `create_chart` supports optional explicit labels: `chart_title`, `x_axis_title`, `y_axis_title`.
|
|
182
|
-
- `create_chart` and `apply_table_style` can be combined in one request when the backend resolves to COM (`backend="com"` or `backend="auto"` with COM available).
|
|
183
|
-
- For stable `apply_table_style` COM runs on Windows, ensure desktop Excel is installed/runnable and the target `range` is a contiguous header-included A1 range.
|
|
184
|
-
- `exstruct_patch` error detail may include `error_code` / `failed_field` / `raw_com_message`; table-related codes include `table_style_invalid`, `list_object_add_failed`, and `com_api_missing`.
|
|
185
|
-
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
186
|
+
- `create_chart` `chart_type` supports: `line`, `column`, `bar`, `area`, `pie`, `doughnut`, `scatter`, `radar` (aliases: `column_clustered`, `bar_clustered`, `xy_scatter`, `donut`).
|
|
187
|
+
- `create_chart` `data_range` accepts either one range string or `list[str]` (multi-series), and `data_range` / `category_range` both support sheet-qualified ranges (`Sheet2!A1:B10`, `'Sales Data'!A1:B10`).
|
|
188
|
+
- `create_chart` supports optional explicit labels: `chart_title`, `x_axis_title`, `y_axis_title`.
|
|
189
|
+
- `create_chart` and `apply_table_style` can be combined in one request when the backend resolves to COM (`backend="com"` or `backend="auto"` with COM available).
|
|
190
|
+
- For stable `apply_table_style` COM runs on Windows, ensure desktop Excel is installed/runnable and the target `range` is a contiguous header-included A1 range.
|
|
191
|
+
- `exstruct_patch` error detail may include `error_code` / `failed_field` / `raw_com_message`; table-related codes include `table_style_invalid`, `list_object_add_failed`, and `com_api_missing`.
|
|
192
|
+
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
186
193
|
- Use `exstruct_make` for new workbook creation and `exstruct_patch` for editing existing workbooks.
|
|
187
194
|
- `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
|
|
188
195
|
- Supports `.xlsx` / `.xlsm` / `.xls`
|
|
@@ -93,6 +93,7 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re
|
|
|
93
93
|
Available tools:
|
|
94
94
|
|
|
95
95
|
- `exstruct_extract`
|
|
96
|
+
- `exstruct_capture_sheet_images`
|
|
96
97
|
- `exstruct_make`
|
|
97
98
|
- `exstruct_patch`
|
|
98
99
|
- `exstruct_read_json_chunk`
|
|
@@ -104,6 +105,12 @@ Available tools:
|
|
|
104
105
|
Notes:
|
|
105
106
|
|
|
106
107
|
- In MCP, `exstruct_extract` defaults to `options.alpha_col=true` (column keys: `A`, `B`, ...). Set `options.alpha_col=false` for legacy 0-based numeric string keys.
|
|
108
|
+
- `exstruct_capture_sheet_images` is COM-only (Experimental) and supports optional `sheet` / `range` targeting (`A1:B2`, `Sheet1!A1:B2`, `'Sheet 1'!A1:B2`). When `out_dir` is omitted, it creates a unique `<workbook_stem>_images` directory under MCP `--root`.
|
|
109
|
+
- MCP server startup sets `EXSTRUCT_RENDER_SUBPROCESS=1` by default (`setdefault`). If you prefer in-process rendering, set `EXSTRUCT_RENDER_SUBPROCESS=0` before launching the server.
|
|
110
|
+
- Timeout tuning for `exstruct_capture_sheet_images`: `EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC` (tool timeout), `EXSTRUCT_RENDER_SUBPROCESS_STARTUP_TIMEOUT_SEC` (worker startup), `EXSTRUCT_RENDER_SUBPROCESS_JOIN_TIMEOUT_SEC` (primary wait budget), `EXSTRUCT_RENDER_SUBPROCESS_RESULT_TIMEOUT_SEC` (post-exit grace).
|
|
111
|
+
- Subprocess errors are stage-aware (`stage=startup|join|result|worker`) so MCP clients can distinguish bootstrap failures vs timeout vs worker-side rendering errors.
|
|
112
|
+
- Trade-off of `EXSTRUCT_RENDER_SUBPROCESS=1`: subprocess startup/coordination overhead and dependency on worker-side module resolution.
|
|
113
|
+
- Trade-off of `EXSTRUCT_RENDER_SUBPROCESS=0`: less crash isolation and higher memory pressure risk in long-running processes.
|
|
107
114
|
- Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses.
|
|
108
115
|
- On Windows with Excel, standard/verbose can use COM for richer extraction. On non-Windows, COM is unavailable and extraction uses openpyxl-based fallbacks.
|
|
109
116
|
- `exstruct_patch` supports `backend` selection:
|
|
@@ -111,13 +118,13 @@ Notes:
|
|
|
111
118
|
- `com`: force COM (rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`)
|
|
112
119
|
- `openpyxl`: force openpyxl (`.xls` is not supported)
|
|
113
120
|
- `create_chart` is COM-only (`backend="openpyxl"` is not allowed for requests that include it), and it also rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`.
|
|
114
|
-
- `create_chart` `chart_type` supports: `line`, `column`, `bar`, `area`, `pie`, `doughnut`, `scatter`, `radar` (aliases: `column_clustered`, `bar_clustered`, `xy_scatter`, `donut`).
|
|
115
|
-
- `create_chart` `data_range` accepts either one range string or `list[str]` (multi-series), and `data_range` / `category_range` both support sheet-qualified ranges (`Sheet2!A1:B10`, `'Sales Data'!A1:B10`).
|
|
116
|
-
- `create_chart` supports optional explicit labels: `chart_title`, `x_axis_title`, `y_axis_title`.
|
|
117
|
-
- `create_chart` and `apply_table_style` can be combined in one request when the backend resolves to COM (`backend="com"` or `backend="auto"` with COM available).
|
|
118
|
-
- For stable `apply_table_style` COM runs on Windows, ensure desktop Excel is installed/runnable and the target `range` is a contiguous header-included A1 range.
|
|
119
|
-
- `exstruct_patch` error detail may include `error_code` / `failed_field` / `raw_com_message`; table-related codes include `table_style_invalid`, `list_object_add_failed`, and `com_api_missing`.
|
|
120
|
-
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
121
|
+
- `create_chart` `chart_type` supports: `line`, `column`, `bar`, `area`, `pie`, `doughnut`, `scatter`, `radar` (aliases: `column_clustered`, `bar_clustered`, `xy_scatter`, `donut`).
|
|
122
|
+
- `create_chart` `data_range` accepts either one range string or `list[str]` (multi-series), and `data_range` / `category_range` both support sheet-qualified ranges (`Sheet2!A1:B10`, `'Sales Data'!A1:B10`).
|
|
123
|
+
- `create_chart` supports optional explicit labels: `chart_title`, `x_axis_title`, `y_axis_title`.
|
|
124
|
+
- `create_chart` and `apply_table_style` can be combined in one request when the backend resolves to COM (`backend="com"` or `backend="auto"` with COM available).
|
|
125
|
+
- For stable `apply_table_style` COM runs on Windows, ensure desktop Excel is installed/runnable and the target `range` is a contiguous header-included A1 range.
|
|
126
|
+
- `exstruct_patch` error detail may include `error_code` / `failed_field` / `raw_com_message`; table-related codes include `table_style_invalid`, `list_object_add_failed`, and `com_api_missing`.
|
|
127
|
+
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
121
128
|
- Use `exstruct_make` for new workbook creation and `exstruct_patch` for editing existing workbooks.
|
|
122
129
|
- `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
|
|
123
130
|
- Supports `.xlsx` / `.xlsm` / `.xls`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "exstruct"
|
|
3
|
-
version = "0.5.
|
|
4
|
-
description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
|
|
1
|
+
[project]
|
|
2
|
+
name = "exstruct"
|
|
3
|
+
version = "0.5.3"
|
|
4
|
+
description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
7
7
|
keywords = ["excel", "structure", "data", "exstruct"]
|
|
@@ -28,6 +28,11 @@ from .patch_runner import (
|
|
|
28
28
|
run_make,
|
|
29
29
|
run_patch,
|
|
30
30
|
)
|
|
31
|
+
from .render_runner import (
|
|
32
|
+
CaptureSheetImagesRequest,
|
|
33
|
+
CaptureSheetImagesResult,
|
|
34
|
+
run_capture_sheet_images,
|
|
35
|
+
)
|
|
31
36
|
from .sheet_reader import (
|
|
32
37
|
CellReadItem,
|
|
33
38
|
FormulaReadItem,
|
|
@@ -42,6 +47,8 @@ from .sheet_reader import (
|
|
|
42
47
|
read_range,
|
|
43
48
|
)
|
|
44
49
|
from .tools import (
|
|
50
|
+
CaptureSheetImagesToolInput,
|
|
51
|
+
CaptureSheetImagesToolOutput,
|
|
45
52
|
DescribeOpToolInput,
|
|
46
53
|
DescribeOpToolOutput,
|
|
47
54
|
ExtractToolInput,
|
|
@@ -61,6 +68,7 @@ from .tools import (
|
|
|
61
68
|
ReadRangeToolOutput,
|
|
62
69
|
ValidateInputToolInput,
|
|
63
70
|
ValidateInputToolOutput,
|
|
71
|
+
run_capture_sheet_images_tool,
|
|
64
72
|
run_describe_op_tool,
|
|
65
73
|
run_extract_tool,
|
|
66
74
|
run_list_ops_tool,
|
|
@@ -79,6 +87,10 @@ from .validate_input import (
|
|
|
79
87
|
)
|
|
80
88
|
|
|
81
89
|
__all__ = [
|
|
90
|
+
"CaptureSheetImagesRequest",
|
|
91
|
+
"CaptureSheetImagesResult",
|
|
92
|
+
"CaptureSheetImagesToolInput",
|
|
93
|
+
"CaptureSheetImagesToolOutput",
|
|
82
94
|
"DescribeOpToolInput",
|
|
83
95
|
"DescribeOpToolOutput",
|
|
84
96
|
"ExtractRequest",
|
|
@@ -133,6 +145,8 @@ __all__ = [
|
|
|
133
145
|
"run_make",
|
|
134
146
|
"run_make_tool",
|
|
135
147
|
"run_patch",
|
|
148
|
+
"run_capture_sheet_images",
|
|
149
|
+
"run_capture_sheet_images_tool",
|
|
136
150
|
"run_patch_tool",
|
|
137
151
|
"read_cells",
|
|
138
152
|
"read_formulas",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from exstruct import render
|
|
9
|
+
|
|
10
|
+
from .io import PathPolicy
|
|
11
|
+
from .shared.a1 import resolve_sheet_and_range
|
|
12
|
+
from .shared.output_path import resolve_image_output_dir
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CaptureSheetImagesRequest(BaseModel):
|
|
18
|
+
"""Input model for MCP sheet image capture."""
|
|
19
|
+
|
|
20
|
+
xlsx_path: Path
|
|
21
|
+
out_dir: Path | None = None
|
|
22
|
+
dpi: int = Field(default=144, ge=1)
|
|
23
|
+
sheet: str | None = None
|
|
24
|
+
range: str | None = None # noqa: A003
|
|
25
|
+
|
|
26
|
+
@field_validator("sheet")
|
|
27
|
+
@classmethod
|
|
28
|
+
def _validate_sheet(cls, value: str | None) -> str | None:
|
|
29
|
+
if value is None:
|
|
30
|
+
return None
|
|
31
|
+
candidate = value.strip()
|
|
32
|
+
if not candidate:
|
|
33
|
+
raise ValueError("sheet must not be empty when provided.")
|
|
34
|
+
return candidate
|
|
35
|
+
|
|
36
|
+
@field_validator("range")
|
|
37
|
+
@classmethod
|
|
38
|
+
def _validate_range(cls, value: str | None) -> str | None:
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
candidate = value.strip()
|
|
42
|
+
if not candidate:
|
|
43
|
+
raise ValueError("range must not be empty when provided.")
|
|
44
|
+
return candidate
|
|
45
|
+
|
|
46
|
+
@model_validator(mode="after")
|
|
47
|
+
def _validate_sheet_range_consistency(self) -> CaptureSheetImagesRequest:
|
|
48
|
+
selection = resolve_sheet_and_range(self.sheet, self.range)
|
|
49
|
+
self.sheet = selection.sheet
|
|
50
|
+
self.range = selection.range_ref
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CaptureSheetImagesResult(BaseModel):
|
|
55
|
+
"""Output model for MCP sheet image capture."""
|
|
56
|
+
|
|
57
|
+
out_dir: str
|
|
58
|
+
image_paths: list[str] = Field(default_factory=list)
|
|
59
|
+
warnings: list[str] = Field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_capture_sheet_images(
|
|
63
|
+
request: CaptureSheetImagesRequest,
|
|
64
|
+
*,
|
|
65
|
+
policy: PathPolicy | None = None,
|
|
66
|
+
) -> CaptureSheetImagesResult:
|
|
67
|
+
"""Capture worksheet images using Excel COM rendering pipeline.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
request: Capture request payload.
|
|
71
|
+
policy: Optional path access policy.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Capture result with resolved output directory and written images.
|
|
75
|
+
"""
|
|
76
|
+
resolved_input = _resolve_input_path(request.xlsx_path, policy=policy)
|
|
77
|
+
resolved_out_dir = resolve_image_output_dir(
|
|
78
|
+
resolved_input,
|
|
79
|
+
out_dir=request.out_dir,
|
|
80
|
+
policy=policy,
|
|
81
|
+
)
|
|
82
|
+
_ensure_com_available()
|
|
83
|
+
resolved_out_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
written_paths = render.export_sheet_images(
|
|
85
|
+
resolved_input,
|
|
86
|
+
resolved_out_dir,
|
|
87
|
+
dpi=request.dpi,
|
|
88
|
+
sheet=request.sheet,
|
|
89
|
+
a1_range=request.range,
|
|
90
|
+
)
|
|
91
|
+
return CaptureSheetImagesResult(
|
|
92
|
+
out_dir=str(resolved_out_dir),
|
|
93
|
+
image_paths=[str(path) for path in written_paths],
|
|
94
|
+
warnings=[],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_input_path(path: Path, *, policy: PathPolicy | None) -> Path:
|
|
99
|
+
"""Resolve and validate input workbook path."""
|
|
100
|
+
resolved = policy.ensure_allowed(path) if policy else path.resolve()
|
|
101
|
+
if not resolved.exists():
|
|
102
|
+
raise FileNotFoundError(f"Input file not found: {resolved}")
|
|
103
|
+
if not resolved.is_file():
|
|
104
|
+
raise ValueError(f"Input path is not a file: {resolved}")
|
|
105
|
+
return resolved
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _ensure_com_available() -> None:
|
|
109
|
+
"""Validate Excel COM availability and raise ValueError when unavailable."""
|
|
110
|
+
app: object | None = None
|
|
111
|
+
try:
|
|
112
|
+
app = render._require_excel_app()
|
|
113
|
+
except Exception as exc: # pragma: no cover - delegated by render internals
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"Excel (COM) is not available. Rendering (PDF/image) requires a desktop Excel installation."
|
|
116
|
+
) from exc
|
|
117
|
+
finally:
|
|
118
|
+
if app is not None:
|
|
119
|
+
quit_method = getattr(app, "quit", None)
|
|
120
|
+
if callable(quit_method):
|
|
121
|
+
try:
|
|
122
|
+
quit_method()
|
|
123
|
+
except Exception as exc: # pragma: no cover - defensive probe cleanup
|
|
124
|
+
logger.warning("Failed to close Excel app after COM probe: %s", exc)
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import functools
|
|
5
5
|
import importlib
|
|
6
6
|
import logging
|
|
7
|
+
import math
|
|
7
8
|
import os
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import sys
|
|
@@ -24,6 +25,8 @@ from .patch.normalize import (
|
|
|
24
25
|
parse_patch_op_json as _normalize_parse_patch_op_json,
|
|
25
26
|
)
|
|
26
27
|
from .tools import (
|
|
28
|
+
CaptureSheetImagesToolInput,
|
|
29
|
+
CaptureSheetImagesToolOutput,
|
|
27
30
|
DescribeOpToolInput,
|
|
28
31
|
DescribeOpToolOutput,
|
|
29
32
|
ExtractToolInput,
|
|
@@ -44,6 +47,7 @@ from .tools import (
|
|
|
44
47
|
RuntimeInfoToolOutput,
|
|
45
48
|
ValidateInputToolInput,
|
|
46
49
|
ValidateInputToolOutput,
|
|
50
|
+
run_capture_sheet_images_tool,
|
|
47
51
|
run_describe_op_tool,
|
|
48
52
|
run_extract_tool,
|
|
49
53
|
run_list_ops_tool,
|
|
@@ -60,6 +64,7 @@ if TYPE_CHECKING: # pragma: no cover - typing only
|
|
|
60
64
|
from mcp.server.fastmcp import FastMCP
|
|
61
65
|
|
|
62
66
|
logger = logging.getLogger(__name__)
|
|
67
|
+
_DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS = 120.0
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
class ServerConfig(BaseModel):
|
|
@@ -105,10 +110,15 @@ def run_server(config: ServerConfig) -> None:
|
|
|
105
110
|
config: Server configuration.
|
|
106
111
|
"""
|
|
107
112
|
os.environ.setdefault("EXSTRUCT_BORDER_CLUSTER_BACKEND", "python")
|
|
113
|
+
os.environ.setdefault("EXSTRUCT_RENDER_SUBPROCESS", "1")
|
|
108
114
|
logger.info(
|
|
109
115
|
"Border cluster backend set to %s for MCP.",
|
|
110
116
|
os.getenv("EXSTRUCT_BORDER_CLUSTER_BACKEND"),
|
|
111
117
|
)
|
|
118
|
+
logger.info(
|
|
119
|
+
"Render subprocess mode for MCP set to %s.",
|
|
120
|
+
os.getenv("EXSTRUCT_RENDER_SUBPROCESS"),
|
|
121
|
+
)
|
|
112
122
|
_import_mcp()
|
|
113
123
|
policy = PathPolicy(root=config.root, deny_globs=config.deny_globs)
|
|
114
124
|
logger.info("MCP root: %s", policy.normalize_root())
|
|
@@ -302,6 +312,8 @@ def _register_tools(
|
|
|
302
312
|
tool = app.tool(name="exstruct_extract")
|
|
303
313
|
tool(_extract_tool)
|
|
304
314
|
|
|
315
|
+
_register_capture_sheet_images_tool(app, policy=policy)
|
|
316
|
+
|
|
305
317
|
async def _read_json_chunk_tool( # pylint: disable=redefined-builtin
|
|
306
318
|
out_path: str,
|
|
307
319
|
sheet: str | None = None,
|
|
@@ -722,6 +734,98 @@ Returns:
|
|
|
722
734
|
return f"{base_description.strip()}\n\n{build_patch_tool_mini_schema()}"
|
|
723
735
|
|
|
724
736
|
|
|
737
|
+
def _register_capture_sheet_images_tool(app: FastMCP, *, policy: PathPolicy) -> None:
|
|
738
|
+
"""Register the sheet image capture MCP tool."""
|
|
739
|
+
|
|
740
|
+
async def _capture_sheet_images_tool( # pylint: disable=redefined-builtin
|
|
741
|
+
xlsx_path: str,
|
|
742
|
+
out_dir: str | None = None,
|
|
743
|
+
dpi: int = 144,
|
|
744
|
+
sheet: str | None = None,
|
|
745
|
+
range: str | None = None, # noqa: A002
|
|
746
|
+
) -> CaptureSheetImagesToolOutput:
|
|
747
|
+
"""Export worksheet images as PNG files (COM only).
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
xlsx_path: Path to the Excel workbook.
|
|
751
|
+
out_dir: Optional output directory for PNG files. When omitted, MCP
|
|
752
|
+
creates a unique directory under server `--root` using
|
|
753
|
+
`<workbook_stem>_images` with numeric suffixes as needed.
|
|
754
|
+
dpi: Rendering DPI (must be >= 1).
|
|
755
|
+
sheet: Optional target sheet name. Required only when `range` is
|
|
756
|
+
unqualified (for example, `A1:B2`).
|
|
757
|
+
range: Optional A1 range (`A1:B2`) with optional sheet qualifier
|
|
758
|
+
(`Sheet1!A1:B2`, `'Sheet 1'!A1:B2`).
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Export result payload with resolved output directory and image paths.
|
|
762
|
+
"""
|
|
763
|
+
payload = CaptureSheetImagesToolInput(
|
|
764
|
+
xlsx_path=xlsx_path,
|
|
765
|
+
out_dir=out_dir,
|
|
766
|
+
dpi=dpi,
|
|
767
|
+
sheet=sheet,
|
|
768
|
+
range=range,
|
|
769
|
+
)
|
|
770
|
+
work = functools.partial(
|
|
771
|
+
run_capture_sheet_images_tool,
|
|
772
|
+
payload,
|
|
773
|
+
policy=policy,
|
|
774
|
+
)
|
|
775
|
+
timeout_seconds = _get_capture_sheet_images_timeout_seconds()
|
|
776
|
+
try:
|
|
777
|
+
with anyio.fail_after(timeout_seconds):
|
|
778
|
+
result = cast(
|
|
779
|
+
CaptureSheetImagesToolOutput,
|
|
780
|
+
await anyio.to_thread.run_sync(work, abandon_on_cancel=True),
|
|
781
|
+
)
|
|
782
|
+
except TimeoutError as exc:
|
|
783
|
+
raise TimeoutError(
|
|
784
|
+
"capture_sheet_images timed out after "
|
|
785
|
+
f"{timeout_seconds:.1f}s. "
|
|
786
|
+
"Adjust workbook/range size or increase "
|
|
787
|
+
"EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC."
|
|
788
|
+
) from exc
|
|
789
|
+
return result
|
|
790
|
+
|
|
791
|
+
capture_sheet_images_tool = app.tool(name="exstruct_capture_sheet_images")
|
|
792
|
+
capture_sheet_images_tool(_capture_sheet_images_tool)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _get_capture_sheet_images_timeout_seconds() -> float:
|
|
796
|
+
"""Return capture timeout seconds from environment."""
|
|
797
|
+
raw_value = os.getenv("EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC")
|
|
798
|
+
if raw_value is None:
|
|
799
|
+
return _DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS
|
|
800
|
+
try:
|
|
801
|
+
parsed = float(raw_value)
|
|
802
|
+
except ValueError:
|
|
803
|
+
logger.warning(
|
|
804
|
+
"Invalid EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC=%r. "
|
|
805
|
+
"Falling back to %.1fs.",
|
|
806
|
+
raw_value,
|
|
807
|
+
_DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS,
|
|
808
|
+
)
|
|
809
|
+
return _DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS
|
|
810
|
+
if not math.isfinite(parsed):
|
|
811
|
+
logger.warning(
|
|
812
|
+
"Non-finite EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC=%r. "
|
|
813
|
+
"Falling back to %.1fs.",
|
|
814
|
+
raw_value,
|
|
815
|
+
_DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS,
|
|
816
|
+
)
|
|
817
|
+
return _DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS
|
|
818
|
+
if parsed <= 0:
|
|
819
|
+
logger.warning(
|
|
820
|
+
"Non-positive EXSTRUCT_MCP_CAPTURE_SHEET_IMAGES_TIMEOUT_SEC=%r. "
|
|
821
|
+
"Falling back to %.1fs.",
|
|
822
|
+
raw_value,
|
|
823
|
+
_DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS,
|
|
824
|
+
)
|
|
825
|
+
return _DEFAULT_CAPTURE_SHEET_IMAGES_TIMEOUT_SECONDS
|
|
826
|
+
return parsed
|
|
827
|
+
|
|
828
|
+
|
|
725
829
|
def _register_op_schema_tools(app: FastMCP) -> None:
|
|
726
830
|
"""Register schema discovery tools."""
|
|
727
831
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .a1 import (
|
|
4
|
+
QualifiedA1Range,
|
|
5
|
+
SheetRangeSelection,
|
|
6
|
+
column_index_to_label,
|
|
7
|
+
column_label_to_index,
|
|
8
|
+
normalize_range,
|
|
9
|
+
parse_qualified_a1_range,
|
|
10
|
+
parse_range_geometry,
|
|
11
|
+
range_cell_count,
|
|
12
|
+
resolve_sheet_and_range,
|
|
13
|
+
split_a1,
|
|
14
|
+
)
|
|
15
|
+
from .output_path import (
|
|
16
|
+
apply_conflict_policy,
|
|
17
|
+
next_available_directory,
|
|
18
|
+
next_available_path,
|
|
19
|
+
resolve_image_output_dir,
|
|
20
|
+
resolve_output_path,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"QualifiedA1Range",
|
|
25
|
+
"SheetRangeSelection",
|
|
26
|
+
"apply_conflict_policy",
|
|
27
|
+
"column_index_to_label",
|
|
28
|
+
"column_label_to_index",
|
|
29
|
+
"next_available_path",
|
|
30
|
+
"next_available_directory",
|
|
31
|
+
"normalize_range",
|
|
32
|
+
"parse_qualified_a1_range",
|
|
33
|
+
"parse_range_geometry",
|
|
34
|
+
"range_cell_count",
|
|
35
|
+
"resolve_image_output_dir",
|
|
36
|
+
"resolve_sheet_and_range",
|
|
37
|
+
"resolve_output_path",
|
|
38
|
+
"split_a1",
|
|
39
|
+
]
|