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.
Files changed (23) hide show
  1. {tabella_docs-2.0.2/tabella_docs.egg-info → tabella_docs-2.0.3}/PKG-INFO +9 -9
  2. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/README.md +4 -4
  3. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/pyproject.toml +5 -5
  4. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/__init__.py +7 -13
  5. tabella_docs-2.0.3/tabella/bootstrap.py +144 -0
  6. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/convert.py +3 -1
  7. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/create.py +120 -23
  8. {tabella_docs-2.0.2 → tabella_docs-2.0.3/tabella_docs.egg-info}/PKG-INFO +9 -9
  9. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella_docs.egg-info/SOURCES.txt +3 -0
  10. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella_docs.egg-info/requires.txt +4 -4
  11. tabella_docs-2.0.3/tests/test_bootstrap_paths.py +153 -0
  12. tabella_docs-2.0.3/tests/test_ensure_venv_win.py +117 -0
  13. tabella_docs-2.0.3/tests/test_odt_table.py +95 -0
  14. tabella_docs-2.0.3/tests/test_security_gates.py +188 -0
  15. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tests/test_text_section_alias.py +7 -2
  16. tabella_docs-2.0.2/tabella/bootstrap.py +0 -83
  17. tabella_docs-2.0.2/tests/test_ensure_venv_win.py +0 -125
  18. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/LICENSE +0 -0
  19. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/setup.cfg +0 -0
  20. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/read.py +0 -0
  21. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella/utils.py +0 -0
  22. {tabella_docs-2.0.2 → tabella_docs-2.0.3}/tabella_docs.egg-info/dependency_links.txt +0 -0
  23. {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.2
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<70.0,>=60.0; extra == "create"
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<13.0,>=10.0; extra == "create"
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<70.0,>=60.0; extra == "all"
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<13.0,>=10.0; extra == "all"
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.2"
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>=60.0,<70.0",
34
+ "weasyprint>=70.0,<71.0",
35
35
  "odfpy>=1.4,<2.0",
36
- "Pillow>=10.0,<13.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>=60.0,<70.0",
49
+ "weasyprint>=70.0,<71.0",
50
50
  "odfpy>=1.4,<2.0",
51
- "Pillow>=10.0,<13.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
- __version__ = "1.0.0"
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
- plugin_data = os.environ.get("PLUGIN_DATA")
31
- if plugin_data:
32
- p = Path(plugin_data) / "venv" / "bin" / "python"
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", "-V", "geometry:margin=1in"]).
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
- 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).
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
- 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
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
- total_rows = len(rows) + (1 if headers else 0)
338
- num_cols = max(1, len(headers) or (len(rows[0]) if rows else 1))
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
- for r in range(total_rows):
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
- p = P(text="")
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 # pymupdf
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.2
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<70.0,>=60.0; extra == "create"
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<13.0,>=10.0; extra == "create"
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<70.0,>=60.0; extra == "all"
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<13.0,>=10.0; extra == "all"
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<70.0,>=60.0
6
+ weasyprint<71.0,>=70.0
7
7
  odfpy<2.0,>=1.4
8
- Pillow<13.0,>=10.0
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<70.0,>=60.0
20
+ weasyprint<71.0,>=70.0
21
21
  odfpy<2.0,>=1.4
22
- Pillow<13.0,>=10.0
22
+ Pillow<14.0,>=10.0
23
23
  lxml<7.0,>=5.0
24
24
 
25
25
  [dev]