tabella-docs 2.0.1__py3-none-any.whl
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/__init__.py +53 -0
- tabella/convert.py +187 -0
- tabella/create.py +777 -0
- tabella/read.py +248 -0
- tabella/utils.py +43 -0
- tabella_docs-2.0.1.dist-info/METADATA +315 -0
- tabella_docs-2.0.1.dist-info/RECORD +10 -0
- tabella_docs-2.0.1.dist-info/WHEEL +5 -0
- tabella_docs-2.0.1.dist-info/licenses/LICENSE +21 -0
- tabella_docs-2.0.1.dist-info/top_level.txt +1 -0
tabella/read.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""documents.read — read documents via anydoc (primary) + LiteParse (OCR fallback).
|
|
2
|
+
|
|
3
|
+
Merged from the doc_read plugin. Dependencies (firecrawl-anydoc, liteparse) are
|
|
4
|
+
now installed directly in the main Hermes venv — no subprocess/venv indirection.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
- Office formats (DOCX, DOC, XLSX, XLSB, PPTX, PPT, ODT, ODS, ODP, RTF,
|
|
8
|
+
EPUB, CSV) and text-based PDFs → anydoc (Rust, in-process, ~4ms,
|
|
9
|
+
GitHub-Flavored Markdown output).
|
|
10
|
+
- Scanned PDFs and images (JPG, PNG, TIFF) → LiteParse (Tesseract OCR).
|
|
11
|
+
Used when anydoc returns empty for a PDF, or for image files directly.
|
|
12
|
+
- Plain text (.txt, .md, .json, .py, .yaml, etc.) → direct file read.
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
# ── Format classification ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
TEXT_SUFFIXES = {
|
|
21
|
+
".txt", ".md", ".markdown", ".json", ".html", ".htm",
|
|
22
|
+
".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
|
|
23
|
+
".rst", ".fountain", ".log", ".py", ".js", ".ts", ".css",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Formats anydoc handles (office + text PDF).
|
|
27
|
+
# Source: anydoc README — supported formats table.
|
|
28
|
+
ANYDOC_SUFFIXES = {
|
|
29
|
+
".doc", ".docx", ".docm",
|
|
30
|
+
".ppt", ".pps", ".pot", ".pptx", ".pptm", ".ppsx", ".ppsm",
|
|
31
|
+
".xls", ".xlsx", ".xlsm", ".xlsb",
|
|
32
|
+
".odt", ".ods", ".odp",
|
|
33
|
+
".rtf",
|
|
34
|
+
".epub",
|
|
35
|
+
".pdf",
|
|
36
|
+
".csv",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Image formats that need OCR (LiteParse + Tesseract).
|
|
40
|
+
IMAGE_SUFFIXES = {
|
|
41
|
+
".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".bmp", ".gif",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── anydoc (primary engine) ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def _run_anydoc(path: str, timeout: int = 60) -> str:
|
|
47
|
+
"""Convert a document to Markdown using anydoc.
|
|
48
|
+
|
|
49
|
+
Returns the Markdown text, or empty string if the document has no
|
|
50
|
+
extractable text (e.g. scanned PDF without OCR).
|
|
51
|
+
"""
|
|
52
|
+
import anydoc
|
|
53
|
+
md = anydoc.to_markdown(path)
|
|
54
|
+
return md or ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── LiteParse (OCR fallback) ─────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def _run_liteparse(path: str, extract_images: bool = True, timeout: int = 120) -> Dict[str, Any]:
|
|
60
|
+
"""Parse a document using LiteParse with OCR enabled.
|
|
61
|
+
|
|
62
|
+
Used for scanned PDFs and images where anydoc returns empty.
|
|
63
|
+
"""
|
|
64
|
+
from liteparse import LiteParse, ParseResult
|
|
65
|
+
|
|
66
|
+
parser = LiteParse(ocr_enabled=extract_images, output_format="text")
|
|
67
|
+
result = parser.parse(path)
|
|
68
|
+
text = ""
|
|
69
|
+
if isinstance(result, ParseResult):
|
|
70
|
+
text = getattr(result, "text", None) or getattr(result, "markdown", None) or ""
|
|
71
|
+
elif isinstance(result, str):
|
|
72
|
+
text = result
|
|
73
|
+
else:
|
|
74
|
+
text = str(result)
|
|
75
|
+
return {
|
|
76
|
+
"text": text,
|
|
77
|
+
"metadata": getattr(result, "metadata", {}) if hasattr(result, "metadata") else {},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Plain text fast path ─────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def _read_text(path: str, max_chars: Optional[int] = None) -> str:
|
|
84
|
+
encodings = ["utf-8", "utf-8-sig", "latin-1"]
|
|
85
|
+
last_err = None
|
|
86
|
+
for enc in encodings:
|
|
87
|
+
try:
|
|
88
|
+
with open(path, "r", encoding=enc, errors="replace") as f:
|
|
89
|
+
text = f.read()
|
|
90
|
+
if max_chars:
|
|
91
|
+
text = text[:max_chars]
|
|
92
|
+
return text
|
|
93
|
+
except Exception as e:
|
|
94
|
+
last_err = e
|
|
95
|
+
raise RuntimeError(f"Could not read text file: {last_err}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Main dispatch ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def read(path: str, max_chars: Optional[int] = None, extract_images: bool = True, max_pages: Optional[int] = None) -> str:
|
|
101
|
+
"""Extract text from a document.
|
|
102
|
+
|
|
103
|
+
Dispatch:
|
|
104
|
+
1. Plain text → direct file read (fast path)
|
|
105
|
+
2. Office formats + text PDFs → anydoc (Markdown output)
|
|
106
|
+
3. Images → LiteParse (OCR)
|
|
107
|
+
4. PDFs that anydoc returns empty → LiteParse (OCR fallback for scanned PDFs)
|
|
108
|
+
"""
|
|
109
|
+
suffix = Path(path).suffix.lower()
|
|
110
|
+
|
|
111
|
+
# 1. Plain text fast path
|
|
112
|
+
if suffix in TEXT_SUFFIXES:
|
|
113
|
+
return _read_text(path, max_chars)
|
|
114
|
+
|
|
115
|
+
# 2. Images → LiteParse OCR directly
|
|
116
|
+
if suffix in IMAGE_SUFFIXES:
|
|
117
|
+
data = _run_liteparse(path, extract_images=extract_images)
|
|
118
|
+
text = data.get("text") or ""
|
|
119
|
+
if max_chars:
|
|
120
|
+
text = text[:max_chars]
|
|
121
|
+
return text
|
|
122
|
+
|
|
123
|
+
# 3. Office formats + PDFs → anydoc first
|
|
124
|
+
if suffix in ANYDOC_SUFFIXES or suffix not in TEXT_SUFFIXES:
|
|
125
|
+
text = ""
|
|
126
|
+
anydoc_error = None
|
|
127
|
+
try:
|
|
128
|
+
text = _run_anydoc(path)
|
|
129
|
+
except (RuntimeError, ImportError) as e:
|
|
130
|
+
anydoc_error = str(e)
|
|
131
|
+
|
|
132
|
+
# 4. If anydoc returned empty text (scanned PDF) or failed, try LiteParse OCR
|
|
133
|
+
if not text.strip():
|
|
134
|
+
try:
|
|
135
|
+
data = _run_liteparse(path, extract_images=extract_images)
|
|
136
|
+
text = data.get("text") or ""
|
|
137
|
+
except (RuntimeError, ImportError) as liteparse_err:
|
|
138
|
+
if anydoc_error:
|
|
139
|
+
raise RuntimeError(f"anydoc failed: {anydoc_error}; liteparse fallback also failed: {liteparse_err}")
|
|
140
|
+
raise RuntimeError(f"both anydoc and liteparse returned no text. liteparse: {liteparse_err}")
|
|
141
|
+
|
|
142
|
+
if max_chars:
|
|
143
|
+
text = text[:max_chars]
|
|
144
|
+
return text
|
|
145
|
+
|
|
146
|
+
# Fallback: unknown format, try plain text read
|
|
147
|
+
return _read_text(path, max_chars)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def read_meta(path: str) -> Dict[str, Any]:
|
|
151
|
+
"""Extract metadata from a document."""
|
|
152
|
+
suffix = Path(path).suffix.lower()
|
|
153
|
+
if suffix in TEXT_SUFFIXES:
|
|
154
|
+
return {"path": path, "format": "text"}
|
|
155
|
+
|
|
156
|
+
# LiteParse has the richest metadata extraction (office + PDF + images).
|
|
157
|
+
try:
|
|
158
|
+
data = _run_liteparse(path, extract_images=False)
|
|
159
|
+
meta = data.get("metadata") or {}
|
|
160
|
+
meta["path"] = path
|
|
161
|
+
return meta
|
|
162
|
+
except Exception:
|
|
163
|
+
# LiteParse unavailable (e.g. its LibreOffice fallback missing for office
|
|
164
|
+
# formats). Fall back to filesystem-truth metadata rather than failing:
|
|
165
|
+
# callers get valid basics instead of a tool error.
|
|
166
|
+
import os
|
|
167
|
+
|
|
168
|
+
st = os.stat(path)
|
|
169
|
+
return {
|
|
170
|
+
"path": path,
|
|
171
|
+
"format": suffix.lstrip("."),
|
|
172
|
+
"size_bytes": st.st_size,
|
|
173
|
+
"mtime": st.st_mtime,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ── Hermes plugin registration ──
|
|
178
|
+
|
|
179
|
+
def register(registry):
|
|
180
|
+
if registry is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
def doc_read(path: str, max_chars: int = 1_000_000, extract_images: bool = True):
|
|
184
|
+
try:
|
|
185
|
+
text = read(path, max_chars=max_chars, extract_images=extract_images)
|
|
186
|
+
return {
|
|
187
|
+
"success": True,
|
|
188
|
+
"path": path,
|
|
189
|
+
"text": text,
|
|
190
|
+
"length": len(text),
|
|
191
|
+
"message": f"Read {len(text)} chars from {path}",
|
|
192
|
+
}
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return {"success": False, "path": path, "error": f"{type(e).__name__}: {e}"}
|
|
195
|
+
|
|
196
|
+
def doc_read_meta(path: str):
|
|
197
|
+
try:
|
|
198
|
+
meta = read_meta(path)
|
|
199
|
+
return {"success": True, "path": path, "metadata": meta}
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return {"success": False, "path": path, "error": f"{type(e).__name__}: {e}"}
|
|
202
|
+
|
|
203
|
+
registry.register_tool(
|
|
204
|
+
name="doc_read",
|
|
205
|
+
toolset="doc",
|
|
206
|
+
schema={
|
|
207
|
+
"type": "object",
|
|
208
|
+
"properties": {
|
|
209
|
+
"path": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"description": "Absolute path to the document.",
|
|
212
|
+
},
|
|
213
|
+
"max_chars": {
|
|
214
|
+
"type": "integer",
|
|
215
|
+
"description": "Maximum characters to return.",
|
|
216
|
+
"default": 1_000_000,
|
|
217
|
+
},
|
|
218
|
+
"extract_images": {
|
|
219
|
+
"type": "boolean",
|
|
220
|
+
"description": "Extract text from images via OCR (LiteParse + Tesseract).",
|
|
221
|
+
"default": True,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
"required": ["path"],
|
|
225
|
+
},
|
|
226
|
+
handler=lambda args, **kw: json.dumps(doc_read(
|
|
227
|
+
args["path"],
|
|
228
|
+
max_chars=args.get("max_chars", 1_000_000),
|
|
229
|
+
extract_images=args.get("extract_images", True),
|
|
230
|
+
)),
|
|
231
|
+
description="Read and extract text from documents. Uses anydoc for office formats (DOCX, XLSX, PPTX, ODT, RTF, EPUB, PDF → Markdown) and LiteParse for OCR (scanned PDFs, images). Plain text files read directly.",
|
|
232
|
+
)
|
|
233
|
+
registry.register_tool(
|
|
234
|
+
name="doc_read_meta",
|
|
235
|
+
toolset="doc",
|
|
236
|
+
schema={
|
|
237
|
+
"type": "object",
|
|
238
|
+
"properties": {
|
|
239
|
+
"path": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"description": "Absolute path to the document.",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
"required": ["path"],
|
|
245
|
+
},
|
|
246
|
+
handler=lambda args, **kw: json.dumps(doc_read_meta(args["path"])),
|
|
247
|
+
description="Read document metadata (PDF info, image EXIF, etc.) via LiteParse.",
|
|
248
|
+
)
|
tabella/utils.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared helpers for the documents plugin.
|
|
2
|
+
|
|
3
|
+
Consolidates output-directory resolution that was previously duplicated in
|
|
4
|
+
doc_convert and doc_create.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_output_dir() -> Path:
|
|
12
|
+
"""Resolve output directory for documents.
|
|
13
|
+
|
|
14
|
+
Uses DOCS_OUTPUT_DIR env var if set, otherwise defaults to ~/Documents.
|
|
15
|
+
Fleet pCloud routing is handled by the document-routing skill, not here.
|
|
16
|
+
"""
|
|
17
|
+
d = os.environ.get("DOCS_OUTPUT_DIR")
|
|
18
|
+
if d:
|
|
19
|
+
p = Path(d).expanduser()
|
|
20
|
+
else:
|
|
21
|
+
p = Path.home() / "Documents"
|
|
22
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return p
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_in_output_dir(path: Path) -> bool:
|
|
27
|
+
"""Check if path is under the configured output directory."""
|
|
28
|
+
try:
|
|
29
|
+
resolved = path.expanduser().resolve()
|
|
30
|
+
user_dir = _get_output_dir()
|
|
31
|
+
return resolved != user_dir and user_dir in resolved.parents
|
|
32
|
+
except Exception:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_output_dir(category: str = None) -> Path:
|
|
37
|
+
"""Return output directory with optional category subfolder."""
|
|
38
|
+
base = _get_output_dir()
|
|
39
|
+
if category:
|
|
40
|
+
out = base / category
|
|
41
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
return out
|
|
43
|
+
return base
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tabella-docs
|
|
3
|
+
Version: 2.0.1
|
|
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
|
+
Author-email: D Yiapanis <d@yiapanis.co>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dyiapanis/tabella
|
|
8
|
+
Project-URL: Source, https://github.com/dyiapanis/tabella
|
|
9
|
+
Project-URL: Issues, https://github.com/dyiapanis/tabella/issues
|
|
10
|
+
Keywords: documents,docx,xlsx,pptx,pdf,odt,pandoc,anydoc,document-generation,markdown,ocr,agent-plugins,mcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Office/Business :: Office Suites
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: create
|
|
23
|
+
Requires-Dist: python-docx<2.0,>=1.0; extra == "create"
|
|
24
|
+
Requires-Dist: openpyxl<4.0,>=3.1; extra == "create"
|
|
25
|
+
Requires-Dist: python-pptx<2.0,>=0.6; extra == "create"
|
|
26
|
+
Requires-Dist: weasyprint<70.0,>=60.0; extra == "create"
|
|
27
|
+
Requires-Dist: odfpy<2.0,>=1.4; extra == "create"
|
|
28
|
+
Requires-Dist: Pillow<13.0,>=10.0; extra == "create"
|
|
29
|
+
Requires-Dist: lxml<7.0,>=5.0; extra == "create"
|
|
30
|
+
Provides-Extra: read
|
|
31
|
+
Requires-Dist: firecrawl-anydoc<0.3.0,>=0.1.1; extra == "read"
|
|
32
|
+
Requires-Dist: liteparse<3.0.0,>=2.0.0; extra == "read"
|
|
33
|
+
Requires-Dist: PyMuPDF<2.0,>=1.24; extra == "read"
|
|
34
|
+
Provides-Extra: convert
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: python-docx<2.0,>=1.0; extra == "all"
|
|
37
|
+
Requires-Dist: openpyxl<4.0,>=3.1; extra == "all"
|
|
38
|
+
Requires-Dist: python-pptx<2.0,>=0.6; extra == "all"
|
|
39
|
+
Requires-Dist: weasyprint<70.0,>=60.0; extra == "all"
|
|
40
|
+
Requires-Dist: odfpy<2.0,>=1.4; extra == "all"
|
|
41
|
+
Requires-Dist: Pillow<13.0,>=10.0; extra == "all"
|
|
42
|
+
Requires-Dist: lxml<7.0,>=5.0; extra == "all"
|
|
43
|
+
Requires-Dist: firecrawl-anydoc<0.3.0,>=0.1.1; extra == "all"
|
|
44
|
+
Requires-Dist: liteparse<3.0.0,>=2.0.0; extra == "all"
|
|
45
|
+
Requires-Dist: PyMuPDF<2.0,>=1.24; extra == "all"
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# tabella
|
|
51
|
+
|
|
52
|
+
A portable document lifecycle suite: create, convert, and read documents
|
|
53
|
+
from structured JSON or raw HTML/CSS.
|
|
54
|
+
|
|
55
|
+
## Cross-Platform Compatibility
|
|
56
|
+
|
|
57
|
+
This package conforms to the [Agent Plugins v1.0.0](https://agent-plugins.org/specification)
|
|
58
|
+
specification. It works with any compatible AI agent client that supports
|
|
59
|
+
MCP servers, including:
|
|
60
|
+
|
|
61
|
+
- [Hermes Agent](https://hermes-agent.nousresearch.com)
|
|
62
|
+
- [Cursor](https://cursor.com)
|
|
63
|
+
- [Claude Desktop](https://claude.ai/download)
|
|
64
|
+
- Any client that supports the Model Context Protocol
|
|
65
|
+
|
|
66
|
+
### Three installation paths
|
|
67
|
+
|
|
68
|
+
**Transports:** the plugin ships both.
|
|
69
|
+
|
|
70
|
+
- **stdio (default)** — the declaring client spawns `mcp_server.py` itself
|
|
71
|
+
(Agent Plugins v1 lifecycle; `PLUGIN_ROOT`/`PLUGIN_DATA` injected by the
|
|
72
|
+
client). Nothing to run by hand.
|
|
73
|
+
- **streamable-HTTP (self-host lane)** — for clients that prefer connecting
|
|
74
|
+
to a URL, run the server yourself:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python3 scripts/mcp_http_server.py # serves 127.0.0.1:8602
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
First run bootstraps the dependency venv (or `scripts/setup.sh`);
|
|
81
|
+
`TABELLA_TOKEN` enables bearer auth. Then point an `mcp.json`
|
|
82
|
+
`streamable-http` entry at `http://127.0.0.1:8602/mcp`.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
**Agent Plugins v1 package** — clients that support the v1 spec auto-discover
|
|
87
|
+
`plugin.json` + `mcp.json` from the repo root and launch the MCP server:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Hermes Agent
|
|
91
|
+
hermes plugins install dyiapanis/tabella --no-enable
|
|
92
|
+
hermes plugins enable tabella
|
|
93
|
+
|
|
94
|
+
# Other v1-compatible clients: follow your client's plugin installation instructions
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**MCP server (manual)** — point any MCP-compatible client at the server directly:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"tabella": {
|
|
103
|
+
"type": "stdio",
|
|
104
|
+
"command": "python3",
|
|
105
|
+
"args": ["/path/to/tabella/mcp_server.py"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**pip install** — use the Python library directly:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install tabella-docs[all]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Or install only the modules you need:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install tabella-docs[create] # python-docx, openpyxl, python-pptx, weasyprint, odfpy
|
|
121
|
+
pip install tabella-docs[read] # firecrawl-anydoc, liteparse, PyMuPDF
|
|
122
|
+
pip install tabella-docs[convert] # no python deps (needs system pandoc + texlive-xetex)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Modules
|
|
126
|
+
|
|
127
|
+
### Create
|
|
128
|
+
|
|
129
|
+
Generate documents from structured JSON content or raw HTML/CSS:
|
|
130
|
+
|
|
131
|
+
- **DOCX** — Word documents (python-docx)
|
|
132
|
+
- **XLSX** — Spreadsheets (openpyxl)
|
|
133
|
+
- **PPTX** — Presentations (python-pptx)
|
|
134
|
+
- **PDF** — Print-ready documents and visual designs (WeasyPrint)
|
|
135
|
+
- Document-style: structured JSON → HTML → PDF
|
|
136
|
+
- Visual-style: raw HTML/CSS (Grid, Flexbox, gradients, themes) → PDF
|
|
137
|
+
- **ODT/ODS/ODP** — OpenDocument formats (odfpy)
|
|
138
|
+
|
|
139
|
+
Accepts plain strings (auto-normalized), HTML, or structured dicts with
|
|
140
|
+
sections, sheets, slides, tables, images, and page breaks.
|
|
141
|
+
|
|
142
|
+
### Convert
|
|
143
|
+
|
|
144
|
+
Format conversion via pandoc + LaTeX (page size, margins, headers/footers
|
|
145
|
+
configurable). **Pandoc 3.1 supports 44 input and 54 output formats** —
|
|
146
|
+
`doc_convert` passes your `from`/`to` through, so all of them work. The
|
|
147
|
+
everyday set:
|
|
148
|
+
|
|
149
|
+
| Format | In | Out | Notes |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| Markdown (`.md`, variants) | ✅ | ✅ | gfm, commonmark, multimarkdown, strict… |
|
|
152
|
+
| HTML | ✅ | ✅ | html4/html5, chunkedhtml |
|
|
153
|
+
| DOCX (Word) | ✅ | ✅ | |
|
|
154
|
+
| ODT | ✅ | ✅ | + `opendocument` |
|
|
155
|
+
| PDF | ✅ | ✅ | in: text-based PDFs (pandoc's pdf reader); out: needs `pdflatex`/`xelatex` (texlive) — engine auto-selected |
|
|
156
|
+
| LaTeX (`.tex`) | ✅ | ✅ | incl. beamer slides out |
|
|
157
|
+
| RST | ✅ | ✅ | |
|
|
158
|
+
| EPUB | ✅ | ✅ | out: epub2/epub3 |
|
|
159
|
+
| CSV / TSV | ✅ | ✅ | out via markdown/AST route |
|
|
160
|
+
| RTF | ✅ | ✅ | |
|
|
161
|
+
| Plain text (`.txt`) | ✅ | ✅ | |
|
|
162
|
+
| Org-mode, MediaWiki, Jira wiki, Textile, DocBook, FB2, OPML, IPynb, JATS | ✅ | ✅ | writers for most |
|
|
163
|
+
| Typst / man / ms / texinfo / AsciiDoc | ✅ | ✅ | typst in+out |
|
|
164
|
+
| Presentations | — | ✅ | beamer, revealjs, slidy, s5, dzslides (from any reader, Markdown especially) |
|
|
165
|
+
| Pandoc AST (JSON) | ✅ | ✅ | for programmatic pipelines |
|
|
166
|
+
|
|
167
|
+
Everything not in the table: if `pandoc --list-input-formats` /
|
|
168
|
+
`--list-output-formats` lists it, `doc_convert` accepts it as-is — unknown
|
|
169
|
+
extensions pass through untouched.
|
|
170
|
+
|
|
171
|
+
### Read
|
|
172
|
+
|
|
173
|
+
Extract text and metadata from any document:
|
|
174
|
+
|
|
175
|
+
- **Office formats** (DOCX, DOC, XLSX, XLSB, PPTX, PPT, ODT, ODS, ODP, RTF,
|
|
176
|
+
EPUB, CSV) and **text-based PDFs** → GitHub-Flavored Markdown via
|
|
177
|
+
[anydoc](https://github.com/firecrawl/anydoc) (~4ms, in-process)
|
|
178
|
+
- **Scanned PDFs** and **images** (JPG, PNG, TIFF) → OCR via
|
|
179
|
+
[LiteParse](https://github.com/run-llama/liteparse) (Tesseract)
|
|
180
|
+
- **Plain text** (.txt, .md, .json, .py, .yaml, etc.) → direct file read
|
|
181
|
+
- Returns structured Markdown (office formats) or plain text (OCR, plain text)
|
|
182
|
+
|
|
183
|
+
## Prerequisites
|
|
184
|
+
|
|
185
|
+
### Create module
|
|
186
|
+
|
|
187
|
+
The create module uses a separate Python venv for heavy document libraries.
|
|
188
|
+
Set `DOCS_VENV_PYTHON` to point to it:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
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
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or install the libraries into your main Python environment:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pip install tabella-docs[create]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Read module
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
~/.venvs/docs/bin/pip install firecrawl-anydoc liteparse PyMuPDF
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Or:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install tabella-docs[read]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Convert module
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pip install tabella-docs[convert]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Requires system-installed pandoc and a LaTeX engine:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Debian/Ubuntu
|
|
224
|
+
sudo apt install pandoc texlive-xetex
|
|
225
|
+
|
|
226
|
+
# macOS
|
|
227
|
+
brew install pandoc basictex
|
|
228
|
+
|
|
229
|
+
# Arch
|
|
230
|
+
sudo pacman -S pandoc texlive
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### WeasyPrint system dependencies (PDF creation)
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Debian/Ubuntu
|
|
237
|
+
sudo apt install libcairo2 libpango-1.0-0 libgdk-pixbuf-2.0-0
|
|
238
|
+
|
|
239
|
+
# macOS
|
|
240
|
+
brew install cairo pango gdk-pixbuf
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Installation from source
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git clone https://github.com/dyiapanis/tabella.git
|
|
247
|
+
cd tabella
|
|
248
|
+
pip install -e ".[all]"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Configuration
|
|
252
|
+
|
|
253
|
+
| Variable | Purpose | Default |
|
|
254
|
+
|----------|---------|---------|
|
|
255
|
+
| `DOCS_VENV_PYTHON` | Python binary for document libraries | `PLUGIN_DATA/venv/bin/python` or `~/.venvs/docs/bin/python` |
|
|
256
|
+
| `DOCS_OUTPUT_DIR` | Default output directory for generated documents | `~/Documents` |
|
|
257
|
+
| `PLUGIN_DATA` | Client-managed data directory (set by v1 client) | — |
|
|
258
|
+
| `PLUGIN_ROOT` | Plugin root directory (set by v1 client) | — |
|
|
259
|
+
|
|
260
|
+
### Automatic bootstrap
|
|
261
|
+
|
|
262
|
+
On first startup, the MCP server checks if the docs venv exists. If not,
|
|
263
|
+
it runs `scripts/setup.sh` to create a venv in `PLUGIN_DATA` (provided by
|
|
264
|
+
the v1 client) or `~/.venvs/docs` as fallback, and installs all dependencies:
|
|
265
|
+
python-docx, openpyxl, python-pptx, weasyprint, odfpy, PyMuPDF,
|
|
266
|
+
firecrawl-anydoc, liteparse.
|
|
267
|
+
|
|
268
|
+
This means the v1 package is fully self-contained — no manual pip install
|
|
269
|
+
required. The bootstrap runs once; subsequent startups use the existing venv.
|
|
270
|
+
|
|
271
|
+
## Usage
|
|
272
|
+
|
|
273
|
+
### Create a PDF from HTML/CSS
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from agent_docs.create import create_document
|
|
277
|
+
|
|
278
|
+
result = create_document(
|
|
279
|
+
format="pdf",
|
|
280
|
+
content={"html": "<html><body><h1>Hello</h1></body></html>"},
|
|
281
|
+
output_path="/tmp/output.pdf",
|
|
282
|
+
options={"page_size": "A4"}
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Read a DOCX file
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from agent_docs.read import read
|
|
290
|
+
|
|
291
|
+
text = read("/path/to/document.docx")
|
|
292
|
+
# Returns GitHub-Flavored Markdown
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Convert Markdown to PDF
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from agent_docs.convert import doc_convert
|
|
299
|
+
|
|
300
|
+
result = doc_convert(
|
|
301
|
+
input_path="/path/to/input.md",
|
|
302
|
+
to_format="pdf",
|
|
303
|
+
output_path="/path/to/output.pdf"
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Via MCP
|
|
308
|
+
|
|
309
|
+
The bundled MCP server exposes five tools: `doc_read`, `doc_read_meta`,
|
|
310
|
+
`doc_convert`, `doc_create_document`, `doc_edit_pdf`. Any MCP-compatible
|
|
311
|
+
client can call these directly.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tabella/__init__.py,sha256=ZWesd1vh6548Mhzaph7RjCZLKpyvVHTcR5w3LblDcZo,1580
|
|
2
|
+
tabella/convert.py,sha256=84a9V2IBjpZYNtnnTXCfGw15-DpczicEoUetOzi0GCg,6529
|
|
3
|
+
tabella/create.py,sha256=uDIpK7slDakjJ3vXFbuOjfeZs14r5ja1JgnccEwPb2k,31105
|
|
4
|
+
tabella/read.py,sha256=kJBYnLywYb3lYyfDyKPU2f3KxJC2Z5ogFQYMUSmOS2c,9423
|
|
5
|
+
tabella/utils.py,sha256=m5hSj9Bep-GS1NOZ7NZk5MT3ZAElH6Q2uNIzMom3HR0,1216
|
|
6
|
+
tabella_docs-2.0.1.dist-info/licenses/LICENSE,sha256=YY_7vguV9tONkLu76Y4S4Rhxkny4g2O-yY5pMq5KLqQ,1066
|
|
7
|
+
tabella_docs-2.0.1.dist-info/METADATA,sha256=8XDi7ykrGyDoDMa0dZOK_F34owrKT1s26NkzQJ9kerg,10025
|
|
8
|
+
tabella_docs-2.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
tabella_docs-2.0.1.dist-info/top_level.txt,sha256=ALebCE3nPaYfRNya0n5oiH6Llu4PJiiVRxuFHk0c9yo,8
|
|
10
|
+
tabella_docs-2.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 D Yiapanis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tabella
|