exstruct 0.4.4__tar.gz → 0.5.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.
Files changed (60) hide show
  1. {exstruct-0.4.4 → exstruct-0.5.0}/PKG-INFO +36 -3
  2. {exstruct-0.4.4 → exstruct-0.5.0}/README.md +35 -2
  3. {exstruct-0.4.4 → exstruct-0.5.0}/pyproject.toml +5 -4
  4. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/__init__.py +20 -0
  5. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/extract_runner.py +15 -33
  6. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/io.py +12 -2
  7. exstruct-0.5.0/src/exstruct/mcp/op_schema.py +405 -0
  8. exstruct-0.5.0/src/exstruct/mcp/patch/__init__.py +13 -0
  9. exstruct-0.5.0/src/exstruct/mcp/patch/engine/__init__.py +6 -0
  10. exstruct-0.5.0/src/exstruct/mcp/patch/engine/base.py +34 -0
  11. exstruct-0.5.0/src/exstruct/mcp/patch/engine/openpyxl_engine.py +18 -0
  12. exstruct-0.5.0/src/exstruct/mcp/patch/engine/xlwings_engine.py +19 -0
  13. exstruct-0.5.0/src/exstruct/mcp/patch/internal.py +3930 -0
  14. exstruct-0.5.0/src/exstruct/mcp/patch/models.py +1427 -0
  15. exstruct-0.5.0/src/exstruct/mcp/patch/normalize.py +181 -0
  16. exstruct-0.5.0/src/exstruct/mcp/patch/ops/__init__.py +6 -0
  17. exstruct-0.5.0/src/exstruct/mcp/patch/ops/common.py +7 -0
  18. exstruct-0.5.0/src/exstruct/mcp/patch/ops/openpyxl_ops.py +29 -0
  19. exstruct-0.5.0/src/exstruct/mcp/patch/ops/xlwings_ops.py +26 -0
  20. exstruct-0.5.0/src/exstruct/mcp/patch/runtime.py +153 -0
  21. exstruct-0.5.0/src/exstruct/mcp/patch/service.py +325 -0
  22. exstruct-0.5.0/src/exstruct/mcp/patch/specs.py +57 -0
  23. exstruct-0.5.0/src/exstruct/mcp/patch/types.py +52 -0
  24. exstruct-0.5.0/src/exstruct/mcp/patch_runner.py +74 -0
  25. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/server.py +260 -36
  26. exstruct-0.5.0/src/exstruct/mcp/shared/__init__.py +23 -0
  27. exstruct-0.5.0/src/exstruct/mcp/shared/a1.py +82 -0
  28. exstruct-0.5.0/src/exstruct/mcp/shared/output_path.py +85 -0
  29. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/tools.py +783 -483
  30. exstruct-0.4.4/src/exstruct/mcp/patch_runner.py +0 -1315
  31. {exstruct-0.4.4 → exstruct-0.5.0}/LICENSE +0 -0
  32. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/__init__.py +0 -0
  33. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/cli/availability.py +0 -0
  34. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/cli/main.py +0 -0
  35. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/__init__.py +0 -0
  36. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/backends/__init__.py +0 -0
  37. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/backends/base.py +0 -0
  38. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/backends/com_backend.py +0 -0
  39. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/backends/openpyxl_backend.py +0 -0
  40. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/cells.py +0 -0
  41. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/charts.py +0 -0
  42. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/integrate.py +0 -0
  43. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/logging_utils.py +0 -0
  44. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/modeling.py +0 -0
  45. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/pipeline.py +0 -0
  46. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/ranges.py +0 -0
  47. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/shapes.py +0 -0
  48. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/core/workbook.py +0 -0
  49. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/engine.py +0 -0
  50. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/errors.py +0 -0
  51. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/io/__init__.py +0 -0
  52. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/io/serialize.py +0 -0
  53. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/chunk_reader.py +0 -0
  54. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/sheet_reader.py +0 -0
  55. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/mcp/validate_input.py +0 -0
  56. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/models/__init__.py +0 -0
  57. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/models/maps.py +0 -0
  58. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/models/types.py +0 -0
  59. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/py.typed +0 -0
  60. {exstruct-0.4.4 → exstruct-0.5.0}/src/exstruct/render/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.4.4
3
+ Version: 0.5.0
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
- [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![codecov](https://codecov.io/gh/harumiWeb/exstruct/graph/badge.svg?token=2XI1O8TTA9)](https://codecov.io/gh/harumiWeb/exstruct) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/harumiWeb/exstruct)
68
+ [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![codecov](https://codecov.io/gh/harumiWeb/exstruct/graph/badge.svg?token=2XI1O8TTA9)](https://codecov.io/gh/harumiWeb/exstruct) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/harumiWeb/exstruct) ![GitHub Repo stars](https://img.shields.io/github/stars/harumiWeb/exstruct)
69
+
69
70
 
70
71
  ![ExStruct Image](docs/assets/icon.webp)
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
- Install the MCP extras and run the stdio server:
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,15 @@ 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
+ - `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
179
+ - `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
180
+ - Supports `.xlsx` / `.xlsm` / `.xls`
181
+ - Initial sheet is normalized to `Sheet1`
182
+ - `.xls` requires COM and rejects `backend=openpyxl`
150
183
 
151
184
  MCP Setup Guide for Each AI Agent:
152
185
 
@@ -1,6 +1,7 @@
1
1
  # ExStruct — Excel Structured Extraction Engine
2
2
 
3
- [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![codecov](https://codecov.io/gh/harumiWeb/exstruct/graph/badge.svg?token=2XI1O8TTA9)](https://codecov.io/gh/harumiWeb/exstruct) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/harumiWeb/exstruct)
3
+ [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![codecov](https://codecov.io/gh/harumiWeb/exstruct/graph/badge.svg?token=2XI1O8TTA9)](https://codecov.io/gh/harumiWeb/exstruct) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/harumiWeb/exstruct) ![GitHub Repo stars](https://img.shields.io/github/stars/harumiWeb/exstruct)
4
+
4
5
 
5
6
  ![ExStruct Image](docs/assets/icon.webp)
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
- Install the MCP extras and run the stdio server:
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,15 @@ 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
+ - `exstruct_patch` response includes `engine` (`com` or `openpyxl`) to show the actual backend used. `restore_design_snapshot` remains openpyxl-only.
114
+ - `exstruct_make` creates a new workbook and applies `ops` in one call (`out_path` required, `ops` optional).
115
+ - Supports `.xlsx` / `.xlsm` / `.xls`
116
+ - Initial sheet is normalized to `Sheet1`
117
+ - `.xls` requires COM and rejects `backend=openpyxl`
85
118
 
86
119
  MCP Setup Guide for Each AI Agent:
87
120
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.4.4"
3
+ version = "0.5.0"
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
- target_dir = out_dir or input_path.parent
183
- target_dir = policy.ensure_allowed(target_dir) if policy else target_dir.resolve()
184
- suffix = _format_suffix(fmt)
185
- name = _normalize_output_name(input_path, out_name, suffix)
186
- output_path = (target_dir / name).resolve()
187
- if policy is not None:
188
- output_path = policy.ensure_allowed(output_path)
189
- return output_path
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
- if not output_path.exists():
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
- if not path.exists():
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(f"Path is outside root: {resolved}")
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