tabella-docs 2.0.1__tar.gz → 2.0.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.
- {tabella_docs-2.0.1/tabella_docs.egg-info → tabella_docs-2.0.3}/PKG-INFO +31 -13
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/README.md +26 -8
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/pyproject.toml +5 -5
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella/__init__.py +7 -13
- tabella_docs-2.0.3/tabella/bootstrap.py +144 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella/convert.py +44 -9
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella/create.py +150 -12
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella/read.py +1 -1
- {tabella_docs-2.0.1 → tabella_docs-2.0.3/tabella_docs.egg-info}/PKG-INFO +31 -13
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella_docs.egg-info/SOURCES.txt +5 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella_docs.egg-info/requires.txt +4 -4
- tabella_docs-2.0.3/tests/test_bootstrap_paths.py +153 -0
- tabella_docs-2.0.3/tests/test_ensure_venv_win.py +117 -0
- tabella_docs-2.0.3/tests/test_odt_table.py +95 -0
- tabella_docs-2.0.3/tests/test_security_gates.py +188 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tests/test_text_section_alias.py +7 -2
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/LICENSE +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/setup.cfg +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella/utils.py +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella_docs.egg-info/dependency_links.txt +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.3}/tabella_docs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabella-docs
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Portable document lifecycle suite — create (.docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp), convert (pandoc), and read (anydoc + LiteParse OCR) documents. Agent Plugins v1 compatible.
|
|
5
5
|
Author-email: D Yiapanis <d@yiapanis.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,9 +23,9 @@ Provides-Extra: create
|
|
|
23
23
|
Requires-Dist: python-docx<2.0,>=1.0; extra == "create"
|
|
24
24
|
Requires-Dist: openpyxl<4.0,>=3.1; extra == "create"
|
|
25
25
|
Requires-Dist: python-pptx<2.0,>=0.6; extra == "create"
|
|
26
|
-
Requires-Dist: weasyprint<
|
|
26
|
+
Requires-Dist: weasyprint<71.0,>=70.0; extra == "create"
|
|
27
27
|
Requires-Dist: odfpy<2.0,>=1.4; extra == "create"
|
|
28
|
-
Requires-Dist: Pillow<
|
|
28
|
+
Requires-Dist: Pillow<14.0,>=10.0; extra == "create"
|
|
29
29
|
Requires-Dist: lxml<7.0,>=5.0; extra == "create"
|
|
30
30
|
Provides-Extra: read
|
|
31
31
|
Requires-Dist: firecrawl-anydoc<0.3.0,>=0.1.1; extra == "read"
|
|
@@ -36,9 +36,9 @@ Provides-Extra: all
|
|
|
36
36
|
Requires-Dist: python-docx<2.0,>=1.0; extra == "all"
|
|
37
37
|
Requires-Dist: openpyxl<4.0,>=3.1; extra == "all"
|
|
38
38
|
Requires-Dist: python-pptx<2.0,>=0.6; extra == "all"
|
|
39
|
-
Requires-Dist: weasyprint<
|
|
39
|
+
Requires-Dist: weasyprint<71.0,>=70.0; extra == "all"
|
|
40
40
|
Requires-Dist: odfpy<2.0,>=1.4; extra == "all"
|
|
41
|
-
Requires-Dist: Pillow<
|
|
41
|
+
Requires-Dist: Pillow<14.0,>=10.0; extra == "all"
|
|
42
42
|
Requires-Dist: lxml<7.0,>=5.0; extra == "all"
|
|
43
43
|
Requires-Dist: firecrawl-anydoc<0.3.0,>=0.1.1; extra == "all"
|
|
44
44
|
Requires-Dist: liteparse<3.0.0,>=2.0.0; extra == "all"
|
|
@@ -119,7 +119,7 @@ Or install only the modules you need:
|
|
|
119
119
|
```bash
|
|
120
120
|
pip install tabella-docs[create] # python-docx, openpyxl, python-pptx, weasyprint, odfpy
|
|
121
121
|
pip install tabella-docs[read] # firecrawl-anydoc, liteparse, PyMuPDF
|
|
122
|
-
pip install tabella-docs[convert] # no python deps (needs system pandoc
|
|
122
|
+
pip install tabella-docs[convert] # no python deps (needs system pandoc; a TeX engine only for pdf output)
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
## Modules
|
|
@@ -189,8 +189,8 @@ Set `DOCS_VENV_PYTHON` to point to it:
|
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
191
|
# Create a dedicated venv
|
|
192
|
-
python -m venv ~/.venvs/docs
|
|
193
|
-
~/.venvs/docs/bin/pip install python-docx openpyxl python-pptx weasyprint odfpy Pillow lxml PyMuPDF firecrawl-anydoc liteparse
|
|
192
|
+
python -m venv ~/.venvs/docs/venv
|
|
193
|
+
~/.venvs/docs/venv/bin/pip install python-docx openpyxl python-pptx weasyprint odfpy Pillow lxml PyMuPDF firecrawl-anydoc liteparse
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
Or install the libraries into your main Python environment:
|
|
@@ -202,7 +202,7 @@ pip install tabella-docs[create]
|
|
|
202
202
|
### Read module
|
|
203
203
|
|
|
204
204
|
```bash
|
|
205
|
-
~/.venvs/docs/bin/pip install firecrawl-anydoc liteparse PyMuPDF
|
|
205
|
+
~/.venvs/docs/venv/bin/pip install firecrawl-anydoc liteparse PyMuPDF
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
Or:
|
|
@@ -240,6 +240,23 @@ sudo apt install libcairo2 libpango-1.0-0 libgdk-pixbuf-2.0-0
|
|
|
240
240
|
brew install cairo pango gdk-pixbuf
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
### Windows
|
|
244
|
+
|
|
245
|
+
tabella runs natively on Windows 10/11 (64-bit, Python 3.10+); all five MCP
|
|
246
|
+
tools are exercised on `windows-latest` in CI. Native dependencies, installed
|
|
247
|
+
per feature:
|
|
248
|
+
|
|
249
|
+
- **PDF creation (WeasyPrint)** — the
|
|
250
|
+
[GTK3 runtime installer](https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer)
|
|
251
|
+
(its default install location is on WeasyPrint's search path)
|
|
252
|
+
- **Convert** — [pandoc](https://pandoc.org/installing.html); a TeX engine
|
|
253
|
+
(e.g. MiKTeX) only when the *output* format is PDF
|
|
254
|
+
- **OCR fallback** — [Tesseract for Windows](https://github.com/UB-Mannheim/tesseract/wiki),
|
|
255
|
+
only for scanned PDFs and images
|
|
256
|
+
|
|
257
|
+
The first-run dependency venv bootstraps natively on Windows (stdlib venv
|
|
258
|
+
module — no bash, MSYS2, or WSL required).
|
|
259
|
+
|
|
243
260
|
## Installation from source
|
|
244
261
|
|
|
245
262
|
```bash
|
|
@@ -252,7 +269,7 @@ pip install -e ".[all]"
|
|
|
252
269
|
|
|
253
270
|
| Variable | Purpose | Default |
|
|
254
271
|
|----------|---------|---------|
|
|
255
|
-
| `DOCS_VENV_PYTHON` | Python binary for document libraries | `PLUGIN_DATA/venv/bin/python` or `~/.venvs/docs/bin/python` |
|
|
272
|
+
| `DOCS_VENV_PYTHON` | Python binary for document libraries | `PLUGIN_DATA/venv/bin/python` or `~/.venvs/docs/venv/bin/python` |
|
|
256
273
|
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
257
274
|
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
258
275
|
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
@@ -260,10 +277,11 @@ pip install -e ".[all]"
|
|
|
260
277
|
### Automatic bootstrap
|
|
261
278
|
|
|
262
279
|
On first startup, the MCP server checks if the docs venv exists. If not,
|
|
263
|
-
it
|
|
264
|
-
|
|
280
|
+
it creates one in `PLUGIN_DATA` (provided by the v1 client) or
|
|
281
|
+
`~/.venvs/docs` as fallback, and installs all dependencies:
|
|
265
282
|
python-docx, openpyxl, python-pptx, weasyprint, odfpy, PyMuPDF,
|
|
266
|
-
firecrawl-anydoc, liteparse.
|
|
283
|
+
firecrawl-anydoc, liteparse. On POSIX this runs `scripts/setup.sh`; on
|
|
284
|
+
Windows the venv is built with the standard library's `venv` module.
|
|
267
285
|
|
|
268
286
|
This means the v1 package is fully self-contained — no manual pip install
|
|
269
287
|
required. The bootstrap runs once; subsequent startups use the existing venv.
|
|
@@ -70,7 +70,7 @@ Or install only the modules you need:
|
|
|
70
70
|
```bash
|
|
71
71
|
pip install tabella-docs[create] # python-docx, openpyxl, python-pptx, weasyprint, odfpy
|
|
72
72
|
pip install tabella-docs[read] # firecrawl-anydoc, liteparse, PyMuPDF
|
|
73
|
-
pip install tabella-docs[convert] # no python deps (needs system pandoc
|
|
73
|
+
pip install tabella-docs[convert] # no python deps (needs system pandoc; a TeX engine only for pdf output)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
## Modules
|
|
@@ -140,8 +140,8 @@ Set `DOCS_VENV_PYTHON` to point to it:
|
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
# Create a dedicated venv
|
|
143
|
-
python -m venv ~/.venvs/docs
|
|
144
|
-
~/.venvs/docs/bin/pip install python-docx openpyxl python-pptx weasyprint odfpy Pillow lxml PyMuPDF firecrawl-anydoc liteparse
|
|
143
|
+
python -m venv ~/.venvs/docs/venv
|
|
144
|
+
~/.venvs/docs/venv/bin/pip install python-docx openpyxl python-pptx weasyprint odfpy Pillow lxml PyMuPDF firecrawl-anydoc liteparse
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
Or install the libraries into your main Python environment:
|
|
@@ -153,7 +153,7 @@ pip install tabella-docs[create]
|
|
|
153
153
|
### Read module
|
|
154
154
|
|
|
155
155
|
```bash
|
|
156
|
-
~/.venvs/docs/bin/pip install firecrawl-anydoc liteparse PyMuPDF
|
|
156
|
+
~/.venvs/docs/venv/bin/pip install firecrawl-anydoc liteparse PyMuPDF
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
Or:
|
|
@@ -191,6 +191,23 @@ sudo apt install libcairo2 libpango-1.0-0 libgdk-pixbuf-2.0-0
|
|
|
191
191
|
brew install cairo pango gdk-pixbuf
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
### Windows
|
|
195
|
+
|
|
196
|
+
tabella runs natively on Windows 10/11 (64-bit, Python 3.10+); all five MCP
|
|
197
|
+
tools are exercised on `windows-latest` in CI. Native dependencies, installed
|
|
198
|
+
per feature:
|
|
199
|
+
|
|
200
|
+
- **PDF creation (WeasyPrint)** — the
|
|
201
|
+
[GTK3 runtime installer](https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer)
|
|
202
|
+
(its default install location is on WeasyPrint's search path)
|
|
203
|
+
- **Convert** — [pandoc](https://pandoc.org/installing.html); a TeX engine
|
|
204
|
+
(e.g. MiKTeX) only when the *output* format is PDF
|
|
205
|
+
- **OCR fallback** — [Tesseract for Windows](https://github.com/UB-Mannheim/tesseract/wiki),
|
|
206
|
+
only for scanned PDFs and images
|
|
207
|
+
|
|
208
|
+
The first-run dependency venv bootstraps natively on Windows (stdlib venv
|
|
209
|
+
module — no bash, MSYS2, or WSL required).
|
|
210
|
+
|
|
194
211
|
## Installation from source
|
|
195
212
|
|
|
196
213
|
```bash
|
|
@@ -203,7 +220,7 @@ pip install -e ".[all]"
|
|
|
203
220
|
|
|
204
221
|
| Variable | Purpose | Default |
|
|
205
222
|
|----------|---------|---------|
|
|
206
|
-
| `DOCS_VENV_PYTHON` | Python binary for document libraries | `PLUGIN_DATA/venv/bin/python` or `~/.venvs/docs/bin/python` |
|
|
223
|
+
| `DOCS_VENV_PYTHON` | Python binary for document libraries | `PLUGIN_DATA/venv/bin/python` or `~/.venvs/docs/venv/bin/python` |
|
|
207
224
|
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
208
225
|
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
209
226
|
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
@@ -211,10 +228,11 @@ pip install -e ".[all]"
|
|
|
211
228
|
### Automatic bootstrap
|
|
212
229
|
|
|
213
230
|
On first startup, the MCP server checks if the docs venv exists. If not,
|
|
214
|
-
it
|
|
215
|
-
|
|
231
|
+
it creates one in `PLUGIN_DATA` (provided by the v1 client) or
|
|
232
|
+
`~/.venvs/docs` as fallback, and installs all dependencies:
|
|
216
233
|
python-docx, openpyxl, python-pptx, weasyprint, odfpy, PyMuPDF,
|
|
217
|
-
firecrawl-anydoc, liteparse.
|
|
234
|
+
firecrawl-anydoc, liteparse. On POSIX this runs `scripts/setup.sh`; on
|
|
235
|
+
Windows the venv is built with the standard library's `venv` module.
|
|
218
236
|
|
|
219
237
|
This means the v1 package is fully self-contained — no manual pip install
|
|
220
238
|
required. The bootstrap runs once; subsequent startups use the existing venv.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabella-docs"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "Portable document lifecycle suite — create (.docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp), convert (pandoc), and read (anydoc + LiteParse OCR) documents. Agent Plugins v1 compatible."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -31,9 +31,9 @@ create = [
|
|
|
31
31
|
"python-docx>=1.0,<2.0",
|
|
32
32
|
"openpyxl>=3.1,<4.0",
|
|
33
33
|
"python-pptx>=0.6,<2.0",
|
|
34
|
-
"weasyprint>=
|
|
34
|
+
"weasyprint>=70.0,<71.0",
|
|
35
35
|
"odfpy>=1.4,<2.0",
|
|
36
|
-
"Pillow>=10.0,<
|
|
36
|
+
"Pillow>=10.0,<14.0",
|
|
37
37
|
"lxml>=5.0,<7.0",
|
|
38
38
|
]
|
|
39
39
|
read = [
|
|
@@ -46,9 +46,9 @@ all = [
|
|
|
46
46
|
"python-docx>=1.0,<2.0",
|
|
47
47
|
"openpyxl>=3.1,<4.0",
|
|
48
48
|
"python-pptx>=0.6,<2.0",
|
|
49
|
-
"weasyprint>=
|
|
49
|
+
"weasyprint>=70.0,<71.0",
|
|
50
50
|
"odfpy>=1.4,<2.0",
|
|
51
|
-
"Pillow>=10.0,<
|
|
51
|
+
"Pillow>=10.0,<14.0",
|
|
52
52
|
"lxml>=5.0,<7.0",
|
|
53
53
|
"firecrawl-anydoc>=0.1.1,<0.3.0",
|
|
54
54
|
"liteparse>=2.0.0,<3.0.0",
|
|
@@ -11,7 +11,9 @@ from __future__ import annotations
|
|
|
11
11
|
import os
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
from .bootstrap import resolve_venv_python
|
|
15
|
+
|
|
16
|
+
__version__ = "2.0.3"
|
|
15
17
|
|
|
16
18
|
# ── Configuration ─────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -21,19 +23,11 @@ def get_docs_venv_python() -> str:
|
|
|
21
23
|
Resolution order:
|
|
22
24
|
1. DOCS_VENV_PYTHON env var (explicit override)
|
|
23
25
|
2. PLUGIN_DATA/venv/bin/python (v1 client-managed data directory)
|
|
24
|
-
3. ~/.venvs/docs/bin/python (fallback)
|
|
25
|
-
"""
|
|
26
|
-
explicit = os.environ.get("DOCS_VENV_PYTHON")
|
|
27
|
-
if explicit and Path(explicit).exists():
|
|
28
|
-
return explicit
|
|
26
|
+
3. ~/.venvs/docs/venv/bin/python (fallback)
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if p.exists():
|
|
34
|
-
return str(p)
|
|
35
|
-
|
|
36
|
-
return str(Path.home() / ".venvs" / "docs" / "bin" / "python")
|
|
28
|
+
Delegates to tabella.bootstrap so the path can only be wrong in one place.
|
|
29
|
+
"""
|
|
30
|
+
return resolve_venv_python()
|
|
37
31
|
|
|
38
32
|
|
|
39
33
|
def get_output_dir() -> Path:
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Shared docs-venv bootstrap for the stdio and HTTP servers.
|
|
2
|
+
|
|
3
|
+
`docs_root` is the directory that owns the venv; the venv itself always lives
|
|
4
|
+
at `<docs_root>/venv`, which is exactly what scripts/setup.sh builds. Keep
|
|
5
|
+
those two in step: the interpreter returned here is the one setup.sh created,
|
|
6
|
+
and `tests/test_bootstrap_paths.py` asserts that instead of hard-coding a path
|
|
7
|
+
that can drift again.
|
|
8
|
+
|
|
9
|
+
Checks DOCS_VENV_PYTHON first. Otherwise the root is PLUGIN_DATA (provided by
|
|
10
|
+
the v1 client) or ~/.venvs/docs.
|
|
11
|
+
|
|
12
|
+
Platform notes:
|
|
13
|
+
- POSIX runs scripts/setup.sh via bash.
|
|
14
|
+
- Windows has no bash on a stock box — the venv is built with the stdlib
|
|
15
|
+
venv module + the venv's own pip, mirroring setup.sh
|
|
16
|
+
(--require-hashes -r requirements-pinned.txt).
|
|
17
|
+
- The venv layout differs: bin/python vs Scripts/python.exe.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def docs_root() -> Path:
|
|
27
|
+
"""Directory that owns the docs venv (the argument setup.sh takes)."""
|
|
28
|
+
plugin_data = os.environ.get("PLUGIN_DATA")
|
|
29
|
+
if plugin_data:
|
|
30
|
+
return Path(plugin_data)
|
|
31
|
+
return Path.home() / ".venvs" / "docs"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def venv_python_path() -> str:
|
|
35
|
+
"""Interpreter inside the docs venv — `<docs_root>/venv`, as setup.sh builds it."""
|
|
36
|
+
venv_dir = docs_root() / "venv"
|
|
37
|
+
if os.name == "nt":
|
|
38
|
+
return str(venv_dir / "Scripts" / "python.exe")
|
|
39
|
+
return str(venv_dir / "bin" / "python")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_venv_python() -> str:
|
|
43
|
+
"""Interpreter to use, honouring an explicit DOCS_VENV_PYTHON override."""
|
|
44
|
+
explicit = os.environ.get("DOCS_VENV_PYTHON")
|
|
45
|
+
if explicit and Path(explicit).exists():
|
|
46
|
+
return explicit
|
|
47
|
+
return venv_python_path()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _venv_ready(venv_python: str) -> bool:
|
|
51
|
+
"""Is this a *usable* docs venv, not just a leftover interpreter?
|
|
52
|
+
|
|
53
|
+
Existence alone is not enough: an interrupted first-run install leaves
|
|
54
|
+
bin/python behind with none of the packages, and every later start then
|
|
55
|
+
accepts it and fails at import time. A sentinel marks a completed install;
|
|
56
|
+
a venv without one is probed once (cheap) and adopted only if the imports
|
|
57
|
+
work — existing venvs created before the sentinel existed are not
|
|
58
|
+
re-bootstrapped for nothing.
|
|
59
|
+
"""
|
|
60
|
+
if not Path(venv_python).exists():
|
|
61
|
+
return False
|
|
62
|
+
marker = Path(venv_python).parent.parent / ".tabella-deps-ok"
|
|
63
|
+
if marker.exists():
|
|
64
|
+
return True
|
|
65
|
+
# weasyprint is deliberately absent from this probe: importing it needs
|
|
66
|
+
# pango/GTK, which a host may legitimately lack (PDF then fails at call
|
|
67
|
+
# time with a clear error). Everything else is pure Python or bundled.
|
|
68
|
+
try:
|
|
69
|
+
probe = subprocess.run(
|
|
70
|
+
[venv_python, "-c", "import docx, openpyxl, pptx, odf, anydoc, liteparse, pymupdf"],
|
|
71
|
+
capture_output=True, text=True, timeout=120,
|
|
72
|
+
)
|
|
73
|
+
except (OSError, subprocess.SubprocessError):
|
|
74
|
+
return False # hung or unexecutable: treat as not ready, rebuild
|
|
75
|
+
if probe.returncode == 0:
|
|
76
|
+
try:
|
|
77
|
+
marker.write_text("")
|
|
78
|
+
except OSError:
|
|
79
|
+
pass # a read-only venv is still usable; the probe will run again
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def ensure_venv() -> str:
|
|
85
|
+
"""Return the docs-venv interpreter, bootstrapping it on first use."""
|
|
86
|
+
explicit = os.environ.get("DOCS_VENV_PYTHON")
|
|
87
|
+
if explicit and Path(explicit).exists():
|
|
88
|
+
return explicit # the operator named an interpreter: use it as-is
|
|
89
|
+
|
|
90
|
+
venv_python = venv_python_path()
|
|
91
|
+
if _venv_ready(venv_python):
|
|
92
|
+
return venv_python
|
|
93
|
+
|
|
94
|
+
root = docs_root()
|
|
95
|
+
root.mkdir(parents=True, exist_ok=True) # ~/.venvs may not exist on a cold host
|
|
96
|
+
venv_dir = root / "venv"
|
|
97
|
+
marker = venv_dir / ".tabella-deps-ok"
|
|
98
|
+
plugin_root = Path(__file__).parent.parent.resolve()
|
|
99
|
+
|
|
100
|
+
if os.name == "nt":
|
|
101
|
+
# No bash on a stock Windows box — build the venv with the stdlib
|
|
102
|
+
# venv module + the venv's own pip, mirroring scripts/setup.sh.
|
|
103
|
+
pinned = plugin_root / "requirements-pinned.txt"
|
|
104
|
+
sys.stderr.write("tabella: bootstrapping docs venv (first run)...\n")
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
[sys.executable, "-m", "venv", str(venv_dir)],
|
|
107
|
+
capture_output=True, text=True, timeout=600,
|
|
108
|
+
)
|
|
109
|
+
if result.returncode == 0:
|
|
110
|
+
subprocess.run(
|
|
111
|
+
[venv_python, "-m", "pip", "install", "--quiet", "--upgrade", "pip"],
|
|
112
|
+
capture_output=True, text=True, timeout=600,
|
|
113
|
+
)
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[venv_python, "-m", "pip", "install", "--quiet", "--require-hashes",
|
|
116
|
+
"-r", str(pinned)],
|
|
117
|
+
capture_output=True, text=True, timeout=600,
|
|
118
|
+
)
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
sys.stderr.write(f"tabella: bootstrap failed: {result.stderr[:500]}\n")
|
|
121
|
+
elif Path(venv_python).exists():
|
|
122
|
+
marker.write_text("")
|
|
123
|
+
sys.stderr.write("tabella: venv ready\n")
|
|
124
|
+
return venv_python
|
|
125
|
+
|
|
126
|
+
# POSIX bootstrap: run the setup script on the docs root — it creates
|
|
127
|
+
# <docs_root>/venv, which is what venv_python points at.
|
|
128
|
+
setup_script = plugin_root / "scripts" / "setup.sh"
|
|
129
|
+
if not setup_script.exists():
|
|
130
|
+
sys.stderr.write(f"tabella: setup script not found at {setup_script}\n")
|
|
131
|
+
return venv_python # let tool calls fail with a helpful error
|
|
132
|
+
|
|
133
|
+
sys.stderr.write("tabella: bootstrapping docs venv (first run)...\n")
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
["bash", str(setup_script), str(root)],
|
|
136
|
+
capture_output=True, text=True, timeout=300,
|
|
137
|
+
)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
sys.stderr.write(f"tabella: bootstrap failed: {result.stderr[:500]}\n")
|
|
140
|
+
elif Path(venv_python).exists():
|
|
141
|
+
marker.write_text("")
|
|
142
|
+
sys.stderr.write("tabella: venv ready\n")
|
|
143
|
+
|
|
144
|
+
return venv_python
|
|
@@ -5,6 +5,7 @@ Output directory is configurable via DOCS_OUTPUT_DIR (defaults to ~/Documents).
|
|
|
5
5
|
Merged from the doc_convert plugin.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import re
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
10
11
|
import shutil
|
|
@@ -17,11 +18,11 @@ from .utils import _get_output_dir, _is_in_output_dir, _resolve_output_dir
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def _check_deps() -> Optional[str]:
|
|
21
|
-
"""Return error string if pandoc or LaTeX is missing
|
|
21
|
+
def _check_deps(needs_tex: bool = False) -> Optional[str]:
|
|
22
|
+
"""Return error string if pandoc — or, for pdf targets, LaTeX — is missing."""
|
|
22
23
|
if not shutil.which("pandoc"):
|
|
23
24
|
return "pandoc is not installed. Install it (e.g. `sudo apt install pandoc`)."
|
|
24
|
-
if not shutil.which("pdflatex") and not shutil.which("xelatex"):
|
|
25
|
+
if needs_tex and not shutil.which("pdflatex") and not shutil.which("xelatex"):
|
|
25
26
|
return "No LaTeX engine found. Install texlive (e.g. `sudo apt install texlive-xetex`)."
|
|
26
27
|
return None
|
|
27
28
|
|
|
@@ -51,17 +52,15 @@ def doc_convert(
|
|
|
51
52
|
When omitted, pandoc infers from file extension.
|
|
52
53
|
output_path: Optional absolute path for the output file.
|
|
53
54
|
When omitted, writes to the user's documents directory.
|
|
54
|
-
extra_args: Optional extra pandoc CLI flags (e.g. ["--toc", "-
|
|
55
|
+
extra_args: Optional extra pandoc CLI flags (e.g. ["--toc", "-Vmargin=1in"]).
|
|
56
|
+
NOTE: -V must be a single token ("-Vkey=value") — the
|
|
57
|
+
two-token "-V key=value" form is rejected by the allowlist.
|
|
55
58
|
category: Optional subfolder under ~/documents/ (e.g. "reports", "invoices").
|
|
56
59
|
|
|
57
60
|
Returns:
|
|
58
61
|
{"success": bool, "output_path": str, "from_format": str, "to_format": str,
|
|
59
62
|
"pdf_engine": str | None, "message": str}
|
|
60
63
|
"""
|
|
61
|
-
dep_err = _check_deps()
|
|
62
|
-
if dep_err:
|
|
63
|
-
return {"success": False, "error": dep_err}
|
|
64
|
-
|
|
65
64
|
src = Path(input_path).expanduser().resolve()
|
|
66
65
|
if not src.exists():
|
|
67
66
|
return {"success": False, "error": f"Source file not found: {src}"}
|
|
@@ -69,6 +68,13 @@ def doc_convert(
|
|
|
69
68
|
# Determine formats
|
|
70
69
|
src_fmt = from_format or _infer_format(src.suffix.lstrip(".").lower())
|
|
71
70
|
tgt_fmt = to_format.lower()
|
|
71
|
+
_check_format(src_fmt, "from_format")
|
|
72
|
+
_check_format(tgt_fmt, "to_format")
|
|
73
|
+
|
|
74
|
+
# pandoc is always required; a TeX engine only for pdf output
|
|
75
|
+
dep_err = _check_deps(tgt_fmt == "pdf")
|
|
76
|
+
if dep_err:
|
|
77
|
+
return {"success": False, "error": dep_err}
|
|
72
78
|
|
|
73
79
|
# Determine output path
|
|
74
80
|
if output_path:
|
|
@@ -93,7 +99,20 @@ def doc_convert(
|
|
|
93
99
|
engine = _pdf_engine()
|
|
94
100
|
cmd.extend(["--pdf-engine", engine])
|
|
95
101
|
if extra_args:
|
|
96
|
-
|
|
102
|
+
# strict allowlist — pandoc can execute arbitrary code via --filter/
|
|
103
|
+
# --lua-filter/--pdf-engine, so only formatting flags are honored
|
|
104
|
+
# (catalog review #2026-10-16, finding 1).
|
|
105
|
+
allowed = {"--toc", "--number-sections"}
|
|
106
|
+
for a in extra_args:
|
|
107
|
+
if a in allowed:
|
|
108
|
+
cmd.append(a)
|
|
109
|
+
elif re.match(r"^-V\S*=", a):
|
|
110
|
+
cmd.append(a)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"unsupported pandoc argument {a!r}: extra_args allows only "
|
|
114
|
+
"--toc, --number-sections and -V <key>=<value>"
|
|
115
|
+
)
|
|
97
116
|
|
|
98
117
|
try:
|
|
99
118
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
@@ -138,6 +157,22 @@ def _infer_format(ext: str) -> str:
|
|
|
138
157
|
return mapping.get(ext, ext)
|
|
139
158
|
|
|
140
159
|
|
|
160
|
+
# Pandoc treats a format name ending in ".lua" as a path to a custom Lua
|
|
161
|
+
# reader/writer and executes it with the full Lua stdlib (os.execute) —
|
|
162
|
+
# same code-exec class as --lua-filter via a different argument. Restrict
|
|
163
|
+
# format names to pandoc's built-in name grammar (catalog finding 2026-10-17).
|
|
164
|
+
_FORMAT_RE = re.compile(r"^[a-z0-9_]+([+-][a-z0-9_]+)*$")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_format(name: str, which: str) -> None:
|
|
168
|
+
"""Reject anything that isn't a built-in pandoc format name."""
|
|
169
|
+
if not _FORMAT_RE.match(name):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"unsupported {which} {name!r}: must be a built-in pandoc format "
|
|
172
|
+
"(alphanumeric/underscore, optional +ext/-ext modifiers)"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
141
176
|
def _handle_doc_convert(args: dict, **kwargs) -> str:
|
|
142
177
|
"""Handler wrapper for doc_convert tool."""
|
|
143
178
|
result = doc_convert(**args)
|