exstruct 0.4.4__tar.gz → 0.5.1__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.4.4 → exstruct-0.5.1}/PKG-INFO +39 -3
- {exstruct-0.4.4 → exstruct-0.5.1}/README.md +38 -2
- {exstruct-0.4.4 → exstruct-0.5.1}/pyproject.toml +5 -4
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/__init__.py +20 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/extract_runner.py +15 -33
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/io.py +12 -2
- exstruct-0.5.1/src/exstruct/mcp/op_schema.py +437 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/__init__.py +13 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/chart_types.py +63 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/engine/__init__.py +6 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/engine/base.py +34 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/engine/openpyxl_engine.py +18 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/engine/xlwings_engine.py +19 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/internal.py +4302 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/models.py +1556 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/normalize.py +181 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/ops/__init__.py +6 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/ops/common.py +7 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/ops/openpyxl_ops.py +69 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/ops/xlwings_ops.py +26 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/runtime.py +159 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/service.py +342 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/specs.py +58 -0
- exstruct-0.5.1/src/exstruct/mcp/patch/types.py +53 -0
- exstruct-0.5.1/src/exstruct/mcp/patch_runner.py +74 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/server.py +261 -36
- exstruct-0.5.1/src/exstruct/mcp/shared/__init__.py +23 -0
- exstruct-0.5.1/src/exstruct/mcp/shared/a1.py +82 -0
- exstruct-0.5.1/src/exstruct/mcp/shared/output_path.py +85 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/tools.py +302 -2
- exstruct-0.4.4/src/exstruct/mcp/patch_runner.py +0 -1315
- {exstruct-0.4.4 → exstruct-0.5.1}/LICENSE +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/__init__.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/cli/availability.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/cli/main.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/__init__.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/backends/__init__.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/backends/base.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/backends/com_backend.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/backends/openpyxl_backend.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/cells.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/charts.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/integrate.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/logging_utils.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/modeling.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/pipeline.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/ranges.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/shapes.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/core/workbook.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/engine.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/errors.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/io/__init__.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/io/serialize.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/chunk_reader.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/sheet_reader.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/mcp/validate_input.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/models/__init__.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/models/maps.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/models/types.py +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/py.typed +0 -0
- {exstruct-0.4.4 → exstruct-0.5.1}/src/exstruct/render/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exstruct
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -65,7 +65,8 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
|
|
66
66
|
# ExStruct — Excel Structured Extraction Engine
|
|
67
67
|
|
|
68
|
-
[](https://pypi.org/project/exstruct/) [](https://pepy.tech/projects/exstruct)  [](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://codecov.io/gh/harumiWeb/exstruct) [](https://deepwiki.com/harumiWeb/exstruct)
|
|
68
|
+
[](https://pypi.org/project/exstruct/) [](https://pepy.tech/projects/exstruct)  [](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://codecov.io/gh/harumiWeb/exstruct) [](https://deepwiki.com/harumiWeb/exstruct) 
|
|
69
|
+
|
|
69
70
|
|
|
70
71
|

|
|
71
72
|
|
|
@@ -120,15 +121,34 @@ exstruct input.xlsx --format toon # TOON (needs python-toon)
|
|
|
120
121
|
exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
|
|
121
122
|
exstruct input.xlsx --auto-page-breaks-dir auto_areas/ # COM only; option appears when available
|
|
122
123
|
exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
|
|
124
|
+
exstruct input.xlsx --alpha-col # output column keys as A, B, ..., AA
|
|
123
125
|
exstruct input.xlsx --mode light # cells + table candidates only
|
|
124
126
|
exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
Auto page-break exports are available via API and CLI when Excel/COM is available; the CLI exposes `--auto-page-breaks-dir` only in COM-capable environments.
|
|
130
|
+
By default, CLI keeps legacy 0-based numeric string column keys (`"0"`, `"1"`, ...). Use `--alpha-col` to emit Excel-style column keys (`"A"`, `"B"`, ...).
|
|
131
|
+
Note: MCP `exstruct_extract` defaults to `options.alpha_col=true`, while CLI defaults to `false` unless `--alpha-col` is specified.
|
|
128
132
|
|
|
129
133
|
## MCP Server (stdio)
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
### Quick Start with uvx (recommended)
|
|
136
|
+
|
|
137
|
+
Run directly without installation:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uvx --from 'exstruct[mcp]' exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict rename
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Benefits:
|
|
144
|
+
- No `pip install` required
|
|
145
|
+
- Automatic dependency management
|
|
146
|
+
- Environment isolation
|
|
147
|
+
- Easy version pinning: `uvx --from 'exstruct[mcp]==0.4.4' exstruct-mcp`
|
|
148
|
+
|
|
149
|
+
### Traditional Installation
|
|
150
|
+
|
|
151
|
+
Alternatively, install with pip:
|
|
132
152
|
|
|
133
153
|
```bash
|
|
134
154
|
pip install exstruct[mcp]
|
|
@@ -138,8 +158,12 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re
|
|
|
138
158
|
Available tools:
|
|
139
159
|
|
|
140
160
|
- `exstruct_extract`
|
|
161
|
+
- `exstruct_make`
|
|
141
162
|
- `exstruct_patch`
|
|
142
163
|
- `exstruct_read_json_chunk`
|
|
164
|
+
- `exstruct_read_range`
|
|
165
|
+
- `exstruct_read_cells`
|
|
166
|
+
- `exstruct_read_formulas`
|
|
143
167
|
- `exstruct_validate_input`
|
|
144
168
|
|
|
145
169
|
Notes:
|
|
@@ -147,6 +171,18 @@ Notes:
|
|
|
147
171
|
- 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.
|
|
148
172
|
- Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses.
|
|
149
173
|
- 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
|
+
- `exstruct_patch` supports `backend` selection:
|
|
175
|
+
- `auto` (default): prefer COM when available, otherwise openpyxl
|
|
176
|
+
- `com`: force COM (rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`)
|
|
177
|
+
- `openpyxl`: force openpyxl (`.xls` is not supported)
|
|
178
|
+
- `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` and `apply_table_style` cannot be combined in one request (split into separate calls).
|
|
181
|
+
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
182
|
+
- `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
|
|
183
|
+
- Supports `.xlsx` / `.xlsm` / `.xls`
|
|
184
|
+
- Initial sheet is normalized to `Sheet1`
|
|
185
|
+
- `.xls` requires COM and rejects `backend=openpyxl`
|
|
150
186
|
|
|
151
187
|
MCP Setup Guide for Each AI Agent:
|
|
152
188
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ExStruct — Excel Structured Extraction Engine
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/exstruct/) [](https://pepy.tech/projects/exstruct)  [](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://codecov.io/gh/harumiWeb/exstruct) [](https://deepwiki.com/harumiWeb/exstruct)
|
|
3
|
+
[](https://pypi.org/project/exstruct/) [](https://pepy.tech/projects/exstruct)  [](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://codecov.io/gh/harumiWeb/exstruct) [](https://deepwiki.com/harumiWeb/exstruct) 
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|

|
|
6
7
|
|
|
@@ -55,15 +56,34 @@ exstruct input.xlsx --format toon # TOON (needs python-toon)
|
|
|
55
56
|
exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
|
|
56
57
|
exstruct input.xlsx --auto-page-breaks-dir auto_areas/ # COM only; option appears when available
|
|
57
58
|
exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
|
|
59
|
+
exstruct input.xlsx --alpha-col # output column keys as A, B, ..., AA
|
|
58
60
|
exstruct input.xlsx --mode light # cells + table candidates only
|
|
59
61
|
exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
Auto page-break exports are available via API and CLI when Excel/COM is available; the CLI exposes `--auto-page-breaks-dir` only in COM-capable environments.
|
|
65
|
+
By default, CLI keeps legacy 0-based numeric string column keys (`"0"`, `"1"`, ...). Use `--alpha-col` to emit Excel-style column keys (`"A"`, `"B"`, ...).
|
|
66
|
+
Note: MCP `exstruct_extract` defaults to `options.alpha_col=true`, while CLI defaults to `false` unless `--alpha-col` is specified.
|
|
63
67
|
|
|
64
68
|
## MCP Server (stdio)
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
### Quick Start with uvx (recommended)
|
|
71
|
+
|
|
72
|
+
Run directly without installation:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uvx --from 'exstruct[mcp]' exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict rename
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Benefits:
|
|
79
|
+
- No `pip install` required
|
|
80
|
+
- Automatic dependency management
|
|
81
|
+
- Environment isolation
|
|
82
|
+
- Easy version pinning: `uvx --from 'exstruct[mcp]==0.4.4' exstruct-mcp`
|
|
83
|
+
|
|
84
|
+
### Traditional Installation
|
|
85
|
+
|
|
86
|
+
Alternatively, install with pip:
|
|
67
87
|
|
|
68
88
|
```bash
|
|
69
89
|
pip install exstruct[mcp]
|
|
@@ -73,8 +93,12 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re
|
|
|
73
93
|
Available tools:
|
|
74
94
|
|
|
75
95
|
- `exstruct_extract`
|
|
96
|
+
- `exstruct_make`
|
|
76
97
|
- `exstruct_patch`
|
|
77
98
|
- `exstruct_read_json_chunk`
|
|
99
|
+
- `exstruct_read_range`
|
|
100
|
+
- `exstruct_read_cells`
|
|
101
|
+
- `exstruct_read_formulas`
|
|
78
102
|
- `exstruct_validate_input`
|
|
79
103
|
|
|
80
104
|
Notes:
|
|
@@ -82,6 +106,18 @@ Notes:
|
|
|
82
106
|
- 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.
|
|
83
107
|
- Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses.
|
|
84
108
|
- 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
|
+
- `exstruct_patch` supports `backend` selection:
|
|
110
|
+
- `auto` (default): prefer COM when available, otherwise openpyxl
|
|
111
|
+
- `com`: force COM (rejects `dry_run` / `return_inverse_ops` / `preflight_formula_check`)
|
|
112
|
+
- `openpyxl`: force openpyxl (`.xls` is not supported)
|
|
113
|
+
- `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` and `apply_table_style` cannot be combined in one request (split into separate calls).
|
|
116
|
+
- `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
|
|
117
|
+
- `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
|
|
118
|
+
- Supports `.xlsx` / `.xlsm` / `.xls`
|
|
119
|
+
- Initial sheet is normalized to `Sheet1`
|
|
120
|
+
- `.xls` requires COM and rejects `backend=openpyxl`
|
|
85
121
|
|
|
86
122
|
MCP Setup Guide for Each AI Agent:
|
|
87
123
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "exstruct"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -68,6 +68,7 @@ omit = [
|
|
|
68
68
|
"tests/*",
|
|
69
69
|
"*/test_*.py",
|
|
70
70
|
"*/gen_py/*",
|
|
71
|
+
"src/exstruct/mcp/patch/engine/base.py",
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
[tool.ruff]
|
|
@@ -136,9 +137,9 @@ ruff = "ruff check ."
|
|
|
136
137
|
ruff-fix = "ruff check . --fix"
|
|
137
138
|
mypy = "mypy src/exstruct --strict"
|
|
138
139
|
precommit-run = "pre-commit run -a"
|
|
139
|
-
test = "pytest -vv --cov=exstruct --cov-report=term-missing --cov-report=xml" # uv sync --extra render --extra toon
|
|
140
|
-
test-unit = "pytest -vv -m \"not com and not render\" --cov=exstruct --cov-report=term-missing --cov-report=xml"
|
|
141
|
-
test-com = "pytest -vv -m \"com\" --cov=exstruct --cov-report=term-missing --cov-report=xml"
|
|
140
|
+
test = "pytest -vv --cov=exstruct --cov-report=term-missing --cov-report=xml --cov-fail-under=80" # uv sync --extra render --extra toon
|
|
141
|
+
test-unit = "pytest -vv -m \"not com and not render\" --cov=exstruct --cov-report=term-missing --cov-report=xml --cov-fail-under=80"
|
|
142
|
+
test-com = "pytest -vv -m \"com\" --cov=exstruct --cov-report=term-missing --cov-report=xml --cov-fail-under=80"
|
|
142
143
|
codecov-unit = "codecov-cli upload-process -f coverage.xml -F unit -C %CODECOV_SHA% -t %CODECOV_TOKEN%"
|
|
143
144
|
codecov-com = "codecov-cli upload-process -f coverage.xml -F com -C %CODECOV_SHA% -t %CODECOV_TOKEN%"
|
|
144
145
|
docs = "mkdocs serve"
|
|
@@ -18,12 +18,14 @@ from .extract_runner import (
|
|
|
18
18
|
from .io import PathPolicy
|
|
19
19
|
from .patch_runner import (
|
|
20
20
|
FormulaIssue,
|
|
21
|
+
MakeRequest,
|
|
21
22
|
PatchDiffItem,
|
|
22
23
|
PatchErrorDetail,
|
|
23
24
|
PatchOp,
|
|
24
25
|
PatchRequest,
|
|
25
26
|
PatchResult,
|
|
26
27
|
PatchValue,
|
|
28
|
+
run_make,
|
|
27
29
|
run_patch,
|
|
28
30
|
)
|
|
29
31
|
from .sheet_reader import (
|
|
@@ -40,8 +42,13 @@ from .sheet_reader import (
|
|
|
40
42
|
read_range,
|
|
41
43
|
)
|
|
42
44
|
from .tools import (
|
|
45
|
+
DescribeOpToolInput,
|
|
46
|
+
DescribeOpToolOutput,
|
|
43
47
|
ExtractToolInput,
|
|
44
48
|
ExtractToolOutput,
|
|
49
|
+
ListOpsToolOutput,
|
|
50
|
+
MakeToolInput,
|
|
51
|
+
MakeToolOutput,
|
|
45
52
|
PatchToolInput,
|
|
46
53
|
PatchToolOutput,
|
|
47
54
|
ReadCellsToolInput,
|
|
@@ -54,7 +61,10 @@ from .tools import (
|
|
|
54
61
|
ReadRangeToolOutput,
|
|
55
62
|
ValidateInputToolInput,
|
|
56
63
|
ValidateInputToolOutput,
|
|
64
|
+
run_describe_op_tool,
|
|
57
65
|
run_extract_tool,
|
|
66
|
+
run_list_ops_tool,
|
|
67
|
+
run_make_tool,
|
|
58
68
|
run_patch_tool,
|
|
59
69
|
run_read_cells_tool,
|
|
60
70
|
run_read_formulas_tool,
|
|
@@ -69,6 +79,8 @@ from .validate_input import (
|
|
|
69
79
|
)
|
|
70
80
|
|
|
71
81
|
__all__ = [
|
|
82
|
+
"DescribeOpToolInput",
|
|
83
|
+
"DescribeOpToolOutput",
|
|
72
84
|
"ExtractRequest",
|
|
73
85
|
"ExtractResult",
|
|
74
86
|
"ExtractOptions",
|
|
@@ -76,6 +88,10 @@ __all__ = [
|
|
|
76
88
|
"ExtractToolOutput",
|
|
77
89
|
"FormulaIssue",
|
|
78
90
|
"FormulaReadItem",
|
|
91
|
+
"ListOpsToolOutput",
|
|
92
|
+
"MakeRequest",
|
|
93
|
+
"MakeToolInput",
|
|
94
|
+
"MakeToolOutput",
|
|
79
95
|
"PatchDiffItem",
|
|
80
96
|
"PatchErrorDetail",
|
|
81
97
|
"PatchOp",
|
|
@@ -111,7 +127,11 @@ __all__ = [
|
|
|
111
127
|
"read_json_chunk",
|
|
112
128
|
"validate_input",
|
|
113
129
|
"run_extract",
|
|
130
|
+
"run_describe_op_tool",
|
|
114
131
|
"run_extract_tool",
|
|
132
|
+
"run_list_ops_tool",
|
|
133
|
+
"run_make",
|
|
134
|
+
"run_make_tool",
|
|
115
135
|
"run_patch",
|
|
116
136
|
"run_patch_tool",
|
|
117
137
|
"read_cells",
|
|
@@ -9,6 +9,11 @@ from pydantic import BaseModel, Field
|
|
|
9
9
|
from exstruct import ExtractionMode, process_excel
|
|
10
10
|
|
|
11
11
|
from .io import PathPolicy
|
|
12
|
+
from .shared.output_path import (
|
|
13
|
+
apply_conflict_policy as _shared_apply_conflict_policy,
|
|
14
|
+
next_available_path as _shared_next_available_path,
|
|
15
|
+
resolve_output_path as _shared_resolve_output_path,
|
|
16
|
+
)
|
|
12
17
|
|
|
13
18
|
logger = logging.getLogger(__name__)
|
|
14
19
|
|
|
@@ -179,14 +184,14 @@ def _resolve_output_path(
|
|
|
179
184
|
Raises:
|
|
180
185
|
ValueError: If the path violates the policy.
|
|
181
186
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
return _shared_resolve_output_path(
|
|
188
|
+
input_path,
|
|
189
|
+
out_dir=out_dir,
|
|
190
|
+
out_name=out_name,
|
|
191
|
+
policy=policy,
|
|
192
|
+
default_suffix=_format_suffix(fmt),
|
|
193
|
+
default_name_builder="same_stem",
|
|
194
|
+
)
|
|
190
195
|
|
|
191
196
|
|
|
192
197
|
def _normalize_output_name(input_path: Path, out_name: str | None, suffix: str) -> str:
|
|
@@ -259,35 +264,12 @@ def _apply_conflict_policy(
|
|
|
259
264
|
Returns:
|
|
260
265
|
Tuple of (resolved output path, warning message or None, skipped flag).
|
|
261
266
|
"""
|
|
262
|
-
|
|
263
|
-
return output_path, None, False
|
|
264
|
-
if on_conflict == "skip":
|
|
265
|
-
return (
|
|
266
|
-
output_path,
|
|
267
|
-
f"Output exists; skipping write: {output_path.name}",
|
|
268
|
-
True,
|
|
269
|
-
)
|
|
270
|
-
if on_conflict == "rename":
|
|
271
|
-
renamed = _next_available_path(output_path)
|
|
272
|
-
return (
|
|
273
|
-
renamed,
|
|
274
|
-
f"Output exists; renamed to: {renamed.name}",
|
|
275
|
-
False,
|
|
276
|
-
)
|
|
277
|
-
return output_path, None, False
|
|
267
|
+
return _shared_apply_conflict_policy(output_path, on_conflict)
|
|
278
268
|
|
|
279
269
|
|
|
280
270
|
def _next_available_path(path: Path) -> Path:
|
|
281
271
|
"""Return the next available path by appending a numeric suffix."""
|
|
282
|
-
|
|
283
|
-
return path
|
|
284
|
-
stem = path.stem
|
|
285
|
-
suffix = path.suffix
|
|
286
|
-
for idx in range(1, 10_000):
|
|
287
|
-
candidate = path.with_name(f"{stem}_{idx}{suffix}")
|
|
288
|
-
if not candidate.exists():
|
|
289
|
-
return candidate
|
|
290
|
-
raise RuntimeError(f"Failed to resolve unique path for {path}")
|
|
272
|
+
return _shared_next_available_path(path)
|
|
291
273
|
|
|
292
274
|
|
|
293
275
|
def _try_read_workbook_meta(path: Path) -> tuple[WorkbookMeta | None, list[str]]:
|
|
@@ -33,14 +33,24 @@ class PathPolicy(BaseModel):
|
|
|
33
33
|
Raises:
|
|
34
34
|
ValueError: If the path is outside the root or denied by glob.
|
|
35
35
|
"""
|
|
36
|
-
resolved = path.resolve()
|
|
37
36
|
root = self.normalize_root()
|
|
37
|
+
resolved = self._resolve_from_root(path, root)
|
|
38
38
|
if resolved != root and root not in resolved.parents:
|
|
39
|
-
raise ValueError(
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"Path is outside root. "
|
|
41
|
+
f"resolved={resolved}, root={root}, "
|
|
42
|
+
"example_relative='outputs/book.xlsx'. "
|
|
43
|
+
"Use exstruct_get_runtime_info to inspect valid root-based paths."
|
|
44
|
+
)
|
|
40
45
|
if self._is_denied(resolved, root):
|
|
41
46
|
raise ValueError(f"Path is denied by policy: {resolved}")
|
|
42
47
|
return resolved
|
|
43
48
|
|
|
49
|
+
def _resolve_from_root(self, path: Path, root: Path) -> Path:
|
|
50
|
+
"""Resolve path using root as the base for relative inputs."""
|
|
51
|
+
candidate = path if path.is_absolute() else root / path
|
|
52
|
+
return candidate.resolve()
|
|
53
|
+
|
|
44
54
|
def _is_denied(self, path: Path, root: Path) -> bool:
|
|
45
55
|
"""Check if a path is denied by glob rules.
|
|
46
56
|
|