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/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""tabella — Document lifecycle suite.
|
|
2
|
+
|
|
3
|
+
Three modules:
|
|
4
|
+
- create — Generate .docx/.xlsx/.pptx/.pdf/.odt/.ods/.odp from structured JSON
|
|
5
|
+
- convert — Format conversion via pandoc + LaTeX
|
|
6
|
+
- read — Extract text and metadata from any document via LiteParse
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
|
|
16
|
+
# ── Configuration ─────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
def get_docs_venv_python() -> str:
|
|
19
|
+
"""Path to the Python binary in the docs venv.
|
|
20
|
+
|
|
21
|
+
Resolution order:
|
|
22
|
+
1. DOCS_VENV_PYTHON env var (explicit override)
|
|
23
|
+
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
|
|
29
|
+
|
|
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")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_output_dir() -> Path:
|
|
40
|
+
"""Default output directory for generated documents.
|
|
41
|
+
|
|
42
|
+
Override with DOCS_OUTPUT_DIR env var. Defaults to ~/Documents.
|
|
43
|
+
"""
|
|
44
|
+
d = os.environ.get("DOCS_OUTPUT_DIR")
|
|
45
|
+
if d:
|
|
46
|
+
p = Path(d).expanduser()
|
|
47
|
+
else:
|
|
48
|
+
p = Path.home() / "Documents"
|
|
49
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return p
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["get_docs_venv_python", "get_output_dir"]
|
tabella/convert.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""documents.convert — Document format conversion via pandoc + LaTeX.
|
|
2
|
+
|
|
3
|
+
Supports: markdown → PDF/DOCX/HTML/ODT/EPUB and any pandoc-supported format pair.
|
|
4
|
+
Output directory is configurable via DOCS_OUTPUT_DIR (defaults to ~/Documents).
|
|
5
|
+
Merged from the doc_convert plugin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .utils import _get_output_dir, _is_in_output_dir, _resolve_output_dir
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _check_deps() -> Optional[str]:
|
|
21
|
+
"""Return error string if pandoc or LaTeX is missing, else None."""
|
|
22
|
+
if not shutil.which("pandoc"):
|
|
23
|
+
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
|
+
return "No LaTeX engine found. Install texlive (e.g. `sudo apt install texlive-xetex`)."
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _pdf_engine() -> str:
|
|
30
|
+
"""Return preferred LaTeX engine."""
|
|
31
|
+
if shutil.which("xelatex"):
|
|
32
|
+
return "xelatex"
|
|
33
|
+
return "pdflatex"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── tool ──
|
|
37
|
+
def doc_convert(
|
|
38
|
+
input_path: str,
|
|
39
|
+
to_format: str,
|
|
40
|
+
from_format: str = None,
|
|
41
|
+
output_path: str = None,
|
|
42
|
+
extra_args: list = None,
|
|
43
|
+
category: str = None,
|
|
44
|
+
) -> dict:
|
|
45
|
+
"""Convert a document from one format to another using pandoc.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
input_path: Absolute path to the source document.
|
|
49
|
+
to_format: Target format (e.g. "pdf", "docx", "html", "odt", "epub").
|
|
50
|
+
from_format: Optional source format hint (e.g. "markdown", "html").
|
|
51
|
+
When omitted, pandoc infers from file extension.
|
|
52
|
+
output_path: Optional absolute path for the output file.
|
|
53
|
+
When omitted, writes to the user's documents directory.
|
|
54
|
+
extra_args: Optional extra pandoc CLI flags (e.g. ["--toc", "-V", "geometry:margin=1in"]).
|
|
55
|
+
category: Optional subfolder under ~/documents/ (e.g. "reports", "invoices").
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
{"success": bool, "output_path": str, "from_format": str, "to_format": str,
|
|
59
|
+
"pdf_engine": str | None, "message": str}
|
|
60
|
+
"""
|
|
61
|
+
dep_err = _check_deps()
|
|
62
|
+
if dep_err:
|
|
63
|
+
return {"success": False, "error": dep_err}
|
|
64
|
+
|
|
65
|
+
src = Path(input_path).expanduser().resolve()
|
|
66
|
+
if not src.exists():
|
|
67
|
+
return {"success": False, "error": f"Source file not found: {src}"}
|
|
68
|
+
|
|
69
|
+
# Determine formats
|
|
70
|
+
src_fmt = from_format or _infer_format(src.suffix.lstrip(".").lower())
|
|
71
|
+
tgt_fmt = to_format.lower()
|
|
72
|
+
|
|
73
|
+
# Determine output path
|
|
74
|
+
if output_path:
|
|
75
|
+
out_file = Path(output_path).expanduser().resolve()
|
|
76
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
else:
|
|
78
|
+
out_dir = _resolve_output_dir(category)
|
|
79
|
+
stem = src.stem
|
|
80
|
+
out_file = out_dir / f"{stem}.{tgt_fmt}"
|
|
81
|
+
|
|
82
|
+
# Warn if output is outside the configured output directory
|
|
83
|
+
if not _is_in_output_dir(out_file):
|
|
84
|
+
logger.warning(
|
|
85
|
+
"doc_convert: output file %s is outside the configured output directory. "
|
|
86
|
+
"Files saved here may not be synced.",
|
|
87
|
+
out_file,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Build pandoc command
|
|
91
|
+
cmd = ["pandoc", str(src), "-o", str(out_file), "-f", src_fmt, "-t", tgt_fmt]
|
|
92
|
+
if tgt_fmt == "pdf":
|
|
93
|
+
engine = _pdf_engine()
|
|
94
|
+
cmd.extend(["--pdf-engine", engine])
|
|
95
|
+
if extra_args:
|
|
96
|
+
cmd.extend(extra_args)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
100
|
+
if result.returncode != 0:
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": f"pandoc failed (exit {result.returncode}): {result.stderr.strip()}",
|
|
104
|
+
}
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
return {"success": False, "error": "pandoc timed out after 5 minutes."}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return {"success": False, "error": f"pandoc error: {e}"}
|
|
109
|
+
|
|
110
|
+
if not out_file.exists():
|
|
111
|
+
return {"success": False, "error": f"Output file was not created: {out_file}"}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"success": True,
|
|
115
|
+
"output_path": str(out_file),
|
|
116
|
+
"from_format": src_fmt,
|
|
117
|
+
"to_format": tgt_fmt,
|
|
118
|
+
"pdf_engine": _pdf_engine() if tgt_fmt == "pdf" else None,
|
|
119
|
+
"message": f"MEDIA:{out_file}\n\nConverted {src.name} → {tgt_fmt}: {out_file}",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _infer_format(ext: str) -> str:
|
|
124
|
+
"""Map common extensions to pandoc format names."""
|
|
125
|
+
mapping = {
|
|
126
|
+
"md": "markdown",
|
|
127
|
+
"markdown": "markdown",
|
|
128
|
+
"txt": "plain",
|
|
129
|
+
"html": "html",
|
|
130
|
+
"htm": "html",
|
|
131
|
+
"docx": "docx",
|
|
132
|
+
"odt": "odt",
|
|
133
|
+
"pdf": "pdf",
|
|
134
|
+
"tex": "latex",
|
|
135
|
+
"rst": "rst",
|
|
136
|
+
"epub": "epub",
|
|
137
|
+
}
|
|
138
|
+
return mapping.get(ext, ext)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _handle_doc_convert(args: dict, **kwargs) -> str:
|
|
142
|
+
"""Handler wrapper for doc_convert tool."""
|
|
143
|
+
result = doc_convert(**args)
|
|
144
|
+
if isinstance(result, dict):
|
|
145
|
+
return json.dumps(result)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Hermes plugin registration ──
|
|
150
|
+
def register(registry):
|
|
151
|
+
registry.register_tool(
|
|
152
|
+
name="doc_convert",
|
|
153
|
+
toolset="doc-convert",
|
|
154
|
+
schema={
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"input_path": {
|
|
158
|
+
"type": "string",
|
|
159
|
+
"description": "Absolute path to the source document.",
|
|
160
|
+
},
|
|
161
|
+
"to_format": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "Target format: pdf, docx, html, odt, epub, etc.",
|
|
164
|
+
},
|
|
165
|
+
"from_format": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Optional source format hint (e.g. markdown, html). Pandoc infers from extension if omitted.",
|
|
168
|
+
},
|
|
169
|
+
"output_path": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"description": "Optional absolute output path. Defaults to user's documents directory.",
|
|
172
|
+
},
|
|
173
|
+
"extra_args": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"items": {"type": "string"},
|
|
176
|
+
"description": "Optional extra pandoc CLI flags.",
|
|
177
|
+
},
|
|
178
|
+
"category": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "Optional subfolder under documents/ (e.g. 'reports').",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
"required": ["input_path", "to_format"],
|
|
184
|
+
},
|
|
185
|
+
handler=_handle_doc_convert,
|
|
186
|
+
description="Convert documents between formats (PDF, DOCX, HTML, ODT, EPUB, etc.) using pandoc + LaTeX.",
|
|
187
|
+
)
|