refigure 0.3.0__tar.gz → 0.3.2__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.
- {refigure-0.3.0 → refigure-0.3.2}/.gitignore +1 -0
- refigure-0.3.0/README.md → refigure-0.3.2/PKG-INFO +118 -2
- refigure-0.3.0/PKG-INFO → refigure-0.3.2/README.md +78 -34
- {refigure-0.3.0 → refigure-0.3.2}/pyproject.toml +9 -1
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/server.py +3 -1
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_http.py +29 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_tools.py +13 -0
- {refigure-0.3.0 → refigure-0.3.2}/ATTRIBUTION.md +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/LICENSE +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/NOTICE +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/__main__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/_io.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/api.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/cli.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/core/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/core/chart_data.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/core/chart_render.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/core/zipsafe.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/docx/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/docx_groups.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/_lru.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/auth.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/cli.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/exceptions.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/state.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/mcp/vlm_cache.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/py.typed +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/vlm/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/vlm/cache.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/vlm/client.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/xlsx/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/refigure/xlsx/charts.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/support.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/core/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/core/test_chart_data.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/core/test_chart_render.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/core/test_chart_render_missing_mermaidx.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/core/test_chart_render_visual.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/docx/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/docx/test_docx.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/conftest.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_auth.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_batch.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_bridge.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_cli.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_lru.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_prompts.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_resources.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_schema_pin.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_state.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/mcp/test_vlm_cache.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_cli.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_concurrency.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_docx_chart_group_coexistence.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_docx_groups.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_io.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_optional_dependency_guards.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_property_based.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_robustness.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_vlm_max_markers.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/test_xml_security.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/vlm/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/vlm/test_soffice_profile_isolation.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/vlm/test_vlm.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/vlm/test_vlm_cache.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/vlm/test_vlm_client.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/xlsx/__init__.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/xlsx/test_xlsx.py +0 -0
- {refigure-0.3.0 → refigure-0.3.2}/tests/unit/xlsx/test_xlsx_charts.py +0 -0
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: refigure
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: DOCX/XLSX -> Markdown conversion with native OOXML chart-data extraction (no rasterize/OCR/VLM needed) + optional VLM interpretation for figures with no native chart data
|
|
5
|
+
Project-URL: Homepage, https://github.com/HelgDemidov/refigure
|
|
6
|
+
Project-URL: Repository, https://github.com/HelgDemidov/refigure
|
|
7
|
+
Author-email: HelgDemidov <helg.demidov88@gmail.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: lxml<7,>=6.1
|
|
18
|
+
Provides-Extra: docx
|
|
19
|
+
Requires-Dist: mammoth<2,>=1.12; extra == 'docx'
|
|
20
|
+
Requires-Dist: markdownify<2,>=1.2; extra == 'docx'
|
|
21
|
+
Provides-Extra: full
|
|
22
|
+
Requires-Dist: anthropic<1,>=0.120; extra == 'full'
|
|
23
|
+
Requires-Dist: mammoth<2,>=1.12; extra == 'full'
|
|
24
|
+
Requires-Dist: markdownify<2,>=1.2; extra == 'full'
|
|
25
|
+
Requires-Dist: mcp<3,>=2.0; extra == 'full'
|
|
26
|
+
Requires-Dist: openai<4,>=3.1.0; extra == 'full'
|
|
27
|
+
Requires-Dist: openpyxl<4,>=3.1; extra == 'full'
|
|
28
|
+
Requires-Dist: pdfplumber<0.12,>=0.11; extra == 'full'
|
|
29
|
+
Provides-Extra: mcp
|
|
30
|
+
Requires-Dist: mcp<3,>=2.0; extra == 'mcp'
|
|
31
|
+
Provides-Extra: vlm
|
|
32
|
+
Requires-Dist: pdfplumber<0.12,>=0.11; extra == 'vlm'
|
|
33
|
+
Provides-Extra: vlm-direct
|
|
34
|
+
Requires-Dist: anthropic<1,>=0.120; extra == 'vlm-direct'
|
|
35
|
+
Requires-Dist: openai<4,>=3.1.0; extra == 'vlm-direct'
|
|
36
|
+
Requires-Dist: pdfplumber<0.12,>=0.11; extra == 'vlm-direct'
|
|
37
|
+
Provides-Extra: xlsx
|
|
38
|
+
Requires-Dist: openpyxl<4,>=3.1; extra == 'xlsx'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
1
41
|
# refigure
|
|
2
42
|
|
|
3
43
|
**Converters where figures survive.**
|
|
@@ -7,6 +47,8 @@
|
|
|
7
47
|
[](LICENSE)
|
|
8
48
|
[](pyproject.toml)
|
|
9
49
|
|
|
50
|
+
<!-- mcp-name: io.github.HelgDemidov/refigure -->
|
|
51
|
+
|
|
10
52
|
DOCX / XLSX → Markdown converters that treat embedded charts, composite
|
|
11
53
|
diagrams and infographics as single semantic objects instead of silently
|
|
12
54
|
dropping or fragmenting them: native OOXML chart-data extraction (no
|
|
@@ -68,6 +110,12 @@ print(result.markdown)
|
|
|
68
110
|
print(f"{result.charts_found} charts, {result.groups_found} composite figures")
|
|
69
111
|
```
|
|
70
112
|
|
|
113
|
+
Or without a permanent install, via [uv](https://docs.astral.sh/uv/)/`uvx`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
uvx --from "refigure[docx,xlsx]" refigure report.docx
|
|
117
|
+
```
|
|
118
|
+
|
|
71
119
|
Optional VLM interpretation, for a composite figure the chart engine can't
|
|
72
120
|
reconstruct on its own (see Features below):
|
|
73
121
|
|
|
@@ -103,6 +151,9 @@ refigure report.docx --vlm # needs the system soffice/LibreOf
|
|
|
103
151
|
- **MCP server included** — `refigure-mcp` console command (`[mcp]` extra),
|
|
104
152
|
stdio or Streamable HTTP, tools/resources/prompts, batch conversion with
|
|
105
153
|
per-file isolation (see below).
|
|
154
|
+
- **Docker image** — `ghcr.io/helgdemidov/refigure`, both console commands
|
|
155
|
+
on `PATH`, `soffice`/LibreOffice baked in — the VLM composite-figure
|
|
156
|
+
path works turnkey, no manual LibreOffice install (see below).
|
|
106
157
|
|
|
107
158
|
## CLI
|
|
108
159
|
|
|
@@ -156,6 +207,22 @@ refigure-mcp # stdio — the MCP client launches it
|
|
|
156
207
|
}
|
|
157
208
|
```
|
|
158
209
|
|
|
210
|
+
Or point the client at `uvx` instead, with no permanent install at all:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"mcpServers": {
|
|
215
|
+
"refigure": {
|
|
216
|
+
"command": "uvx",
|
|
217
|
+
"args": ["--from", "refigure[mcp,docx,xlsx,vlm-direct]", "refigure-mcp"]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`refigure[full]` is a shortcut for `refigure[mcp,docx,xlsx,vlm-direct]` —
|
|
224
|
+
every tool, both formats, every VLM provider, one extras string.
|
|
225
|
+
|
|
159
226
|
Three tools — `convert_docx`, `convert_xlsx`, and `convert_batch` (multiple
|
|
160
227
|
files in one call: one bad file reports its own error without aborting the
|
|
161
228
|
rest) — each registered only if its format extra is actually installed.
|
|
@@ -179,6 +246,52 @@ fairness soft-cap once 2+ callers are configured; `refigure-mcp --help`
|
|
|
179
246
|
covers every tuning flag (concurrency, timeouts, resource-store limits,
|
|
180
247
|
batch size, VLM ceiling).
|
|
181
248
|
|
|
249
|
+
## Docker
|
|
250
|
+
|
|
251
|
+
One image, both surfaces — `refigure` and `refigure-mcp` are already on
|
|
252
|
+
`PATH`, no separate CLI/MCP builds to choose between. The one thing this
|
|
253
|
+
format buys over `pip`/`uvx` that neither can: the system `soffice`/
|
|
254
|
+
LibreOffice binary the VLM composite-figure path needs is baked in, not a
|
|
255
|
+
manual install.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
docker pull ghcr.io/helgdemidov/refigure:0.3.1
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
CLI, via a bind mount (the image's working directory is already `/data`):
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
docker run --rm -v "$PWD:/data:ro" ghcr.io/helgdemidov/refigure:0.3.1 \
|
|
265
|
+
refigure /data/report.docx
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
MCP, stdio — the client launches the container itself:
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"mcpServers": {
|
|
273
|
+
"refigure": {
|
|
274
|
+
"command": "docker",
|
|
275
|
+
"args": ["run", "-i", "--rm", "ghcr.io/helgdemidov/refigure:0.3.1", "refigure-mcp"]
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
MCP, Streamable HTTP — `--mcp-http-host 0.0.0.0` is required here, not
|
|
282
|
+
optional: the default `127.0.0.1` bind is unreachable through `-p` port
|
|
283
|
+
publishing (Docker's NAT reaches the container's external network
|
|
284
|
+
interface, not its loopback), so the "obvious" invocation without this
|
|
285
|
+
flag would silently never respond:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
echo "sk-... = alice" > tokens.txt
|
|
289
|
+
docker run --rm -p 8000:8000 -v "$PWD/tokens.txt:/data/tokens.txt:ro" \
|
|
290
|
+
ghcr.io/helgdemidov/refigure:0.3.1 \
|
|
291
|
+
refigure-mcp --transport http --mcp-http-host 0.0.0.0 \
|
|
292
|
+
--mcp-auth-token-file /data/tokens.txt
|
|
293
|
+
```
|
|
294
|
+
|
|
182
295
|
## Real examples
|
|
183
296
|
|
|
184
297
|
Full `convert()` output on real, openly-licensed documents — not
|
|
@@ -243,8 +356,11 @@ else:
|
|
|
243
356
|
markdown = refigure.xlsx.convert(path).markdown
|
|
244
357
|
```
|
|
245
358
|
|
|
246
|
-
`v0.1
|
|
247
|
-
|
|
359
|
+
`v0.3.1` published via trusted publishing (GitHub↔PyPI, no stored tokens),
|
|
360
|
+
also on GHCR as `ghcr.io/helgdemidov/refigure` and on the official
|
|
361
|
+
[MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
362
|
+
`io.github.HelgDemidov/refigure`. `refigure-md` is a reserved alternate
|
|
363
|
+
name, not an active release.
|
|
248
364
|
|
|
249
365
|
## License
|
|
250
366
|
|
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: refigure
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: DOCX/XLSX -> Markdown conversion with native OOXML chart-data extraction (no rasterize/OCR/VLM needed) + optional VLM interpretation for figures with no native chart data
|
|
5
|
-
Project-URL: Homepage, https://github.com/HelgDemidov/refigure
|
|
6
|
-
Project-URL: Repository, https://github.com/HelgDemidov/refigure
|
|
7
|
-
Author-email: HelgDemidov <helg.demidov88@gmail.com>
|
|
8
|
-
License-Expression: Apache-2.0
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
License-File: NOTICE
|
|
11
|
-
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
16
|
-
Requires-Python: >=3.10
|
|
17
|
-
Requires-Dist: lxml<7,>=6.1
|
|
18
|
-
Provides-Extra: docx
|
|
19
|
-
Requires-Dist: mammoth<2,>=1.12; extra == 'docx'
|
|
20
|
-
Requires-Dist: markdownify<2,>=1.2; extra == 'docx'
|
|
21
|
-
Provides-Extra: mcp
|
|
22
|
-
Requires-Dist: mcp<3,>=2.0; extra == 'mcp'
|
|
23
|
-
Provides-Extra: vlm
|
|
24
|
-
Requires-Dist: pdfplumber<0.12,>=0.11; extra == 'vlm'
|
|
25
|
-
Provides-Extra: vlm-direct
|
|
26
|
-
Requires-Dist: anthropic<1,>=0.120; extra == 'vlm-direct'
|
|
27
|
-
Requires-Dist: openai<4,>=3.1.0; extra == 'vlm-direct'
|
|
28
|
-
Requires-Dist: pdfplumber<0.12,>=0.11; extra == 'vlm-direct'
|
|
29
|
-
Provides-Extra: xlsx
|
|
30
|
-
Requires-Dist: openpyxl<4,>=3.1; extra == 'xlsx'
|
|
31
|
-
Description-Content-Type: text/markdown
|
|
32
|
-
|
|
33
1
|
# refigure
|
|
34
2
|
|
|
35
3
|
**Converters where figures survive.**
|
|
@@ -39,6 +7,8 @@ Description-Content-Type: text/markdown
|
|
|
39
7
|
[](LICENSE)
|
|
40
8
|
[](pyproject.toml)
|
|
41
9
|
|
|
10
|
+
<!-- mcp-name: io.github.HelgDemidov/refigure -->
|
|
11
|
+
|
|
42
12
|
DOCX / XLSX → Markdown converters that treat embedded charts, composite
|
|
43
13
|
diagrams and infographics as single semantic objects instead of silently
|
|
44
14
|
dropping or fragmenting them: native OOXML chart-data extraction (no
|
|
@@ -100,6 +70,12 @@ print(result.markdown)
|
|
|
100
70
|
print(f"{result.charts_found} charts, {result.groups_found} composite figures")
|
|
101
71
|
```
|
|
102
72
|
|
|
73
|
+
Or without a permanent install, via [uv](https://docs.astral.sh/uv/)/`uvx`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uvx --from "refigure[docx,xlsx]" refigure report.docx
|
|
77
|
+
```
|
|
78
|
+
|
|
103
79
|
Optional VLM interpretation, for a composite figure the chart engine can't
|
|
104
80
|
reconstruct on its own (see Features below):
|
|
105
81
|
|
|
@@ -135,6 +111,9 @@ refigure report.docx --vlm # needs the system soffice/LibreOf
|
|
|
135
111
|
- **MCP server included** — `refigure-mcp` console command (`[mcp]` extra),
|
|
136
112
|
stdio or Streamable HTTP, tools/resources/prompts, batch conversion with
|
|
137
113
|
per-file isolation (see below).
|
|
114
|
+
- **Docker image** — `ghcr.io/helgdemidov/refigure`, both console commands
|
|
115
|
+
on `PATH`, `soffice`/LibreOffice baked in — the VLM composite-figure
|
|
116
|
+
path works turnkey, no manual LibreOffice install (see below).
|
|
138
117
|
|
|
139
118
|
## CLI
|
|
140
119
|
|
|
@@ -188,6 +167,22 @@ refigure-mcp # stdio — the MCP client launches it
|
|
|
188
167
|
}
|
|
189
168
|
```
|
|
190
169
|
|
|
170
|
+
Or point the client at `uvx` instead, with no permanent install at all:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"mcpServers": {
|
|
175
|
+
"refigure": {
|
|
176
|
+
"command": "uvx",
|
|
177
|
+
"args": ["--from", "refigure[mcp,docx,xlsx,vlm-direct]", "refigure-mcp"]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`refigure[full]` is a shortcut for `refigure[mcp,docx,xlsx,vlm-direct]` —
|
|
184
|
+
every tool, both formats, every VLM provider, one extras string.
|
|
185
|
+
|
|
191
186
|
Three tools — `convert_docx`, `convert_xlsx`, and `convert_batch` (multiple
|
|
192
187
|
files in one call: one bad file reports its own error without aborting the
|
|
193
188
|
rest) — each registered only if its format extra is actually installed.
|
|
@@ -211,6 +206,52 @@ fairness soft-cap once 2+ callers are configured; `refigure-mcp --help`
|
|
|
211
206
|
covers every tuning flag (concurrency, timeouts, resource-store limits,
|
|
212
207
|
batch size, VLM ceiling).
|
|
213
208
|
|
|
209
|
+
## Docker
|
|
210
|
+
|
|
211
|
+
One image, both surfaces — `refigure` and `refigure-mcp` are already on
|
|
212
|
+
`PATH`, no separate CLI/MCP builds to choose between. The one thing this
|
|
213
|
+
format buys over `pip`/`uvx` that neither can: the system `soffice`/
|
|
214
|
+
LibreOffice binary the VLM composite-figure path needs is baked in, not a
|
|
215
|
+
manual install.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
docker pull ghcr.io/helgdemidov/refigure:0.3.1
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
CLI, via a bind mount (the image's working directory is already `/data`):
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
docker run --rm -v "$PWD:/data:ro" ghcr.io/helgdemidov/refigure:0.3.1 \
|
|
225
|
+
refigure /data/report.docx
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
MCP, stdio — the client launches the container itself:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"mcpServers": {
|
|
233
|
+
"refigure": {
|
|
234
|
+
"command": "docker",
|
|
235
|
+
"args": ["run", "-i", "--rm", "ghcr.io/helgdemidov/refigure:0.3.1", "refigure-mcp"]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
MCP, Streamable HTTP — `--mcp-http-host 0.0.0.0` is required here, not
|
|
242
|
+
optional: the default `127.0.0.1` bind is unreachable through `-p` port
|
|
243
|
+
publishing (Docker's NAT reaches the container's external network
|
|
244
|
+
interface, not its loopback), so the "obvious" invocation without this
|
|
245
|
+
flag would silently never respond:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
echo "sk-... = alice" > tokens.txt
|
|
249
|
+
docker run --rm -p 8000:8000 -v "$PWD/tokens.txt:/data/tokens.txt:ro" \
|
|
250
|
+
ghcr.io/helgdemidov/refigure:0.3.1 \
|
|
251
|
+
refigure-mcp --transport http --mcp-http-host 0.0.0.0 \
|
|
252
|
+
--mcp-auth-token-file /data/tokens.txt
|
|
253
|
+
```
|
|
254
|
+
|
|
214
255
|
## Real examples
|
|
215
256
|
|
|
216
257
|
Full `convert()` output on real, openly-licensed documents — not
|
|
@@ -275,8 +316,11 @@ else:
|
|
|
275
316
|
markdown = refigure.xlsx.convert(path).markdown
|
|
276
317
|
```
|
|
277
318
|
|
|
278
|
-
`v0.1
|
|
279
|
-
|
|
319
|
+
`v0.3.1` published via trusted publishing (GitHub↔PyPI, no stored tokens),
|
|
320
|
+
also on GHCR as `ghcr.io/helgdemidov/refigure` and on the official
|
|
321
|
+
[MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
322
|
+
`io.github.HelgDemidov/refigure`. `refigure-md` is a reserved alternate
|
|
323
|
+
name, not an active release.
|
|
280
324
|
|
|
281
325
|
## License
|
|
282
326
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "refigure"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "DOCX/XLSX -> Markdown conversion with native OOXML chart-data extraction (no rasterize/OCR/VLM needed) + optional VLM interpretation for figures with no native chart data"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -64,6 +64,14 @@ vlm-direct = ["refigure[vlm]", "openai>=3.1.0,<4", "anthropic>=0.120,<1"]
|
|
|
64
64
|
# registered — see docs/mcp-server/mcp-server-architecture/
|
|
65
65
|
# mcp-server-architecture.md §2.
|
|
66
66
|
mcp = ["mcp>=2.0,<3"]
|
|
67
|
+
# Convenience meta-extra for one-shot `uvx --from 'refigure[full]' ...`
|
|
68
|
+
# invocations — union of every capability extra, nothing of its own. Carries
|
|
69
|
+
# no independent isolation contract: if any component subset breaks, the
|
|
70
|
+
# per-extra CI legs above catch it first. Exists specifically because [mcp]
|
|
71
|
+
# alone registers 0 tools (see comment above) and [vlm-direct] alone has no
|
|
72
|
+
# format to convert — a useful refigure-mcp needs all four together, and
|
|
73
|
+
# spelling that out by hand in every doc/README invocation got repetitive.
|
|
74
|
+
full = ["refigure[mcp,docx,xlsx,vlm-direct]"]
|
|
67
75
|
|
|
68
76
|
[project.scripts]
|
|
69
77
|
refigure = "refigure.cli:main"
|
|
@@ -45,6 +45,7 @@ from mcp.types import (
|
|
|
45
45
|
)
|
|
46
46
|
from pydantic import AnyHttpUrl
|
|
47
47
|
|
|
48
|
+
from .. import __version__
|
|
48
49
|
from ..api import (
|
|
49
50
|
Config,
|
|
50
51
|
ConversionResult,
|
|
@@ -1221,6 +1222,7 @@ def build_server(
|
|
|
1221
1222
|
if token_map is not None:
|
|
1222
1223
|
mcp = MCPServer(
|
|
1223
1224
|
"refigure",
|
|
1225
|
+
version=__version__,
|
|
1224
1226
|
token_verifier=_StaticTokenVerifier(token_map),
|
|
1225
1227
|
auth=AuthSettings(
|
|
1226
1228
|
issuer_url=AnyHttpUrl("http://localhost/"),
|
|
@@ -1228,7 +1230,7 @@ def build_server(
|
|
|
1228
1230
|
),
|
|
1229
1231
|
)
|
|
1230
1232
|
else:
|
|
1231
|
-
mcp = MCPServer("refigure")
|
|
1233
|
+
mcp = MCPServer("refigure", version=__version__)
|
|
1232
1234
|
server_ctx = _ServerContext(
|
|
1233
1235
|
limiter=anyio.CapacityLimiter(max_concurrent),
|
|
1234
1236
|
vlm_client=vlm_client,
|
|
@@ -70,6 +70,35 @@ def _build_http_server(**kwargs):
|
|
|
70
70
|
return build_server(transport="http", **kwargs)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
async def test_server_reports_its_own_version_over_http_too() -> None:
|
|
74
|
+
"""Same regression as test_tools.py's stdio-side version, exercised
|
|
75
|
+
against the OTHER build_server() construction branch (token_map is
|
|
76
|
+
not None -> the token_verifier/AuthSettings path) — both branches
|
|
77
|
+
pass version=__version__ as a separate literal, line coverage alone
|
|
78
|
+
doesn't prove this one's value is right."""
|
|
79
|
+
import refigure
|
|
80
|
+
|
|
81
|
+
mcp_server = _build_http_server()
|
|
82
|
+
app = mcp_server.streamable_http_app()
|
|
83
|
+
|
|
84
|
+
async with app.router.lifespan_context(app):
|
|
85
|
+
http_client = httpx.AsyncClient(
|
|
86
|
+
transport=httpx.ASGITransport(app=app),
|
|
87
|
+
base_url=_BASE_URL,
|
|
88
|
+
headers={"Authorization": "Bearer tok-alice"},
|
|
89
|
+
)
|
|
90
|
+
async with http_client:
|
|
91
|
+
async with streamable_http_client(url=f"{_BASE_URL}/mcp", http_client=http_client) as (
|
|
92
|
+
read,
|
|
93
|
+
write,
|
|
94
|
+
):
|
|
95
|
+
async with ClientSession(read, write) as session:
|
|
96
|
+
init = await session.initialize()
|
|
97
|
+
|
|
98
|
+
assert init.server_info.version == refigure.__version__
|
|
99
|
+
assert init.server_info.version != ""
|
|
100
|
+
|
|
101
|
+
|
|
73
102
|
async def test_valid_bearer_token_converts_successfully() -> None:
|
|
74
103
|
mcp_server = _build_http_server()
|
|
75
104
|
app = mcp_server.streamable_http_app()
|
|
@@ -182,6 +182,19 @@ async def test_list_tools_registers_both_convert_tools(client: Client) -> None:
|
|
|
182
182
|
assert tool.annotations.read_only_hint is True
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
async def test_server_reports_its_own_version_in_the_mcp_handshake(client: Client) -> None:
|
|
186
|
+
"""Regression: MCPServer("refigure") was built with no version= kwarg
|
|
187
|
+
at all, so a real client's initialize() handshake saw an empty string
|
|
188
|
+
instead of refigure's actual version — found live via a real
|
|
189
|
+
subprocess stdio_client() round trip (this repo's first, every other
|
|
190
|
+
test here uses the in-memory Client), not by code review."""
|
|
191
|
+
import refigure
|
|
192
|
+
|
|
193
|
+
assert client.server_info is not None
|
|
194
|
+
assert client.server_info.version == refigure.__version__
|
|
195
|
+
assert client.server_info.version != ""
|
|
196
|
+
|
|
197
|
+
|
|
185
198
|
async def test_corrupt_zip_bytes_are_rejected_as_corrupt_archive(client: Client) -> None:
|
|
186
199
|
"""A structurally-valid zip with no OOXML content still degrades to
|
|
187
200
|
CorruptArchiveError, not something more exotic — belt-and-suspenders
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|