tabella-docs 2.0.2__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.2/tabella_docs.egg-info → tabella_docs-2.0.3}/PKG-INFO +9 -9
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/README.md +4 -4
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/pyproject.toml +5 -5
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/__init__.py +7 -13
- tabella_docs-2.0.3/tabella/bootstrap.py +144 -0
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/convert.py +3 -1
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/create.py +120 -23
- {tabella_docs-2.0.2 → tabella_docs-2.0.3/tabella_docs.egg-info}/PKG-INFO +9 -9
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella_docs.egg-info/SOURCES.txt +3 -0
- {tabella_docs-2.0.2 → 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.2 → tabella_docs-2.0.3}/tests/test_text_section_alias.py +7 -2
- tabella_docs-2.0.2/tabella/bootstrap.py +0 -83
- tabella_docs-2.0.2/tests/test_ensure_venv_win.py +0 -125
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/LICENSE +0 -0
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/setup.cfg +0 -0
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/read.py +0 -0
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/utils.py +0 -0
- {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella_docs.egg-info/dependency_links.txt +0 -0
- {tabella_docs-2.0.2 → 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"
|
|
@@ -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:
|
|
@@ -269,7 +269,7 @@ pip install -e ".[all]"
|
|
|
269
269
|
|
|
270
270
|
| Variable | Purpose | Default |
|
|
271
271
|
|----------|---------|---------|
|
|
272
|
-
| `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` |
|
|
273
273
|
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
274
274
|
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
275
275
|
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
@@ -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:
|
|
@@ -220,7 +220,7 @@ pip install -e ".[all]"
|
|
|
220
220
|
|
|
221
221
|
| Variable | Purpose | Default |
|
|
222
222
|
|----------|---------|---------|
|
|
223
|
-
| `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` |
|
|
224
224
|
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
225
225
|
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
226
226
|
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
@@ -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
|
|
@@ -52,7 +52,9 @@ def doc_convert(
|
|
|
52
52
|
When omitted, pandoc infers from file extension.
|
|
53
53
|
output_path: Optional absolute path for the output file.
|
|
54
54
|
When omitted, writes to the user's documents directory.
|
|
55
|
-
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.
|
|
56
58
|
category: Optional subfolder under ~/documents/ (e.g. "reports", "invoices").
|
|
57
59
|
|
|
58
60
|
Returns:
|
|
@@ -18,29 +18,122 @@ from .utils import _get_output_dir, _is_in_output_dir, _resolve_output_dir
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
_DATA_MIME_ALLOW = frozenset({
|
|
22
|
+
"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml",
|
|
23
|
+
# fonts are rendered by WeasyPrint's own text stack, not by Pillow
|
|
24
|
+
"font/woff", "font/woff2", "font/ttf", "font/otf",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _data_mime_allowed(url: str) -> bool:
|
|
29
|
+
"""Only data: URIs of formats whose decoding we can vouch for."""
|
|
30
|
+
head = url[len("data:"):].split(",", 1)[0]
|
|
31
|
+
return head.split(";", 1)[0].strip().lower() in _DATA_MIME_ALLOW
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Pillow's EpsImagePlugin._accept takes either the PostScript text magic or the
|
|
35
|
+
# DOS EPS binary header (little-endian 0xC6D3D0C5, i.e. these bytes).
|
|
36
|
+
_EPS_MAGICS = (b"%!PS", b"\xc5\xd0\xd3\xc6")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _data_payload_head(url: str, n: int = 32) -> bytes:
|
|
40
|
+
"""First bytes of a data: URI payload, decoded the way the consumer decodes it.
|
|
41
|
+
|
|
42
|
+
WeasyPrint fetches data: URIs through urllib's DataHandler: it percent-
|
|
43
|
+
unquotes the payload first, then base64-decodes MIME-tolerantly (whitespace
|
|
44
|
+
ignored). A sniffer that slices the raw string decodes something else
|
|
45
|
+
entirely — a newline after character four or a percent-encoded alphabet is
|
|
46
|
+
enough to make it see b"" while urllib still delivers the real bytes.
|
|
47
|
+
"""
|
|
48
|
+
import base64
|
|
49
|
+
from urllib.parse import unquote_to_bytes
|
|
50
|
+
|
|
51
|
+
head, _, payload = url[len("data:"):].partition(",")
|
|
52
|
+
raw = unquote_to_bytes(payload)
|
|
53
|
+
if "base64" in head.lower():
|
|
54
|
+
compact = bytes(b for b in raw if b not in b" \t\r\n")
|
|
55
|
+
take = ((n + 2) // 3) * 4
|
|
56
|
+
chunk = compact[:take]
|
|
57
|
+
chunk += b"=" * (-len(chunk) % 4)
|
|
58
|
+
try:
|
|
59
|
+
return base64.b64decode(chunk)[:n]
|
|
60
|
+
except Exception:
|
|
61
|
+
return b""
|
|
62
|
+
return raw[:n]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_postscript(head: bytes) -> bool:
|
|
66
|
+
"""PostScript/EPS magic. Pillow dispatches on bytes, not on the declared MIME."""
|
|
67
|
+
stripped = head.lstrip()
|
|
68
|
+
return any(stripped.startswith(magic) for magic in _EPS_MAGICS)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _in_tmp_dir(url: str, tmp_root) -> bool:
|
|
72
|
+
"""Real path containment for WeasyPrint's own font temp files."""
|
|
73
|
+
from pathlib import Path as _P
|
|
74
|
+
from urllib.parse import unquote, urlparse
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
candidate = _P(unquote(urlparse(url).path)).resolve()
|
|
78
|
+
except Exception:
|
|
79
|
+
return False
|
|
80
|
+
try:
|
|
81
|
+
return candidate.is_relative_to(tmp_root)
|
|
82
|
+
except Exception:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
21
86
|
def _safe_url_fetcher(font_config):
|
|
22
87
|
"""URL fetcher for model-supplied HTML: refuse network + host filesystem.
|
|
23
88
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
89
|
+
Three gates, because a declared MIME type proves nothing about the bytes
|
|
90
|
+
behind it:
|
|
91
|
+
|
|
92
|
+
1. Only allowlisted `data:` types pass — anything else raises.
|
|
93
|
+
2. The payload's magic number is checked, so PostScript/EPS inside an
|
|
94
|
+
`image/png` wrapper is refused. That is the actual hazard: WeasyPrint
|
|
95
|
+
hands image bytes to a format-agnostic `PIL.Image.open`, and older
|
|
96
|
+
releases let EPS reach Ghostscript.
|
|
97
|
+
3. `file:` resolves only inside WeasyPrint's own font temp directory, by
|
|
98
|
+
resolved path containment rather than string prefix.
|
|
99
|
+
|
|
100
|
+
WeasyPrint 70 additionally disables the EPS handler outright; these gates
|
|
101
|
+
are what keep the property true if that ever changes.
|
|
27
102
|
"""
|
|
28
103
|
from pathlib import Path as _P
|
|
104
|
+
from weasyprint.urls import URLFetcher
|
|
105
|
+
|
|
106
|
+
tmp_dir = str(getattr(font_config, "tmp_directory", "") or "")
|
|
107
|
+
tmp_root = _P(tmp_dir).resolve() if tmp_dir else None
|
|
108
|
+
allowed_types = ", ".join(sorted(_DATA_MIME_ALLOW))
|
|
109
|
+
|
|
110
|
+
class Guarded(URLFetcher):
|
|
111
|
+
def fetch(self, url, *a, **kw):
|
|
112
|
+
if url.startswith("data:"):
|
|
113
|
+
if not _data_mime_allowed(url):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"blocked data: URI of a type that is not allowed "
|
|
116
|
+
f"({allowed_types}): {url[:60]!r}"
|
|
117
|
+
)
|
|
118
|
+
if _is_postscript(_data_payload_head(url)):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"blocked PostScript/EPS payload hidden in a data: URI "
|
|
121
|
+
f"({url[:60]!r})"
|
|
122
|
+
)
|
|
123
|
+
elif tmp_root is not None and url.startswith("file:"):
|
|
124
|
+
if not _in_tmp_dir(url, tmp_root):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"blocked file: URL outside the font temp directory: {url[:80]!r}"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"blocked external URL in HTML: {url[:80]!r} — network and "
|
|
131
|
+
"host-file access are disabled; use embedded data: URIs of "
|
|
132
|
+
f"an allowed type ({allowed_types})"
|
|
133
|
+
)
|
|
134
|
+
return super().fetch(url, *a, **kw)
|
|
29
135
|
|
|
30
|
-
|
|
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
|
|
136
|
+
return Guarded()
|
|
44
137
|
|
|
45
138
|
|
|
46
139
|
def _css_escape(s: str) -> str:
|
|
@@ -334,15 +427,19 @@ def _build_odt(content: dict, output_path: str, template: str = None, options: d
|
|
|
334
427
|
elif stype == "table":
|
|
335
428
|
rows = section.get("rows", [])
|
|
336
429
|
headers = section.get("headers", [])
|
|
337
|
-
|
|
338
|
-
|
|
430
|
+
# Width across headers and EVERY row — sizing from rows[0]
|
|
431
|
+
# alone silently drops later cells that are wider.
|
|
432
|
+
num_cols = max([1, len(headers)] + [len(r) for r in rows])
|
|
339
433
|
table = Table()
|
|
340
|
-
|
|
434
|
+
# Header row then body rows, each cell carrying its value —
|
|
435
|
+
# an empty grid with success:true is silent data loss.
|
|
436
|
+
body = ([headers] if headers else []) + list(rows)
|
|
437
|
+
for row in body:
|
|
341
438
|
tr = TableRow()
|
|
342
439
|
for c in range(num_cols):
|
|
440
|
+
cell = row[c] if c < len(row) else ""
|
|
343
441
|
tc = TableCell()
|
|
344
|
-
|
|
345
|
-
tc.addElement(p)
|
|
442
|
+
tc.addElement(P(text="" if cell is None else str(cell)))
|
|
346
443
|
tr.addElement(tc)
|
|
347
444
|
table.addElement(tr)
|
|
348
445
|
doc.text.addElement(table)
|
|
@@ -594,7 +691,7 @@ def create_document(format: str, content, output_path: str, template: str = None
|
|
|
594
691
|
def _edit_pdf_text(path: str, page: int, instruction: str, output_path: str = None) -> dict:
|
|
595
692
|
"""Edit PDF text via pymupdf search-and-replace (no LLM, provider-agnostic)."""
|
|
596
693
|
try:
|
|
597
|
-
import fitz #
|
|
694
|
+
import pymupdf as fitz # PyMuPDF >= 1.24 exposes the canonical name
|
|
598
695
|
except ImportError:
|
|
599
696
|
return {"success": False, "error": "pymupdf not installed."}
|
|
600
697
|
|
|
@@ -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"
|
|
@@ -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:
|
|
@@ -269,7 +269,7 @@ pip install -e ".[all]"
|
|
|
269
269
|
|
|
270
270
|
| Variable | Purpose | Default |
|
|
271
271
|
|----------|---------|---------|
|
|
272
|
-
| `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` |
|
|
273
273
|
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
274
274
|
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
275
275
|
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
@@ -12,5 +12,8 @@ tabella_docs.egg-info/SOURCES.txt
|
|
|
12
12
|
tabella_docs.egg-info/dependency_links.txt
|
|
13
13
|
tabella_docs.egg-info/requires.txt
|
|
14
14
|
tabella_docs.egg-info/top_level.txt
|
|
15
|
+
tests/test_bootstrap_paths.py
|
|
15
16
|
tests/test_ensure_venv_win.py
|
|
17
|
+
tests/test_odt_table.py
|
|
18
|
+
tests/test_security_gates.py
|
|
16
19
|
tests/test_text_section_alias.py
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
python-docx<2.0,>=1.0
|
|
4
4
|
openpyxl<4.0,>=3.1
|
|
5
5
|
python-pptx<2.0,>=0.6
|
|
6
|
-
weasyprint<
|
|
6
|
+
weasyprint<71.0,>=70.0
|
|
7
7
|
odfpy<2.0,>=1.4
|
|
8
|
-
Pillow<
|
|
8
|
+
Pillow<14.0,>=10.0
|
|
9
9
|
lxml<7.0,>=5.0
|
|
10
10
|
firecrawl-anydoc<0.3.0,>=0.1.1
|
|
11
11
|
liteparse<3.0.0,>=2.0.0
|
|
@@ -17,9 +17,9 @@ PyMuPDF<2.0,>=1.24
|
|
|
17
17
|
python-docx<2.0,>=1.0
|
|
18
18
|
openpyxl<4.0,>=3.1
|
|
19
19
|
python-pptx<2.0,>=0.6
|
|
20
|
-
weasyprint<
|
|
20
|
+
weasyprint<71.0,>=70.0
|
|
21
21
|
odfpy<2.0,>=1.4
|
|
22
|
-
Pillow<
|
|
22
|
+
Pillow<14.0,>=10.0
|
|
23
23
|
lxml<7.0,>=5.0
|
|
24
24
|
|
|
25
25
|
[dev]
|