tablewise 1.1.0__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.
- tablewise-1.1.0/LICENSE +21 -0
- tablewise-1.1.0/PKG-INFO +217 -0
- tablewise-1.1.0/README.md +169 -0
- tablewise-1.1.0/motor_docling.py +80 -0
- tablewise-1.1.0/motor_excel.py +500 -0
- tablewise-1.1.0/preconversion.py +93 -0
- tablewise-1.1.0/pyproject.toml +37 -0
- tablewise-1.1.0/setup.cfg +4 -0
- tablewise-1.1.0/tablewise.egg-info/PKG-INFO +217 -0
- tablewise-1.1.0/tablewise.egg-info/SOURCES.txt +29 -0
- tablewise-1.1.0/tablewise.egg-info/dependency_links.txt +1 -0
- tablewise-1.1.0/tablewise.egg-info/entry_points.txt +2 -0
- tablewise-1.1.0/tablewise.egg-info/requires.txt +11 -0
- tablewise-1.1.0/tablewise.egg-info/top_level.txt +6 -0
- tablewise-1.1.0/tablewise_app.py +361 -0
- tablewise-1.1.0/tablewise_cli.py +185 -0
- tablewise-1.1.0/tablewise_core.py +394 -0
- tablewise-1.1.0/tests/test_app_convertir.py +341 -0
- tablewise-1.1.0/tests/test_cli.py +149 -0
- tablewise-1.1.0/tests/test_cli_motores.py +93 -0
- tablewise-1.1.0/tests/test_core.py +649 -0
- tablewise-1.1.0/tests/test_docs.py +69 -0
- tablewise-1.1.0/tests/test_entradas_externas.py +128 -0
- tablewise-1.1.0/tests/test_motor_docling.py +50 -0
- tablewise-1.1.0/tests/test_motor_docling_cli.py +131 -0
- tablewise-1.1.0/tests/test_motor_docling_convertir.py +115 -0
- tablewise-1.1.0/tests/test_motor_excel.py +578 -0
- tablewise-1.1.0/tests/test_preconversion.py +113 -0
- tablewise-1.1.0/tests/test_preconversion_ejecutables_falsos.py +147 -0
- tablewise-1.1.0/tests/test_preconversion_errores.py +167 -0
- tablewise-1.1.0/tests/test_red_sintaxis.py +69 -0
tablewise-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luis Francisco Palomares Gamarra
|
|
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.
|
tablewise-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tablewise
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Convierte PDF/Word/Excel/PowerPoint a Markdown con cabecera de procedencia, listo para un RAG
|
|
5
|
+
Author: Luis Francisco Palomares Gamarra
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Luis Francisco Palomares Gamarra
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: markdown,rag,pdf,docx,xlsx,markitdown,docling
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
34
|
+
Classifier: Environment :: MacOS X
|
|
35
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
36
|
+
Requires-Python: >=3.11
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Provides-Extra: excel
|
|
40
|
+
Requires-Dist: openpyxl>=3.1; extra == "excel"
|
|
41
|
+
Provides-Extra: docling
|
|
42
|
+
Requires-Dist: docling; extra == "docling"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
45
|
+
Requires-Dist: openpyxl>=3.1; extra == "dev"
|
|
46
|
+
Requires-Dist: PyYAML>=6; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Tablewise
|
|
50
|
+
|
|
51
|
+
**Spreadsheet-grade document conversion to Markdown, with provenance.**
|
|
52
|
+
PDF, Word, Excel and PowerPoint in; clean, citable Markdown out — built for RAG,
|
|
53
|
+
LLM ingestion and audit trails.
|
|
54
|
+
|
|
55
|
+
Every output file starts with a header that tells your retrieval layer where the
|
|
56
|
+
text came from:
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
---
|
|
60
|
+
source: "2026-budget.xlsx"
|
|
61
|
+
engine: "excel"
|
|
62
|
+
converted: 2026-10-22
|
|
63
|
+
---
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Three front ends, one decision engine:
|
|
67
|
+
|
|
68
|
+
- **`tablewise` CLI** — one document in, one `.md` out. Scripts, agents, batches.
|
|
69
|
+
- **Desktop app** (Tkinter) — for people who do not live in a terminal.
|
|
70
|
+
- **Hosted API** — `POST /v1/convert`, per-month plans, no install. See
|
|
71
|
+
[PRICING.md](PRICING.md).
|
|
72
|
+
|
|
73
|
+
## The problem it solves
|
|
74
|
+
|
|
75
|
+
Most converters flatten a financial table into something no model can use. A
|
|
76
|
+
three-level header (`Hotel · 2026 · Real/Budget · Jan..Dec`) comes out as 70
|
|
77
|
+
columns named `Unnamed: 12`, formulas vanish, and a percentage becomes `0.8769`.
|
|
78
|
+
A RAG built on that answers with the wrong number and no way to prove it.
|
|
79
|
+
|
|
80
|
+
Tablewise routes spreadsheets to an engine that reads the sheet as a sheet:
|
|
81
|
+
|
|
82
|
+
| Hotel | 2026 | 2026 | 2026 |
|
|
83
|
+
| | Real | Budget | Last year |
|
|
84
|
+
| | Jan | Feb | Jan |
|
|
85
|
+
| ADR | 120.50 | 110 | 105.25 |
|
|
86
|
+
| Occupancy | 87.69% | | |
|
|
87
|
+
|
|
88
|
+
- Merged cells and multi-level headers preserved as a real Markdown table.
|
|
89
|
+
- Dates as `YYYY-MM-DD`, percentages with their format, `-0.00` never appears.
|
|
90
|
+
- Hidden sheets kept but labelled `(hidden sheet)`.
|
|
91
|
+
- A formula that was never recalculated says `(formula not calculated)` instead
|
|
92
|
+
of an empty cell that a model reads as zero.
|
|
93
|
+
- Deterministic: no AI, no network, same bytes in → same Markdown out.
|
|
94
|
+
|
|
95
|
+
## Quickstart
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install -e . # installs the `tablewise` command
|
|
99
|
+
tablewise report.pdf # -> ~/md-repo/report.md
|
|
100
|
+
tablewise budget.xlsx -o ~/md-repo -n "2026 budget"
|
|
101
|
+
tablewise contract.docx --dry-run # print the engine command, do not run it
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The desktop app:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python tablewise_app.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The API:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -X POST https://api.tablewise.dev/v1/convert \
|
|
114
|
+
-H "Authorization: Bearer $TABLEWISE_API_KEY" \
|
|
115
|
+
-F "file=@budget.xlsx"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Why not just MarkItDown / Docling?
|
|
119
|
+
|
|
120
|
+
They are good engines and Tablewise uses both. The difference is the routing,
|
|
121
|
+
the table fidelity and the provenance — not a new parser.
|
|
122
|
+
|
|
123
|
+
| | MarkItDown | Docling | Tablewise |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| Excel with merged / multi-level header | flattened | partial | read as a sheet |
|
|
126
|
+
| Formula never recalculated | empty cell | empty cell | labelled |
|
|
127
|
+
| Provenance header on every file | no | no | yes |
|
|
128
|
+
| Deterministic, no AI, no network | yes | yes (heavy) | yes |
|
|
129
|
+
| Desktop GUI for non-technical users | no | no | yes |
|
|
130
|
+
| Hosted API with per-month plans and batch | no | no | yes |
|
|
131
|
+
|
|
132
|
+
## Engine routing
|
|
133
|
+
|
|
134
|
+
Each extension has exactly one owner:
|
|
135
|
+
|
|
136
|
+
- `.xlsx` `.xlsm` `.xltx` `.xltm` → `motor_excel.py` (openpyxl, local, no AI)
|
|
137
|
+
- `.pdf` `.docx` `.pptx` → `motor_docling.py` (Docling, local, no AI)
|
|
138
|
+
- `.doc` `.rtf` `.odt` → `textutil` (macOS) → HTML → `motor_docling.py`
|
|
139
|
+
- `.xls` `.ppt` (Office 97-2003) → LibreOffice → `.xlsx`/`.pptx` → `motor_excel.py` / `motor_docling.py`
|
|
140
|
+
- everything else (CSV, HTML, EPUB, …) → MarkItDown
|
|
141
|
+
|
|
142
|
+
The two own engines are `motor_excel.py` and `motor_docling.py`. A format
|
|
143
|
+
without an available engine is refused with an explicit message, never silently
|
|
144
|
+
degraded.
|
|
145
|
+
|
|
146
|
+
## CLI reference
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
tablewise DOCUMENT [-o DIR] [-n NAME] [--python PATH] [--markitdown PATH]
|
|
150
|
+
[--force] [--dry-run] [-q] [--version]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- Never overwrites an existing `.md` without `--force`.
|
|
154
|
+
- Exit codes: `0` ok · `1` the engine produced no file · `2` invalid input.
|
|
155
|
+
- `--python` / `TABLEWISE_PYTHON`: interpreter that carries docling/openpyxl.
|
|
156
|
+
- `--markitdown` / `TABLEWISE_MARKITDOWN`: MarkItDown executable.
|
|
157
|
+
|
|
158
|
+
## Hosted API
|
|
159
|
+
|
|
160
|
+
The API is the same engine behind HTTP, for teams that cannot install anything:
|
|
161
|
+
|
|
162
|
+
| Plan | Units / month | Price |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| Free | 50 | $0 |
|
|
165
|
+
| Starter | 1,000 | $19 |
|
|
166
|
+
| Pro | 5,000 | $49 |
|
|
167
|
+
| Business | 25,000 | $149 |
|
|
168
|
+
|
|
169
|
+
One unit = one PDF page, one spreadsheet sheet, one slide, or one document.
|
|
170
|
+
Overage $0.015/unit. Annual −20%. The full contract (quota, DPA, refunds) is in
|
|
171
|
+
[PRICING.md](PRICING.md) and on the website.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
.venv/bin/python3 -m pytest -q # 432 tests
|
|
179
|
+
.venv/bin/python3 -m pytest server/tests -q # 46 API tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
| File | What it covers |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `tests/test_core.py` | `tablewise_core.py`: extension routing, safe names, engine command (`shlex`, `~`), YAML frontmatter, atomic write, free name |
|
|
185
|
+
| `tests/test_motor_excel.py` | `motor_excel.py` on synthetic books: merged cells, unevaluated formulas, empty rows/columns, dates, hidden sheets, safety ceiling |
|
|
186
|
+
| `tests/test_cli_motores.py` | engine CLI contract by subprocess (exit code + message, never a traceback) |
|
|
187
|
+
| `tests/test_motor_docling.py` | `.doc`/`.rtf`/`.odt` preconversion to HTML with `textutil` (no Docling import; skipped off macOS) |
|
|
188
|
+
| `tests/test_preconversion.py` | LibreOffice (`soffice`): lookup outside PATH and `.xls` → `.xlsx` keeping merges (skipped without LibreOffice) |
|
|
189
|
+
| `tests/test_preconversion_errores.py` | `preconversion.py` with simulated `subprocess.run`: timeout, tool that cannot start, exit 0 without file, error code, own profile |
|
|
190
|
+
| `tests/test_motor_docling_convertir.py` | `motor_docling.convertir` with an injected fake Docling: empty output, temp cleanup, `.ppt` routing, uppercase extensions |
|
|
191
|
+
| `tests/test_entradas_externas.py` | `.xls` path, malformed array formulas, backslashes in the YAML header, non-UTF-8 output, BOM and CRLF |
|
|
192
|
+
| `tests/test_preconversion_ejecutables_falsos.py` | textutil/LibreOffice boundary with fake executables |
|
|
193
|
+
| `tests/test_app_convertir.py` | `App.convert()` with fake tkinter and engine: existing `.md` (cancel / numbered copy / overwrite), failing engine, non-UTF-8 output, partial writes |
|
|
194
|
+
| `tests/test_motor_docling_cli.py` | `motor_docling` CLI with a fake `docling` package |
|
|
195
|
+
| `tests/test_red_sintaxis.py` | regression net: every application file is valid Python and the app imports without opening a window |
|
|
196
|
+
| `tests/test_cli.py` | `tablewise` CLI: same decisions as the app, `--dry-run`, no overwrite without `--force`, exit codes |
|
|
197
|
+
| `tests/test_docs.py` | drift net: this README lists every test file and every routed extension |
|
|
198
|
+
|
|
199
|
+
No test uses real documents.
|
|
200
|
+
|
|
201
|
+
## Limits and security
|
|
202
|
+
|
|
203
|
+
- Upload limit 25 MB and 2,000 units per request (configurable).
|
|
204
|
+
- Engines run as subprocesses with a timeout; a hung converter cannot take the
|
|
205
|
+
worker down.
|
|
206
|
+
- API keys are stored as SHA-256 hashes and shown once.
|
|
207
|
+
- No document content is logged. See `web/privacy.html` and `web/dpa.html`.
|
|
208
|
+
- macOS-only pieces: `textutil` (`.doc`/`.rtf`/`.odt`) and the Tkinter app.
|
|
209
|
+
LibreOffice is needed for `.xls`/`.ppt`.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT. See [LICENSE](LICENSE).
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
Spanish engineering log, kept for continuity: [README.es.md](README.es.md).
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Tablewise
|
|
2
|
+
|
|
3
|
+
**Spreadsheet-grade document conversion to Markdown, with provenance.**
|
|
4
|
+
PDF, Word, Excel and PowerPoint in; clean, citable Markdown out — built for RAG,
|
|
5
|
+
LLM ingestion and audit trails.
|
|
6
|
+
|
|
7
|
+
Every output file starts with a header that tells your retrieval layer where the
|
|
8
|
+
text came from:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
---
|
|
12
|
+
source: "2026-budget.xlsx"
|
|
13
|
+
engine: "excel"
|
|
14
|
+
converted: 2026-10-22
|
|
15
|
+
---
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Three front ends, one decision engine:
|
|
19
|
+
|
|
20
|
+
- **`tablewise` CLI** — one document in, one `.md` out. Scripts, agents, batches.
|
|
21
|
+
- **Desktop app** (Tkinter) — for people who do not live in a terminal.
|
|
22
|
+
- **Hosted API** — `POST /v1/convert`, per-month plans, no install. See
|
|
23
|
+
[PRICING.md](PRICING.md).
|
|
24
|
+
|
|
25
|
+
## The problem it solves
|
|
26
|
+
|
|
27
|
+
Most converters flatten a financial table into something no model can use. A
|
|
28
|
+
three-level header (`Hotel · 2026 · Real/Budget · Jan..Dec`) comes out as 70
|
|
29
|
+
columns named `Unnamed: 12`, formulas vanish, and a percentage becomes `0.8769`.
|
|
30
|
+
A RAG built on that answers with the wrong number and no way to prove it.
|
|
31
|
+
|
|
32
|
+
Tablewise routes spreadsheets to an engine that reads the sheet as a sheet:
|
|
33
|
+
|
|
34
|
+
| Hotel | 2026 | 2026 | 2026 |
|
|
35
|
+
| | Real | Budget | Last year |
|
|
36
|
+
| | Jan | Feb | Jan |
|
|
37
|
+
| ADR | 120.50 | 110 | 105.25 |
|
|
38
|
+
| Occupancy | 87.69% | | |
|
|
39
|
+
|
|
40
|
+
- Merged cells and multi-level headers preserved as a real Markdown table.
|
|
41
|
+
- Dates as `YYYY-MM-DD`, percentages with their format, `-0.00` never appears.
|
|
42
|
+
- Hidden sheets kept but labelled `(hidden sheet)`.
|
|
43
|
+
- A formula that was never recalculated says `(formula not calculated)` instead
|
|
44
|
+
of an empty cell that a model reads as zero.
|
|
45
|
+
- Deterministic: no AI, no network, same bytes in → same Markdown out.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e . # installs the `tablewise` command
|
|
51
|
+
tablewise report.pdf # -> ~/md-repo/report.md
|
|
52
|
+
tablewise budget.xlsx -o ~/md-repo -n "2026 budget"
|
|
53
|
+
tablewise contract.docx --dry-run # print the engine command, do not run it
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The desktop app:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python tablewise_app.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The API:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl -X POST https://api.tablewise.dev/v1/convert \
|
|
66
|
+
-H "Authorization: Bearer $TABLEWISE_API_KEY" \
|
|
67
|
+
-F "file=@budget.xlsx"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Why not just MarkItDown / Docling?
|
|
71
|
+
|
|
72
|
+
They are good engines and Tablewise uses both. The difference is the routing,
|
|
73
|
+
the table fidelity and the provenance — not a new parser.
|
|
74
|
+
|
|
75
|
+
| | MarkItDown | Docling | Tablewise |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| Excel with merged / multi-level header | flattened | partial | read as a sheet |
|
|
78
|
+
| Formula never recalculated | empty cell | empty cell | labelled |
|
|
79
|
+
| Provenance header on every file | no | no | yes |
|
|
80
|
+
| Deterministic, no AI, no network | yes | yes (heavy) | yes |
|
|
81
|
+
| Desktop GUI for non-technical users | no | no | yes |
|
|
82
|
+
| Hosted API with per-month plans and batch | no | no | yes |
|
|
83
|
+
|
|
84
|
+
## Engine routing
|
|
85
|
+
|
|
86
|
+
Each extension has exactly one owner:
|
|
87
|
+
|
|
88
|
+
- `.xlsx` `.xlsm` `.xltx` `.xltm` → `motor_excel.py` (openpyxl, local, no AI)
|
|
89
|
+
- `.pdf` `.docx` `.pptx` → `motor_docling.py` (Docling, local, no AI)
|
|
90
|
+
- `.doc` `.rtf` `.odt` → `textutil` (macOS) → HTML → `motor_docling.py`
|
|
91
|
+
- `.xls` `.ppt` (Office 97-2003) → LibreOffice → `.xlsx`/`.pptx` → `motor_excel.py` / `motor_docling.py`
|
|
92
|
+
- everything else (CSV, HTML, EPUB, …) → MarkItDown
|
|
93
|
+
|
|
94
|
+
The two own engines are `motor_excel.py` and `motor_docling.py`. A format
|
|
95
|
+
without an available engine is refused with an explicit message, never silently
|
|
96
|
+
degraded.
|
|
97
|
+
|
|
98
|
+
## CLI reference
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
tablewise DOCUMENT [-o DIR] [-n NAME] [--python PATH] [--markitdown PATH]
|
|
102
|
+
[--force] [--dry-run] [-q] [--version]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Never overwrites an existing `.md` without `--force`.
|
|
106
|
+
- Exit codes: `0` ok · `1` the engine produced no file · `2` invalid input.
|
|
107
|
+
- `--python` / `TABLEWISE_PYTHON`: interpreter that carries docling/openpyxl.
|
|
108
|
+
- `--markitdown` / `TABLEWISE_MARKITDOWN`: MarkItDown executable.
|
|
109
|
+
|
|
110
|
+
## Hosted API
|
|
111
|
+
|
|
112
|
+
The API is the same engine behind HTTP, for teams that cannot install anything:
|
|
113
|
+
|
|
114
|
+
| Plan | Units / month | Price |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Free | 50 | $0 |
|
|
117
|
+
| Starter | 1,000 | $19 |
|
|
118
|
+
| Pro | 5,000 | $49 |
|
|
119
|
+
| Business | 25,000 | $149 |
|
|
120
|
+
|
|
121
|
+
One unit = one PDF page, one spreadsheet sheet, one slide, or one document.
|
|
122
|
+
Overage $0.015/unit. Annual −20%. The full contract (quota, DPA, refunds) is in
|
|
123
|
+
[PRICING.md](PRICING.md) and on the website.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
.venv/bin/python3 -m pytest -q # 432 tests
|
|
131
|
+
.venv/bin/python3 -m pytest server/tests -q # 46 API tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| File | What it covers |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `tests/test_core.py` | `tablewise_core.py`: extension routing, safe names, engine command (`shlex`, `~`), YAML frontmatter, atomic write, free name |
|
|
137
|
+
| `tests/test_motor_excel.py` | `motor_excel.py` on synthetic books: merged cells, unevaluated formulas, empty rows/columns, dates, hidden sheets, safety ceiling |
|
|
138
|
+
| `tests/test_cli_motores.py` | engine CLI contract by subprocess (exit code + message, never a traceback) |
|
|
139
|
+
| `tests/test_motor_docling.py` | `.doc`/`.rtf`/`.odt` preconversion to HTML with `textutil` (no Docling import; skipped off macOS) |
|
|
140
|
+
| `tests/test_preconversion.py` | LibreOffice (`soffice`): lookup outside PATH and `.xls` → `.xlsx` keeping merges (skipped without LibreOffice) |
|
|
141
|
+
| `tests/test_preconversion_errores.py` | `preconversion.py` with simulated `subprocess.run`: timeout, tool that cannot start, exit 0 without file, error code, own profile |
|
|
142
|
+
| `tests/test_motor_docling_convertir.py` | `motor_docling.convertir` with an injected fake Docling: empty output, temp cleanup, `.ppt` routing, uppercase extensions |
|
|
143
|
+
| `tests/test_entradas_externas.py` | `.xls` path, malformed array formulas, backslashes in the YAML header, non-UTF-8 output, BOM and CRLF |
|
|
144
|
+
| `tests/test_preconversion_ejecutables_falsos.py` | textutil/LibreOffice boundary with fake executables |
|
|
145
|
+
| `tests/test_app_convertir.py` | `App.convert()` with fake tkinter and engine: existing `.md` (cancel / numbered copy / overwrite), failing engine, non-UTF-8 output, partial writes |
|
|
146
|
+
| `tests/test_motor_docling_cli.py` | `motor_docling` CLI with a fake `docling` package |
|
|
147
|
+
| `tests/test_red_sintaxis.py` | regression net: every application file is valid Python and the app imports without opening a window |
|
|
148
|
+
| `tests/test_cli.py` | `tablewise` CLI: same decisions as the app, `--dry-run`, no overwrite without `--force`, exit codes |
|
|
149
|
+
| `tests/test_docs.py` | drift net: this README lists every test file and every routed extension |
|
|
150
|
+
|
|
151
|
+
No test uses real documents.
|
|
152
|
+
|
|
153
|
+
## Limits and security
|
|
154
|
+
|
|
155
|
+
- Upload limit 25 MB and 2,000 units per request (configurable).
|
|
156
|
+
- Engines run as subprocesses with a timeout; a hung converter cannot take the
|
|
157
|
+
worker down.
|
|
158
|
+
- API keys are stored as SHA-256 hashes and shown once.
|
|
159
|
+
- No document content is logged. See `web/privacy.html` and `web/dpa.html`.
|
|
160
|
+
- macOS-only pieces: `textutil` (`.doc`/`.rtf`/`.odt`) and the Tkinter app.
|
|
161
|
+
LibreOffice is needed for `.xls`/`.ppt`.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT. See [LICENSE](LICENSE).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
Spanish engineering log, kept for continuity: [README.es.md](README.es.md).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversor de PDF/Word/PowerPoint (y escaneados) a Markdown con Docling
|
|
3
|
+
(IBM, open source, 100% local, gratis) -- sustituye a MarkItDown para estos
|
|
4
|
+
formatos porque reconstruye tablas complejas con celdas combinadas en vez
|
|
5
|
+
de volcarlas en crudo (ver README.md, "Piloto de alternativas gratis",
|
|
6
|
+
23/08/2026).
|
|
7
|
+
|
|
8
|
+
Import diferido: docling carga modelos pesados (layout/tabla) la primera
|
|
9
|
+
vez que se usa. Se importa dentro de convertir(), no al importar el
|
|
10
|
+
módulo, para que cargar este fichero sea barato -- solo paga el coste
|
|
11
|
+
quien de verdad convierte un documento (invocado siempre como subprocess
|
|
12
|
+
en un venv aparte, nunca dentro del proceso de la GUI Tkinter).
|
|
13
|
+
|
|
14
|
+
CLI: python motor_docling.py <origen> <destino.md>
|
|
15
|
+
Librería: from motor_docling import convertir
|
|
16
|
+
"""
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from tablewise_core import EXTENSIONES_TEXTUTIL, escribir_atomico, misma_ruta
|
|
22
|
+
from preconversion import con_soffice, con_textutil
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConversionVacia(Exception):
|
|
26
|
+
"""Docling no produjo ningún contenido."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def preconvertir(origen: Path, carpeta: Path) -> Path:
|
|
30
|
+
"""Lo que Docling no lee bien, a un formato que sí:
|
|
31
|
+
- .doc/.rtf/.odt → HTML con textutil (macOS). HTML y no .docx: el
|
|
32
|
+
.docx de textutil aplana las tablas, y la ruta nativa de Docling para
|
|
33
|
+
.doc también (pasa por LibreOffice y saca "ConceptoImporte...").
|
|
34
|
+
- .ppt → .pptx con LibreOffice.
|
|
35
|
+
Lo demás se devuelve tal cual."""
|
|
36
|
+
ext = origen.suffix.lower()
|
|
37
|
+
if ext in EXTENSIONES_TEXTUTIL:
|
|
38
|
+
return con_textutil(origen, carpeta)
|
|
39
|
+
if ext == ".ppt":
|
|
40
|
+
return con_soffice(origen, "pptx", carpeta)
|
|
41
|
+
return origen
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def convertir(origen: Path) -> str:
|
|
45
|
+
from docling.document_converter import DocumentConverter
|
|
46
|
+
|
|
47
|
+
converter = DocumentConverter()
|
|
48
|
+
with tempfile.TemporaryDirectory(prefix="tablewise-") as tmp:
|
|
49
|
+
resultado = converter.convert(str(preconvertir(origen, Path(tmp))))
|
|
50
|
+
md = resultado.document.export_to_markdown()
|
|
51
|
+
if not md.strip():
|
|
52
|
+
raise ConversionVacia(f"Docling no produjo contenido para {origen.name}.")
|
|
53
|
+
return md
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
if len(sys.argv) < 3:
|
|
58
|
+
sys.exit("Uso: python motor_docling.py <origen> <destino.md>")
|
|
59
|
+
origen = Path(sys.argv[1]).expanduser()
|
|
60
|
+
destino = Path(sys.argv[2]).expanduser()
|
|
61
|
+
if not origen.exists():
|
|
62
|
+
sys.exit(f"No existe: {origen}")
|
|
63
|
+
if misma_ruta(origen, destino):
|
|
64
|
+
sys.exit(f"El destino es el propio documento de origen ({origen.name}): "
|
|
65
|
+
"no se sobrescribe.")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
md = convertir(origen)
|
|
69
|
+
except ConversionVacia as e:
|
|
70
|
+
sys.exit(str(e))
|
|
71
|
+
except Exception as e:
|
|
72
|
+
sys.exit(f"motor_docling falló con {origen.name}: {e}")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
escribir_atomico(destino, md)
|
|
76
|
+
except (OSError, UnicodeError) as e:
|
|
77
|
+
# Sin esto, un destino no escribible acababa en traceback crudo,
|
|
78
|
+
# que la app enseña tal cual en el diálogo de error.
|
|
79
|
+
sys.exit(f"No se pudo escribir {destino}: {e}")
|
|
80
|
+
print(f"Guardado en {destino} ({len(md)} caracteres)")
|