tabella-docs 2.0.1__tar.gz → 2.0.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.
- {tabella_docs-2.0.1/tabella_docs.egg-info → tabella_docs-2.0.2}/PKG-INFO +23 -5
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/README.md +22 -4
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/pyproject.toml +1 -1
- tabella_docs-2.0.2/tabella/bootstrap.py +83 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella/convert.py +41 -8
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella/create.py +47 -6
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella/read.py +1 -1
- {tabella_docs-2.0.1 → tabella_docs-2.0.2/tabella_docs.egg-info}/PKG-INFO +23 -5
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella_docs.egg-info/SOURCES.txt +2 -0
- tabella_docs-2.0.2/tests/test_ensure_venv_win.py +125 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/LICENSE +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/setup.cfg +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella/__init__.py +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella/utils.py +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella_docs.egg-info/dependency_links.txt +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella_docs.egg-info/requires.txt +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tabella_docs.egg-info/top_level.txt +0 -0
- {tabella_docs-2.0.1 → tabella_docs-2.0.2}/tests/test_text_section_alias.py +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.2
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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.2"
|
|
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"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Shared docs-venv bootstrap for the stdio and HTTP servers.
|
|
2
|
+
|
|
3
|
+
Returns the path to the docs venv Python, bootstrapping if needed.
|
|
4
|
+
|
|
5
|
+
Checks DOCS_VENV_PYTHON first. If unset or the venv doesn't exist, one is
|
|
6
|
+
created in PLUGIN_DATA (provided by the v1 client) or ~/.venvs/docs.
|
|
7
|
+
|
|
8
|
+
Platform notes:
|
|
9
|
+
- POSIX runs scripts/setup.sh via bash.
|
|
10
|
+
- Windows has no bash on a stock box — the venv is built with the stdlib
|
|
11
|
+
venv module + the venv's own pip, mirroring setup.sh
|
|
12
|
+
(--require-hashes -r requirements-pinned.txt).
|
|
13
|
+
- The venv layout differs: bin/python vs Scripts/python.exe.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ensure_venv() -> str:
|
|
23
|
+
# If DOCS_VENV_PYTHON is set and exists, use it
|
|
24
|
+
venv_python = os.environ.get("DOCS_VENV_PYTHON")
|
|
25
|
+
if venv_python and Path(venv_python).exists():
|
|
26
|
+
return venv_python
|
|
27
|
+
|
|
28
|
+
# Determine where to create the venv
|
|
29
|
+
plugin_data = os.environ.get("PLUGIN_DATA")
|
|
30
|
+
if plugin_data:
|
|
31
|
+
venv_dir = Path(plugin_data) / "venv"
|
|
32
|
+
else:
|
|
33
|
+
venv_dir = Path.home() / ".venvs" / "docs"
|
|
34
|
+
|
|
35
|
+
if os.name == "nt":
|
|
36
|
+
venv_python = str(venv_dir / "Scripts" / "python.exe")
|
|
37
|
+
else:
|
|
38
|
+
venv_python = str(venv_dir / "bin" / "python")
|
|
39
|
+
|
|
40
|
+
if venv_dir.exists() and Path(venv_python).exists():
|
|
41
|
+
return venv_python
|
|
42
|
+
|
|
43
|
+
plugin_root = Path(__file__).parent.parent.resolve()
|
|
44
|
+
|
|
45
|
+
if os.name == "nt":
|
|
46
|
+
# No bash on a stock Windows box — build the venv with the stdlib
|
|
47
|
+
# venv module + the venv's own pip, mirroring scripts/setup.sh.
|
|
48
|
+
pinned = plugin_root / "requirements-pinned.txt"
|
|
49
|
+
sys.stderr.write("tabella: bootstrapping docs venv (first run)...\n")
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
[sys.executable, "-m", "venv", str(venv_dir)],
|
|
52
|
+
capture_output=True, text=True, timeout=600,
|
|
53
|
+
)
|
|
54
|
+
if result.returncode == 0:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[venv_python, "-m", "pip", "install", "--quiet", "--require-hashes",
|
|
57
|
+
"-r", str(pinned)],
|
|
58
|
+
capture_output=True, text=True, timeout=600,
|
|
59
|
+
)
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
sys.stderr.write(f"tabella: bootstrap failed: {result.stderr[:500]}\n")
|
|
62
|
+
else:
|
|
63
|
+
sys.stderr.write("tabella: venv ready\n")
|
|
64
|
+
return venv_python
|
|
65
|
+
|
|
66
|
+
# POSIX bootstrap: run the setup script
|
|
67
|
+
setup_script = plugin_root / "scripts" / "setup.sh"
|
|
68
|
+
if not setup_script.exists():
|
|
69
|
+
sys.stderr.write(f"tabella: setup script not found at {setup_script}\n")
|
|
70
|
+
return venv_python # let tool calls fail with a helpful error
|
|
71
|
+
|
|
72
|
+
sys.stderr.write("tabella: bootstrapping docs venv (first run)...\n")
|
|
73
|
+
data_dir = str(venv_dir.parent) if plugin_data else str(venv_dir)
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["bash", str(setup_script), data_dir],
|
|
76
|
+
capture_output=True, text=True, timeout=300,
|
|
77
|
+
)
|
|
78
|
+
if result.returncode != 0:
|
|
79
|
+
sys.stderr.write(f"tabella: bootstrap failed: {result.stderr[:500]}\n")
|
|
80
|
+
else:
|
|
81
|
+
sys.stderr.write("tabella: venv ready\n")
|
|
82
|
+
|
|
83
|
+
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
|
|
|
@@ -58,10 +59,6 @@ def doc_convert(
|
|
|
58
59
|
{"success": bool, "output_path": str, "from_format": str, "to_format": str,
|
|
59
60
|
"pdf_engine": str | None, "message": str}
|
|
60
61
|
"""
|
|
61
|
-
dep_err = _check_deps()
|
|
62
|
-
if dep_err:
|
|
63
|
-
return {"success": False, "error": dep_err}
|
|
64
|
-
|
|
65
62
|
src = Path(input_path).expanduser().resolve()
|
|
66
63
|
if not src.exists():
|
|
67
64
|
return {"success": False, "error": f"Source file not found: {src}"}
|
|
@@ -69,6 +66,13 @@ def doc_convert(
|
|
|
69
66
|
# Determine formats
|
|
70
67
|
src_fmt = from_format or _infer_format(src.suffix.lstrip(".").lower())
|
|
71
68
|
tgt_fmt = to_format.lower()
|
|
69
|
+
_check_format(src_fmt, "from_format")
|
|
70
|
+
_check_format(tgt_fmt, "to_format")
|
|
71
|
+
|
|
72
|
+
# pandoc is always required; a TeX engine only for pdf output
|
|
73
|
+
dep_err = _check_deps(tgt_fmt == "pdf")
|
|
74
|
+
if dep_err:
|
|
75
|
+
return {"success": False, "error": dep_err}
|
|
72
76
|
|
|
73
77
|
# Determine output path
|
|
74
78
|
if output_path:
|
|
@@ -93,7 +97,20 @@ def doc_convert(
|
|
|
93
97
|
engine = _pdf_engine()
|
|
94
98
|
cmd.extend(["--pdf-engine", engine])
|
|
95
99
|
if extra_args:
|
|
96
|
-
|
|
100
|
+
# strict allowlist — pandoc can execute arbitrary code via --filter/
|
|
101
|
+
# --lua-filter/--pdf-engine, so only formatting flags are honored
|
|
102
|
+
# (catalog review #2026-10-16, finding 1).
|
|
103
|
+
allowed = {"--toc", "--number-sections"}
|
|
104
|
+
for a in extra_args:
|
|
105
|
+
if a in allowed:
|
|
106
|
+
cmd.append(a)
|
|
107
|
+
elif re.match(r"^-V\S*=", a):
|
|
108
|
+
cmd.append(a)
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"unsupported pandoc argument {a!r}: extra_args allows only "
|
|
112
|
+
"--toc, --number-sections and -V <key>=<value>"
|
|
113
|
+
)
|
|
97
114
|
|
|
98
115
|
try:
|
|
99
116
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
@@ -138,6 +155,22 @@ def _infer_format(ext: str) -> str:
|
|
|
138
155
|
return mapping.get(ext, ext)
|
|
139
156
|
|
|
140
157
|
|
|
158
|
+
# Pandoc treats a format name ending in ".lua" as a path to a custom Lua
|
|
159
|
+
# reader/writer and executes it with the full Lua stdlib (os.execute) —
|
|
160
|
+
# same code-exec class as --lua-filter via a different argument. Restrict
|
|
161
|
+
# format names to pandoc's built-in name grammar (catalog finding 2026-10-17).
|
|
162
|
+
_FORMAT_RE = re.compile(r"^[a-z0-9_]+([+-][a-z0-9_]+)*$")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _check_format(name: str, which: str) -> None:
|
|
166
|
+
"""Reject anything that isn't a built-in pandoc format name."""
|
|
167
|
+
if not _FORMAT_RE.match(name):
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"unsupported {which} {name!r}: must be a built-in pandoc format "
|
|
170
|
+
"(alphanumeric/underscore, optional +ext/-ext modifiers)"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
141
174
|
def _handle_doc_convert(args: dict, **kwargs) -> str:
|
|
142
175
|
"""Handler wrapper for doc_convert tool."""
|
|
143
176
|
result = doc_convert(**args)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Merged from the doc_create plugin. Dependencies (python-docx, openpyxl,
|
|
4
4
|
python-pptx, weasyprint, odfpy, nano-pdf) are now installed directly in the
|
|
5
|
-
|
|
5
|
+
the bootstrapped per-plugin venv (PYTHON_BOOTSTRAP: scripts/setup.sh; stdio/HTTP servers exec into it on first serve)
|
|
6
6
|
Builder functions import and use the libraries directly.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -18,8 +18,44 @@ from .utils import _get_output_dir, _is_in_output_dir, _resolve_output_dir
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _safe_url_fetcher(font_config):
|
|
22
|
+
"""URL fetcher for model-supplied HTML: refuse network + host filesystem.
|
|
23
|
+
|
|
24
|
+
Only data: URIs and WeasyPrint's own font temp-file references resolve
|
|
25
|
+
— <img src>, @import and file:// against anything else raise (catalog
|
|
26
|
+
review finding 2: 'no network' must be true, not a wording promise).
|
|
27
|
+
"""
|
|
28
|
+
from pathlib import Path as _P
|
|
29
|
+
|
|
30
|
+
def fetch(url, *a, **kw):
|
|
31
|
+
tmp_dir = str(getattr(font_config, "tmp_directory", "") or "")
|
|
32
|
+
if url.startswith("data:") or (
|
|
33
|
+
url.startswith("file:") and tmp_dir
|
|
34
|
+
and url[len("file://"):].startswith(_P(tmp_dir).resolve().as_posix())
|
|
35
|
+
):
|
|
36
|
+
from weasyprint import default_url_fetcher as _default
|
|
37
|
+
return _default(url, *a, **kw)
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"blocked external URL in HTML: {url[:80]!r} — network and "
|
|
40
|
+
"host-file access are disabled; use embedded data: URIs"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return fetch
|
|
44
|
+
|
|
45
|
+
|
|
21
46
|
def _css_escape(s: str) -> str:
|
|
22
|
-
|
|
47
|
+
# Escape string delimiters AND CSS-significant chars. CSS strings don't
|
|
48
|
+
# understand \n/\r as literal chars — convert them to the \A control
|
|
49
|
+
# sequence so an injected newline can't break out of a `content: '...'`
|
|
50
|
+
# declaration and smuggle in @font-face urls or import statements.
|
|
51
|
+
return (
|
|
52
|
+
s.replace("\\", "\\\\")
|
|
53
|
+
.replace("'", "\\'")
|
|
54
|
+
.replace("\n", "\\A ")
|
|
55
|
+
.replace("\r", "\\A ")
|
|
56
|
+
.replace("{", "\\{")
|
|
57
|
+
.replace("}", "\\}")
|
|
58
|
+
)
|
|
23
59
|
|
|
24
60
|
|
|
25
61
|
# ── builder functions (direct, no subprocess) ──
|
|
@@ -265,7 +301,8 @@ def _build_pdf(content: dict, output_path: str, template: str = None, options: d
|
|
|
265
301
|
"""
|
|
266
302
|
|
|
267
303
|
font_config = FontConfiguration()
|
|
268
|
-
|
|
304
|
+
fetcher = _safe_url_fetcher(font_config)
|
|
305
|
+
HTML(string=html_str, url_fetcher=fetcher).write_pdf(output_path, stylesheets=[CSS(string=css_str, url_fetcher=fetcher, font_config=font_config)])
|
|
269
306
|
return {"success": True, "path": output_path, "format": "pdf"}
|
|
270
307
|
except Exception as e:
|
|
271
308
|
return {"success": False, "error": str(e)}
|
|
@@ -483,16 +520,20 @@ def _build_pdf_from_html(content: dict, output_path: str, template: str = None,
|
|
|
483
520
|
css_lines.append("td, th { border: 1px solid #ccc; padding: 6px; text-align: left; }")
|
|
484
521
|
|
|
485
522
|
if header_text:
|
|
486
|
-
css_lines.append("@page { @top-center { content: '" + header_text
|
|
523
|
+
css_lines.append("@page { @top-center { content: '" + _css_escape(header_text) + "'; font-size: 9pt; color: #666; } }")
|
|
487
524
|
if footer_text or page_numbers:
|
|
488
525
|
footer_content = footer_text or ""
|
|
489
526
|
if page_numbers:
|
|
490
527
|
footer_content += " Page " + ("" if not footer_text else " ") + "counter(page)"
|
|
491
|
-
css_lines.append("@page { @bottom-center { content: '" + footer_content
|
|
528
|
+
css_lines.append("@page { @bottom-center { content: '" + _css_escape(footer_content) + "'; font-size: 9pt; color: #666; } }")
|
|
492
529
|
|
|
493
530
|
css_str = "\n".join(css_lines)
|
|
494
531
|
font_config = FontConfiguration()
|
|
495
|
-
|
|
532
|
+
fetcher = _safe_url_fetcher(font_config)
|
|
533
|
+
HTML(string=html_str, url_fetcher=fetcher).write_pdf(
|
|
534
|
+
output_path,
|
|
535
|
+
stylesheets=[CSS(string=css_str, url_fetcher=fetcher, font_config=font_config)],
|
|
536
|
+
)
|
|
496
537
|
return {"success": True, "path": output_path, "format": "pdf"}
|
|
497
538
|
except Exception as e:
|
|
498
539
|
return {"success": False, "error": str(e)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""documents.read — read documents via anydoc (primary) + LiteParse (OCR fallback).
|
|
2
2
|
|
|
3
3
|
Merged from the doc_read plugin. Dependencies (firecrawl-anydoc, liteparse) are
|
|
4
|
-
now installed directly in the
|
|
4
|
+
now installed directly in the the bootstrapped per-plugin venv (PYTHON_BOOTSTRAP: scripts/setup.sh; stdio/HTTP servers exec into it on first serve)
|
|
5
5
|
|
|
6
6
|
Architecture:
|
|
7
7
|
- Office formats (DOCX, DOC, XLSX, XLSB, PPTX, PPT, ODT, ODS, ODP, RTF,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabella-docs
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
tabella/__init__.py
|
|
5
|
+
tabella/bootstrap.py
|
|
5
6
|
tabella/convert.py
|
|
6
7
|
tabella/create.py
|
|
7
8
|
tabella/read.py
|
|
@@ -11,4 +12,5 @@ tabella_docs.egg-info/SOURCES.txt
|
|
|
11
12
|
tabella_docs.egg-info/dependency_links.txt
|
|
12
13
|
tabella_docs.egg-info/requires.txt
|
|
13
14
|
tabella_docs.egg-info/top_level.txt
|
|
15
|
+
tests/test_ensure_venv_win.py
|
|
14
16
|
tests/test_text_section_alias.py
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
r"""Seam test for tabella.bootstrap.ensure_venv platform handling (direct-run).
|
|
2
|
+
|
|
3
|
+
Shared module serving BOTH servers (mcp_server.py + scripts/mcp_http_server.py).
|
|
4
|
+
Proves, without a Windows box:
|
|
5
|
+
1. DOCS_VENV_PYTHON override short-circuits.
|
|
6
|
+
2. POSIX fast path unchanged (bin/python).
|
|
7
|
+
3. os.name=="nt": Scripts\python.exe layout, venv+pip bootstrap commands
|
|
8
|
+
mirror setup.sh (--require-hashes -r requirements-pinned.txt), success
|
|
9
|
+
and failure paths both behave.
|
|
10
|
+
|
|
11
|
+
The nt seam swaps mcp_server.os for a namespace (NOT global os.name — that
|
|
12
|
+
flips the shared pathlib factory to WindowsPath, uninstantiable on Linux).
|
|
13
|
+
Path separators in expectations are built with the same Path arithmetic, so
|
|
14
|
+
they match this platform; the real backslash layout is proven by CI on
|
|
15
|
+
windows-latest.
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
25
|
+
from tabella import bootstrap as B # noqa: E402
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check(name, cond, detail=""):
|
|
29
|
+
if not cond:
|
|
30
|
+
print(f"FAIL {name}: {detail}")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
print(f"PASS {name}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# 1. override short-circuits
|
|
36
|
+
with tempfile.TemporaryDirectory() as td:
|
|
37
|
+
fake = Path(td) / "wpython.exe"
|
|
38
|
+
fake.write_text("")
|
|
39
|
+
os.environ["DOCS_VENV_PYTHON"] = str(fake)
|
|
40
|
+
check("override", B.ensure_venv() == str(fake))
|
|
41
|
+
del os.environ["DOCS_VENV_PYTHON"]
|
|
42
|
+
|
|
43
|
+
# 2. POSIX fast path: existing venv dir with bin/python returns that path
|
|
44
|
+
with tempfile.TemporaryDirectory() as td:
|
|
45
|
+
os.environ["PLUGIN_DATA"] = str(Path(td) / "data")
|
|
46
|
+
venv = Path(td) / "data" / "venv"
|
|
47
|
+
(venv / "bin").mkdir(parents=True)
|
|
48
|
+
(venv / "bin" / "python").write_text("")
|
|
49
|
+
check("posix fast path", B.ensure_venv() == str(venv / "bin" / "python"))
|
|
50
|
+
del os.environ["PLUGIN_DATA"]
|
|
51
|
+
|
|
52
|
+
# 2b. POSIX bootstrap seam: bash + setup.sh with the data_dir arg
|
|
53
|
+
with tempfile.TemporaryDirectory() as td:
|
|
54
|
+
os.environ["PLUGIN_DATA"] = str(Path(td) / "data")
|
|
55
|
+
calls = []
|
|
56
|
+
real_run = subprocess.run
|
|
57
|
+
|
|
58
|
+
def fake_run_ok(cmd, **kw):
|
|
59
|
+
calls.append(cmd)
|
|
60
|
+
return R0(0)
|
|
61
|
+
|
|
62
|
+
class R0:
|
|
63
|
+
def __init__(self, returncode=0, stderr=""):
|
|
64
|
+
self.returncode, self.stderr = returncode, stderr
|
|
65
|
+
|
|
66
|
+
subprocess.run = fake_run_ok
|
|
67
|
+
try:
|
|
68
|
+
out = B.ensure_venv()
|
|
69
|
+
finally:
|
|
70
|
+
subprocess.run = real_run
|
|
71
|
+
want = str(Path(td) / "data" / "venv" / "bin" / "python")
|
|
72
|
+
check("posix bash cmd", calls and calls[0][0] == "bash"
|
|
73
|
+
and calls[0][1].endswith("scripts/setup.sh")
|
|
74
|
+
and calls[0][2] == str(Path(td) / "data"), f"got {calls}")
|
|
75
|
+
check("posix returns venv python", out == want, f"got {out}")
|
|
76
|
+
del os.environ["PLUGIN_DATA"]
|
|
77
|
+
|
|
78
|
+
# 3. Windows seam
|
|
79
|
+
with tempfile.TemporaryDirectory() as td:
|
|
80
|
+
os.environ["PLUGIN_DATA"] = str(Path(td) / "data")
|
|
81
|
+
venv_dir = Path(td) / "data" / "venv"
|
|
82
|
+
calls = []
|
|
83
|
+
real_run = subprocess.run
|
|
84
|
+
real_os = B.os
|
|
85
|
+
|
|
86
|
+
class R2:
|
|
87
|
+
def __init__(self, returncode=0, stderr=""):
|
|
88
|
+
self.returncode, self.stderr = returncode, stderr
|
|
89
|
+
|
|
90
|
+
def fake_run(cmd, **kw):
|
|
91
|
+
calls.append(cmd)
|
|
92
|
+
return R2(0) if len(calls) == 1 else R2(1, "boom") # first ok, second fails
|
|
93
|
+
|
|
94
|
+
B.os = SimpleNamespace(name="nt", environ=os.environ)
|
|
95
|
+
subprocess.run = fake_run
|
|
96
|
+
try:
|
|
97
|
+
out = B.ensure_venv()
|
|
98
|
+
finally:
|
|
99
|
+
B.os, subprocess.run = real_os, real_run
|
|
100
|
+
|
|
101
|
+
want_python = str(venv_dir / "Scripts" / "python.exe")
|
|
102
|
+
check("nt layout", out == want_python, f"got {out}")
|
|
103
|
+
check("nt venv cmd", calls and calls[0] == [sys.executable, "-m", "venv", str(venv_dir)],
|
|
104
|
+
f"got {calls}")
|
|
105
|
+
check("nt pip cmd", len(calls) == 2 and calls[1][:3] == [want_python, "-m", "pip"]
|
|
106
|
+
and "--require-hashes" in calls[1] and "requirements-pinned.txt" in calls[1][-1],
|
|
107
|
+
f"got {calls}")
|
|
108
|
+
|
|
109
|
+
# failure at venv-creation stage: single call, still returns the path
|
|
110
|
+
calls.clear()
|
|
111
|
+
|
|
112
|
+
def fake_run2(cmd, **kw):
|
|
113
|
+
calls.append(cmd)
|
|
114
|
+
return R2(1, "no venv for you")
|
|
115
|
+
|
|
116
|
+
B.os = SimpleNamespace(name="nt", environ=os.environ)
|
|
117
|
+
subprocess.run = fake_run2
|
|
118
|
+
try:
|
|
119
|
+
out = B.ensure_venv()
|
|
120
|
+
finally:
|
|
121
|
+
B.os, subprocess.run = real_os, real_run
|
|
122
|
+
check("nt create-fail graceful", out == want_python and len(calls) == 1)
|
|
123
|
+
del os.environ["PLUGIN_DATA"]
|
|
124
|
+
|
|
125
|
+
print("ensure_venv seam: ALL PASS")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|