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 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
+ )