pdf-tool-cli 1.1.0__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.
- pdf_tool_cli-1.1.0.dist-info/METADATA +179 -0
- pdf_tool_cli-1.1.0.dist-info/RECORD +66 -0
- pdf_tool_cli-1.1.0.dist-info/WHEEL +4 -0
- pdf_tool_cli-1.1.0.dist-info/entry_points.txt +4 -0
- pdf_tool_cli-1.1.0.dist-info/licenses/LICENSE +21 -0
- pdftool/__init__.py +3 -0
- pdftool/cli/__init__.py +179 -0
- pdftool/cli/__main__.py +5 -0
- pdftool/cli/args.py +138 -0
- pdftool/cli/report.py +71 -0
- pdftool/core/__init__.py +0 -0
- pdftool/core/atomic.py +43 -0
- pdftool/core/batch.py +111 -0
- pdftool/core/config.py +89 -0
- pdftool/core/errors.py +36 -0
- pdftool/core/jobs.py +188 -0
- pdftool/core/logger.py +84 -0
- pdftool/core/naming.py +91 -0
- pdftool/core/plugin.py +96 -0
- pdftool/core/registry.py +59 -0
- pdftool/core/thumbnails.py +37 -0
- pdftool/core/updater.py +67 -0
- pdftool/main.py +30 -0
- pdftool/tools/__init__.py +0 -0
- pdftool/tools/compress/__init__.py +0 -0
- pdftool/tools/compress/logic.py +387 -0
- pdftool/tools/compress/panel.py +72 -0
- pdftool/tools/compress/params.py +18 -0
- pdftool/tools/compress/spec.py +18 -0
- pdftool/tools/images2pdf/__init__.py +0 -0
- pdftool/tools/images2pdf/logic.py +127 -0
- pdftool/tools/images2pdf/panel.py +22 -0
- pdftool/tools/images2pdf/params.py +6 -0
- pdftool/tools/images2pdf/spec.py +19 -0
- pdftool/tools/merge/__init__.py +0 -0
- pdftool/tools/merge/logic.py +54 -0
- pdftool/tools/merge/panel.py +44 -0
- pdftool/tools/merge/params.py +20 -0
- pdftool/tools/merge/spec.py +19 -0
- pdftool/tools/protect/__init__.py +0 -0
- pdftool/tools/protect/logic.py +85 -0
- pdftool/tools/protect/panel.py +72 -0
- pdftool/tools/protect/params.py +18 -0
- pdftool/tools/protect/spec.py +18 -0
- pdftool/tools/split/__init__.py +0 -0
- pdftool/tools/split/logic.py +139 -0
- pdftool/tools/split/panel.py +129 -0
- pdftool/tools/split/params.py +28 -0
- pdftool/tools/split/spec.py +19 -0
- pdftool/tools/watermark/__init__.py +0 -0
- pdftool/tools/watermark/logic.py +110 -0
- pdftool/tools/watermark/panel.py +67 -0
- pdftool/tools/watermark/params.py +19 -0
- pdftool/tools/watermark/spec.py +19 -0
- pdftool/ui/__init__.py +0 -0
- pdftool/ui/accessibility.py +44 -0
- pdftool/ui/app.py +425 -0
- pdftool/ui/errors.py +12 -0
- pdftool/ui/file_list.py +275 -0
- pdftool/ui/logs.py +58 -0
- pdftool/ui/output_dir.py +127 -0
- pdftool/ui/panel_base.py +631 -0
- pdftool/ui/platform.py +30 -0
- pdftool/ui/registry.py +61 -0
- pdftool/ui/theme.py +30 -0
- pdftool/ui/thumbnails.py +148 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdf-tool-cli
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Herramientas PDF (unir, dividir, comprimir, proteger…) como CLI para agentes y app de escritorio.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: pdf,cli,agents,merge,split,compress
|
|
8
|
+
Author: Juan Manuel Armero
|
|
9
|
+
Author-email: juanmanuel.armero@gmail.com
|
|
10
|
+
Requires-Python: >=3.11,<4.0
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Provides-Extra: gui
|
|
22
|
+
Requires-Dist: flet (==0.28.2) ; extra == "gui"
|
|
23
|
+
Requires-Dist: platformdirs (>=4.2.0,<5.0.0)
|
|
24
|
+
Requires-Dist: pydantic (>=2.6.0,<3.0.0)
|
|
25
|
+
Requires-Dist: pymupdf (>=1.24.3,<2.0.0)
|
|
26
|
+
Project-URL: Homepage, https://github.com/juanMaAV92/pdf-tool
|
|
27
|
+
Project-URL: Repository, https://github.com/juanMaAV92/pdf-tool
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
|
|
32
|
+
# 📄 PDF Tool
|
|
33
|
+
|
|
34
|
+
**Una app de escritorio sencilla para trabajar con tus PDFs — sin subir nada a internet.**
|
|
35
|
+
|
|
36
|
+
<img width="980" height="680" alt="Screenshot 2026-06-15 at 6 54 49 PM" src="https://github.com/user-attachments/assets/adf416d8-bb05-478d-8a6d-31a35410af28" />
|
|
37
|
+
|
|
38
|
+
Comprime, une, divide, protege y marca tus documentos —y conviértelos desde imágenes—
|
|
39
|
+
desde tu propio ordenador. Gratis, de código abierto y sin anuncios.
|
|
40
|
+
|
|
41
|
+
[](https://github.com/juanMaAV92/pdf-tool/releases/latest)
|
|
42
|
+
|
|
43
|
+
[](https://github.com/juanMaAV92/pdf-tool/actions/workflows/ci.yml)
|
|
44
|
+
[](https://github.com/juanMaAV92/pdf-tool/releases/latest)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- 📸 Añade aquí un screenshot o GIF de la app:
|
|
51
|
+

|
|
52
|
+
Es lo que más convence a quien entra al repo. -->
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ✨ Qué puedes hacer
|
|
57
|
+
|
|
58
|
+
| | Herramienta | Para qué sirve |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 🗜️ | **Comprimir PDF** | Reduce el peso de uno o varios PDFs hasta el tamaño en MB que elijas. |
|
|
61
|
+
| 🔗 | **Unir PDFs** | Combina varios documentos en uno solo, en el orden que quieras y con el nombre que elijas. |
|
|
62
|
+
| ✂️ | **Dividir PDF** | Separa un PDF en varios: por rangos de páginas, una página por archivo o cada N páginas. |
|
|
63
|
+
| 🖼️ | **Imágenes a PDF** | Convierte cada imagen (JPG/PNG) en su propio PDF — y si quieres uno solo, únelos después. |
|
|
64
|
+
| 🔒 | **Proteger PDF** | Añade o quita la contraseña de uno o varios PDFs a la vez. |
|
|
65
|
+
| 💧 | **Marca de agua** | Repite un texto en diagonal sobre todas las páginas. |
|
|
66
|
+
|
|
67
|
+
> 🔐 **Todo ocurre en tu equipo.** Tus archivos nunca salen de tu ordenador.
|
|
68
|
+
|
|
69
|
+
En **Comprimir PDF**, el modo predeterminado busca la reducción máxima. También
|
|
70
|
+
puedes elegir **Conservar texto y enlaces** desde el selector de modo; el icono
|
|
71
|
+
de ayuda explica la diferencia.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## ⬇️ Descargar e instalar
|
|
76
|
+
|
|
77
|
+
Ve a la **[página de descargas](https://github.com/juanMaAV92/pdf-tool/releases/latest)** y elige el archivo de tu sistema:
|
|
78
|
+
|
|
79
|
+
### 🍎 macOS
|
|
80
|
+
1. Descarga el archivo **`.dmg`** y ábrelo.
|
|
81
|
+
2. Arrastra **PDF Tool** a la carpeta *Aplicaciones*.
|
|
82
|
+
3. Ábrela desde *Aplicaciones*. macOS mostrará un aviso y **no** dejará abrirla aún
|
|
83
|
+
(es normal: la app no está firmada, pero es segura).
|
|
84
|
+
4. Ve a **Menú Apple () → Ajustes del Sistema → Privacidad y Seguridad**.
|
|
85
|
+
5. Baja hasta la sección **Seguridad**: verás un mensaje que dice que se bloqueó *“PDF Tool”*.
|
|
86
|
+
Pulsa **Abrir de todas formas** y confirma con tu contraseña o Touch ID.
|
|
87
|
+
<br><sub>Solo hay que hacerlo la primera vez. A partir de ahí se abre con doble clic como cualquier app.</sub>
|
|
88
|
+
|
|
89
|
+
### 🪟 Windows
|
|
90
|
+
1. Descarga el **instalador** (`.exe`) y ejecútalo.
|
|
91
|
+
2. Si aparece SmartScreen: **Más información** → **Ejecutar de todas formas**.
|
|
92
|
+
<br><sub>(El aviso sale porque el instalador no está firmado; es seguro.)</sub>
|
|
93
|
+
|
|
94
|
+
> ✅ Tus ajustes se guardan fuera de la app, así que se conservan al actualizar. Cuando publique una nueva versión, la app te avisa con un banner.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 🤖 Para agentes de IA y la línea de comandos
|
|
99
|
+
|
|
100
|
+
Las mismas herramientas están disponibles como comando, pensado para agentes
|
|
101
|
+
(Claude Code, Cursor, Copilot…) y scripts. Todo sigue ocurriendo en tu equipo.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pipx install pdf-tool-cli # solo la CLI (sin la app de escritorio)
|
|
105
|
+
pdf-tool list --json # qué herramientas hay y qué aceptan
|
|
106
|
+
pdf-tool compress informe.pdf --json # comprimir y obtener el resultado en JSON
|
|
107
|
+
pdf-tool merge a.pdf b.pdf --output-name todo
|
|
108
|
+
echo "$CLAVE" | pdf-tool protect a.pdf --password-stdin
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Cada herramienta tiene su ayuda (`pdf-tool compress --help`). Códigos de salida:
|
|
112
|
+
`0` ok · `1` error al procesar · `2` uso inválido · `3` algún archivo del lote falló.
|
|
113
|
+
`pipx install 'pdf-tool-cli[gui]'` instala además la app (comando `pdftool`).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🤝 Contribuir
|
|
118
|
+
|
|
119
|
+
¡Las contribuciones son bienvenidas! Puedes abrir un *issue* con ideas o errores,
|
|
120
|
+
o mandar un *pull request*. La arquitectura es modular: añadir una herramienta nueva
|
|
121
|
+
es crear una carpeta y la app la descubre sola. En el [roadmap](docs/roadmap.md)
|
|
122
|
+
está lo que viene: una CLI para agentes de IA, a la par con la app.
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary><b>🛠️ Para desarrolladores (montar el proyecto en local)</b></summary>
|
|
126
|
+
|
|
127
|
+
### Requisitos
|
|
128
|
+
- Python 3.11+ y [Poetry](https://python-poetry.org/)
|
|
129
|
+
|
|
130
|
+
### Arrancar
|
|
131
|
+
```bash
|
|
132
|
+
poetry install --all-extras # incluye Flet (extra [gui]) para la app
|
|
133
|
+
poetry run pdftool # abre la app
|
|
134
|
+
poetry run pytest # corre los tests
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Añadir una herramienta nueva
|
|
138
|
+
1. Crea `pdftool/tools/<id>/` con `params.py`, `logic.py` (función pura + tests),
|
|
139
|
+
`spec.py` (registra la herramienta, sin Flet), `panel.py` y un `__init__.py` vacío.
|
|
140
|
+
2. La app la muestra en la barra lateral y la CLI la ofrece como `pdf-tool <id>`,
|
|
141
|
+
las dos automáticamente.
|
|
142
|
+
|
|
143
|
+
La **lógica** (`logic.py`) no importa Flet y se prueba sin UI; la **UI** (`panel.py`)
|
|
144
|
+
solo arma controles. Detalles en [AGENTS.md](AGENTS.md).
|
|
145
|
+
|
|
146
|
+
### Publicar una versión (automatizado con GitHub Actions)
|
|
147
|
+
```bash
|
|
148
|
+
git tag v0.2.0
|
|
149
|
+
git push origin v0.2.0
|
|
150
|
+
```
|
|
151
|
+
Eso dispara `release.yml`, que corre los tests, construye macOS (`.dmg`) y Windows
|
|
152
|
+
(instalador Inno Setup) en paralelo, hornea la versión del tag en el build y crea
|
|
153
|
+
un **GitHub Release en borrador** con ambos instaladores. Revísalo y pulsa
|
|
154
|
+
**Publish release** para que las apps instaladas detecten la actualización.
|
|
155
|
+
Si los instaladores se construyen bien, publica también el paquete en **PyPI**
|
|
156
|
+
(Trusted Publishing, entorno `pypi` de GitHub; sin tokens).
|
|
157
|
+
|
|
158
|
+
Los PRs que cambian la app, el empaquetado o sus dependencias ejecutan además
|
|
159
|
+
**Package smoke**: construye e inspecciona el DMG de macOS y compila e instala de
|
|
160
|
+
forma silenciosa el instalador de Windows. Antes de una release importante,
|
|
161
|
+
seguir la [checklist manual](docs/release-smoke.md) para validar la ventana y los
|
|
162
|
+
diálogos nativos.
|
|
163
|
+
|
|
164
|
+
El número de versión es **el tag** — única fuente de verdad.
|
|
165
|
+
|
|
166
|
+
### Build local (para depurar el empaquetado)
|
|
167
|
+
```bash
|
|
168
|
+
poetry run flet build macos # -> build/macos/pdf-tool.app
|
|
169
|
+
poetry run flet build windows # -> build/windows/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
</details>
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
<div align="center">
|
|
177
|
+
<sub>Hecho con 🐍 Python + <a href="https://flet.dev">Flet</a> · Licencia <a href="LICENSE">MIT</a></sub>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
pdftool/__init__.py,sha256=WBAFDUQGkc9SJZWu6dmQHTAxXUG5xf6imG6emenBA_o,170
|
|
2
|
+
pdftool/cli/__init__.py,sha256=pQZ8WGZuslAR0-KXVyOO1vZe0pnWvhZ1mA9eH2STJEU,6289
|
|
3
|
+
pdftool/cli/__main__.py,sha256=FwvSvCBK50eXZfN_hx8uQabEU5XS-BEBvm66BxTjsyI,59
|
|
4
|
+
pdftool/cli/args.py,sha256=2_D1EB8N9u6kTtHb44NrDf9Kule3P4VbK-NaI4UeyNI,4662
|
|
5
|
+
pdftool/cli/report.py,sha256=-zfi_lXlM1JC3-XanuecRzsh-xUY7aLpQIsfjK3REyI,2071
|
|
6
|
+
pdftool/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pdftool/core/atomic.py,sha256=KnMeGznQklI00gQgahcWTNHj4tPbPQahOuzHNdIi9h0,1511
|
|
8
|
+
pdftool/core/batch.py,sha256=clt9F8kIL5FqjNOZaq9pYuLMxVS3q60-xQ0vx2vbdwE,3338
|
|
9
|
+
pdftool/core/config.py,sha256=0QHkU0NlWzfAs6gFqWdU6atebageCdJJtKVt693tIxQ,2855
|
|
10
|
+
pdftool/core/errors.py,sha256=OBcNR17H7THDaAjzDWG1ldsfVJ7xBoowJus_aSeI3nM,1284
|
|
11
|
+
pdftool/core/jobs.py,sha256=OsHtjUQnXHRcNlGFZvZbCo4lCSu-eKcZWof0h-JmQqo,5672
|
|
12
|
+
pdftool/core/logger.py,sha256=1A2kPvRdbo45o1Z5vP7NmLpR2CqHH7gSysRTOQVE_Z8,3234
|
|
13
|
+
pdftool/core/naming.py,sha256=y9IBzlOOQUVZXTRV0fasDsExmJvyqyRBFxqtCG1KcTY,3193
|
|
14
|
+
pdftool/core/plugin.py,sha256=zmSimrzoQ-5PeLjFhI-VpEwqwvlAdFGjPm-MXZklvv4,2780
|
|
15
|
+
pdftool/core/registry.py,sha256=d-Y85c4vMT8Qr-Mz52MiaGF6Z4Ic84A2acWoRF54AR0,2055
|
|
16
|
+
pdftool/core/thumbnails.py,sha256=7fKEoPbX1oA5cfY_kHivawgnfTGuR9jJtWQNA7J1nB8,1422
|
|
17
|
+
pdftool/core/updater.py,sha256=idPQ2aczATwYLGQ6Cu5fXhlUhSsO5uKgUQv3stCxBDA,2407
|
|
18
|
+
pdftool/main.py,sha256=e_sswy-GGcJqr64wUY7iHGs4vRMY5FFclXfwTC3bNeM,740
|
|
19
|
+
pdftool/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
pdftool/tools/compress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
pdftool/tools/compress/logic.py,sha256=ZImLu-LumdmSDFaBqc2UQyuyW9oC8HHuz7dEd7TmwWk,13563
|
|
22
|
+
pdftool/tools/compress/panel.py,sha256=2loLSClDjLUj-k-dyOlmPCkAOsGcolD7AGEWdaSGnfQ,2705
|
|
23
|
+
pdftool/tools/compress/params.py,sha256=iHDg_QdmH3xZQx-9VymTnfKPIgmaU_FmKOeamBn679c,642
|
|
24
|
+
pdftool/tools/compress/spec.py,sha256=oFZKUN6DPhY8hSN1XnshxhVt6FPkIZj4Fwobg2_zuEo,532
|
|
25
|
+
pdftool/tools/images2pdf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
pdftool/tools/images2pdf/logic.py,sha256=fXyNNQVVQzDBcNUmcGYxSUyF7fWwZivHTEX7GvhiP-E,4216
|
|
27
|
+
pdftool/tools/images2pdf/panel.py,sha256=nCABYZrsSUMyu45Xzl_nhvO8TGFHALRxq5AC6ojuccM,601
|
|
28
|
+
pdftool/tools/images2pdf/params.py,sha256=OKhQNY5OpuaL6nVhFSRbOEAoLIUttbwqVS-soXNcqzA,213
|
|
29
|
+
pdftool/tools/images2pdf/spec.py,sha256=Vw2xuwva62EbmKORlZZTM-nJZb6pO77gmKd9cZigjMI,580
|
|
30
|
+
pdftool/tools/merge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
pdftool/tools/merge/logic.py,sha256=KzTXkIG4GDIb3XNFMvLgaDSAP9f8CS7jGouoll3vOvc,1919
|
|
32
|
+
pdftool/tools/merge/panel.py,sha256=piWOju5z6ud-L8L9ZVNkkmjq0jwm3dbp8G0Hal-ZDg4,1391
|
|
33
|
+
pdftool/tools/merge/params.py,sha256=VTKRxa4ZNMwvIwhpnlC-aCRZcTMtdHz4GFqHaItmcUA,689
|
|
34
|
+
pdftool/tools/merge/spec.py,sha256=c0awYK64h3GPYM1cygpesGTb2hjBXS9SA65lzyhpqzs,526
|
|
35
|
+
pdftool/tools/protect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
pdftool/tools/protect/logic.py,sha256=TWHyzWqOIQ9VU0TqBXHeaKNWGTcTDWhv5rdOY4vHEic,3171
|
|
37
|
+
pdftool/tools/protect/panel.py,sha256=nYsYmZP3RgTeiB1hP2xcDcNDq5xM9QC6DIOwlJhSNqY,2738
|
|
38
|
+
pdftool/tools/protect/params.py,sha256=qhQYvPuVzopzPg_2cWNlwq-F3qtbp0SYIkptidlRrzU,513
|
|
39
|
+
pdftool/tools/protect/spec.py,sha256=-P3qvxS5x_VG5uNdM3J40qHSjJL1l-CtYhzkYm1beM0,516
|
|
40
|
+
pdftool/tools/split/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
pdftool/tools/split/logic.py,sha256=vm-y3o0S9pPm3gd6jdOJBfJzQhaucDgkFzT3tyTzA78,4854
|
|
42
|
+
pdftool/tools/split/panel.py,sha256=c8fICu12aGUs2G6f8Dekizi-CTEd0unlVnWElw46oeM,4670
|
|
43
|
+
pdftool/tools/split/params.py,sha256=0fsqhFYcFlSiOLkg2K61qnSiGz_bewq0sBz5HrHVdDA,814
|
|
44
|
+
pdftool/tools/split/spec.py,sha256=_6AKzirSVC3e8tWu2hs_S1kW2-D0hTo-9HlOrYtQOD0,523
|
|
45
|
+
pdftool/tools/watermark/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
pdftool/tools/watermark/logic.py,sha256=ie39AelUiRJBEvX5wW1mXjwxtcxzIo-km6wNVvbWzAE,4244
|
|
47
|
+
pdftool/tools/watermark/panel.py,sha256=3K3fiX0ap_3QyPsueiqx7t50GLiAnZKr0c9ZpOOrGpM,2120
|
|
48
|
+
pdftool/tools/watermark/params.py,sha256=uutERPzkIOVtDzSA9OXsjYCF4dcc29hg-Vz4T2uL7-s,611
|
|
49
|
+
pdftool/tools/watermark/spec.py,sha256=KQKQBag0Bsx8nq21IVsqSfoSYH4Wn3M31pWmNZuu5tQ,552
|
|
50
|
+
pdftool/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
+
pdftool/ui/accessibility.py,sha256=b7bfTZf3SpIldMDKX8kcQtiMWiTwSun0PiSyHichmSc,1226
|
|
52
|
+
pdftool/ui/app.py,sha256=dCZotEIqWadjuHnZQzrlnDJbmzpHlmjz7AcF_69OLMU,14418
|
|
53
|
+
pdftool/ui/errors.py,sha256=QNOrgpqwLVjZpArAo123fr5RMuREC2YZO3eP-3AyheM,402
|
|
54
|
+
pdftool/ui/file_list.py,sha256=fTJ7oafBQ0B6OVYvQJZs7mAxP00FHahlT75jKjeVcQ8,10541
|
|
55
|
+
pdftool/ui/logs.py,sha256=WRZ8popiG6zgF-_b-DdzxyEnFwT0ELoLvZi18MHgB64,1703
|
|
56
|
+
pdftool/ui/output_dir.py,sha256=2pxgM00kE7REhzcnyxk1BnMMMMd1Fc4pOWolIvHJUXA,4494
|
|
57
|
+
pdftool/ui/panel_base.py,sha256=JkLp92muH3NB-hYd6qqvL4wr3kLCo3_oog9xgFuaxZw,24799
|
|
58
|
+
pdftool/ui/platform.py,sha256=7N5R3DadObrqbZ2rfbFN9VxkfBH32CKywmflvxR5cA8,847
|
|
59
|
+
pdftool/ui/registry.py,sha256=xW1afZYbmOppZTZVmFJ4QBgqN59ZwqhYT7tvV2Tc3Ho,2072
|
|
60
|
+
pdftool/ui/theme.py,sha256=siyopw5U5HWsPLtl_1lFiSla_FRfQCLTHXezzhWOIdU,668
|
|
61
|
+
pdftool/ui/thumbnails.py,sha256=IKeVPl2DqjXVM4aDklMjic6tTO8HBFmjMOG8wE60djE,5058
|
|
62
|
+
pdf_tool_cli-1.1.0.dist-info/METADATA,sha256=6rp6SRMZoC-oh8Fi0IlKtgP6ZhPuMKKPHmSpwqX2PyM,7895
|
|
63
|
+
pdf_tool_cli-1.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
64
|
+
pdf_tool_cli-1.1.0.dist-info/entry_points.txt,sha256=MkYurBGAWAzCn--whsJYB-RURm5V6n_q_lD930ftFAQ,71
|
|
65
|
+
pdf_tool_cli-1.1.0.dist-info/licenses/LICENSE,sha256=vt-PHUat0X1Blgpf3iJ-zX8roxuSXMbSO4DZ4GVhUt0,1075
|
|
66
|
+
pdf_tool_cli-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Manuel Armero
|
|
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.
|
pdftool/__init__.py
ADDED
pdftool/cli/__init__.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""`pdf-tool`: las herramientas de la app como línea de comandos (sin Flet).
|
|
2
|
+
|
|
3
|
+
Los subcomandos salen del registro de specs y sus flags, de los params de cada
|
|
4
|
+
herramienta: una herramienta nueva aparece aquí sin tocar este módulo.
|
|
5
|
+
Códigos de salida: 0 ok · 1 error al procesar · 2 uso o params inválidos ·
|
|
6
|
+
3 lote parcial (algún archivo falló).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, TextIO
|
|
19
|
+
|
|
20
|
+
from pdftool import __version__
|
|
21
|
+
from pdftool.cli.args import add_params, build_params
|
|
22
|
+
from pdftool.cli.report import error_to_dict, exit_code, render_text, result_to_dict
|
|
23
|
+
from pdftool.core import registry
|
|
24
|
+
from pdftool.core.errors import UserError, user_message
|
|
25
|
+
from pdftool.core.plugin import ToolSpec
|
|
26
|
+
|
|
27
|
+
EXIT_ERROR = 1
|
|
28
|
+
EXIT_USAGE = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _UsageError(Exception):
|
|
32
|
+
def __init__(self, message: str, usage: str) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.usage = usage
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Parser(argparse.ArgumentParser):
|
|
38
|
+
"""Un uso inválido lanza en vez de salir: así `--json` también lo reporta
|
|
39
|
+
en JSON. Los subcomandos heredan esta clase."""
|
|
40
|
+
|
|
41
|
+
def error(self, message: str): # type: ignore[override]
|
|
42
|
+
raise _UsageError(f"{self.prog}: {message}", self.format_usage())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_parser(specs: Sequence[ToolSpec]) -> argparse.ArgumentParser:
|
|
46
|
+
parser = _Parser(
|
|
47
|
+
prog="pdf-tool",
|
|
48
|
+
description="Herramientas locales para PDFs: nada sale de tu equipo.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
51
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="COMANDO")
|
|
52
|
+
listing = sub.add_parser("list", help="Lista las herramientas disponibles.")
|
|
53
|
+
listing.add_argument("--json", action="store_true", help="Salida en JSON.")
|
|
54
|
+
for spec in specs:
|
|
55
|
+
tool = sub.add_parser(
|
|
56
|
+
spec.meta.id, help=spec.meta.name, description=spec.meta.description
|
|
57
|
+
)
|
|
58
|
+
tool.add_argument(
|
|
59
|
+
"inputs",
|
|
60
|
+
nargs="+",
|
|
61
|
+
type=Path,
|
|
62
|
+
metavar="ARCHIVO",
|
|
63
|
+
help="Archivos de entrada (" + ", ".join(spec.extensions) + ").",
|
|
64
|
+
)
|
|
65
|
+
add_params(tool, spec.params)
|
|
66
|
+
tool.add_argument("--json", action="store_true", help="Salida en JSON.")
|
|
67
|
+
return parser
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _spec_to_dict(spec: ToolSpec) -> dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"id": spec.meta.id,
|
|
73
|
+
"name": spec.meta.name,
|
|
74
|
+
"description": spec.meta.description,
|
|
75
|
+
"category": spec.meta.category,
|
|
76
|
+
"inputs": {
|
|
77
|
+
"min": spec.min_inputs,
|
|
78
|
+
"max": spec.max_inputs,
|
|
79
|
+
"extensions": list(spec.extensions),
|
|
80
|
+
},
|
|
81
|
+
"params": spec.params.model_json_schema(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _print_json(data: Any) -> None:
|
|
86
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _list(specs: Sequence[ToolSpec], as_json: bool) -> int:
|
|
90
|
+
if as_json:
|
|
91
|
+
_print_json([_spec_to_dict(s) for s in specs])
|
|
92
|
+
else:
|
|
93
|
+
for s in specs:
|
|
94
|
+
print(f"{s.meta.id:<11} {s.meta.name} — {s.meta.description}")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_inputs(spec: ToolSpec, inputs: list[Path]) -> list[Path]:
|
|
99
|
+
n = len(inputs)
|
|
100
|
+
if n < spec.min_inputs:
|
|
101
|
+
raise UserError(f"{spec.meta.id} necesita al menos {spec.min_inputs} archivos")
|
|
102
|
+
if spec.max_inputs is not None and n > spec.max_inputs:
|
|
103
|
+
raise UserError(
|
|
104
|
+
f"{spec.meta.id} acepta como máximo {spec.max_inputs} archivo(s)"
|
|
105
|
+
)
|
|
106
|
+
for path in inputs:
|
|
107
|
+
if not path.exists():
|
|
108
|
+
raise UserError(f"No existe: {path}")
|
|
109
|
+
if not path.is_file():
|
|
110
|
+
raise UserError(f"No es un archivo: {path}")
|
|
111
|
+
if path.suffix.lower().lstrip(".") not in spec.extensions:
|
|
112
|
+
expected = ", ".join(f".{e}" for e in spec.extensions)
|
|
113
|
+
raise UserError(f"{path.name}: se espera {expected}")
|
|
114
|
+
return [path.resolve() for path in inputs]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fail(spec: ToolSpec, exc: BaseException, as_json: bool, code: int) -> int:
|
|
118
|
+
if as_json:
|
|
119
|
+
_print_json(error_to_dict(spec.meta.id, exc))
|
|
120
|
+
else:
|
|
121
|
+
message, detail = user_message(exc)
|
|
122
|
+
print(f"pdf-tool: {message}", file=sys.stderr)
|
|
123
|
+
if detail is not None:
|
|
124
|
+
print(f"detalle: {detail}", file=sys.stderr)
|
|
125
|
+
return code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run(spec: ToolSpec, ns: argparse.Namespace, env, stdin) -> int:
|
|
129
|
+
try:
|
|
130
|
+
inputs = _check_inputs(spec, ns.inputs)
|
|
131
|
+
params = build_params(ns, spec.params, env=env, stdin=stdin)
|
|
132
|
+
if params.output_dir is not None and not params.output_dir.is_dir():
|
|
133
|
+
raise UserError(f"No existe la carpeta de salida: {params.output_dir}")
|
|
134
|
+
except UserError as exc:
|
|
135
|
+
return _fail(spec, exc, ns.json, EXIT_USAGE)
|
|
136
|
+
try:
|
|
137
|
+
result = spec.run(inputs, params)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
return _fail(spec, exc, ns.json, EXIT_ERROR)
|
|
140
|
+
if ns.json:
|
|
141
|
+
_print_json(result_to_dict(spec.meta.id, result))
|
|
142
|
+
else:
|
|
143
|
+
print(render_text(result))
|
|
144
|
+
return exit_code(result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(
|
|
148
|
+
argv: list[str] | None = None,
|
|
149
|
+
*,
|
|
150
|
+
stdin: TextIO | None = None,
|
|
151
|
+
env: Mapping[str, str] | None = None,
|
|
152
|
+
) -> int:
|
|
153
|
+
# La lógica registra avisos por lote; sin handler, Python los escribiría
|
|
154
|
+
# en stderr y ensuciaría la salida. El resultado ya los reporta.
|
|
155
|
+
logging.getLogger("pdftool").addHandler(logging.NullHandler())
|
|
156
|
+
registry.discover()
|
|
157
|
+
specs = registry.get_specs()
|
|
158
|
+
parser = _build_parser(specs)
|
|
159
|
+
args = sys.argv[1:] if argv is None else argv
|
|
160
|
+
try:
|
|
161
|
+
ns = parser.parse_args(args)
|
|
162
|
+
except SystemExit as exc: # --help/--version
|
|
163
|
+
return exc.code if isinstance(exc.code, int) else 0
|
|
164
|
+
except _UsageError as exc:
|
|
165
|
+
if "--json" in args:
|
|
166
|
+
tool_id = next((a for a in args if a in {s.meta.id for s in specs}), None)
|
|
167
|
+
_print_json(error_to_dict(tool_id, UserError(str(exc))))
|
|
168
|
+
else:
|
|
169
|
+
print(f"{exc.usage}{exc}", file=sys.stderr)
|
|
170
|
+
return EXIT_USAGE
|
|
171
|
+
if ns.command == "list":
|
|
172
|
+
return _list(specs, ns.json)
|
|
173
|
+
spec = next(s for s in specs if s.meta.id == ns.command)
|
|
174
|
+
return _run(
|
|
175
|
+
spec,
|
|
176
|
+
ns,
|
|
177
|
+
os.environ if env is None else env,
|
|
178
|
+
sys.stdin if stdin is None else stdin,
|
|
179
|
+
)
|
pdftool/cli/__main__.py
ADDED
pdftool/cli/args.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Traduce los params Pydantic de una herramienta a flags de argparse (sin Flet).
|
|
2
|
+
|
|
3
|
+
Cada campo `snake_case` se vuelve `--kebab-case`. Lo que no se pasa queda en
|
|
4
|
+
None y no llega al modelo, así manda el default de Pydantic. Los campos
|
|
5
|
+
secretos (`json_schema_extra={"secret": True}`) nunca son un flag: se leen de
|
|
6
|
+
stdin con `--<campo>-stdin` o de la variable `PDFTOOL_<CAMPO>`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import types
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated, Any, Literal, TextIO, Union, get_args, get_origin
|
|
16
|
+
|
|
17
|
+
from pydantic import ValidationError
|
|
18
|
+
from pydantic.fields import FieldInfo
|
|
19
|
+
|
|
20
|
+
from pdftool.core.errors import UserError
|
|
21
|
+
from pdftool.core.plugin import BaseParams
|
|
22
|
+
|
|
23
|
+
_SCALARS = (int, float, str, Path)
|
|
24
|
+
# Nombres que ya usa el comando (`pdf-tool <id> ARCHIVO... --json`) o argparse.
|
|
25
|
+
RESERVED = frozenset({"json", "inputs", "command", "help", "version"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _flag(name: str) -> str:
|
|
29
|
+
return "--" + name.replace("_", "-")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _env_var(name: str) -> str:
|
|
33
|
+
return f"PDFTOOL_{name.upper()}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_secret(field: FieldInfo) -> bool:
|
|
37
|
+
extra = field.json_schema_extra
|
|
38
|
+
return isinstance(extra, dict) and extra.get("secret") is True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _strip_optional(annotation: Any) -> Any:
|
|
42
|
+
if get_origin(annotation) in (Union, types.UnionType):
|
|
43
|
+
args = [a for a in get_args(annotation) if a is not type(None)]
|
|
44
|
+
if len(args) == 1:
|
|
45
|
+
return args[0]
|
|
46
|
+
return annotation
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _unannotated(annotation: Any) -> Any:
|
|
50
|
+
if get_origin(annotation) is Annotated:
|
|
51
|
+
return get_args(annotation)[0]
|
|
52
|
+
return annotation
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _type_kwargs(name: str, annotation: Any) -> dict[str, Any]:
|
|
56
|
+
annotation = _strip_optional(annotation)
|
|
57
|
+
origin = get_origin(annotation)
|
|
58
|
+
if origin is Literal:
|
|
59
|
+
return {"choices": list(get_args(annotation))}
|
|
60
|
+
if origin is tuple:
|
|
61
|
+
items = tuple(_unannotated(i) for i in get_args(annotation))
|
|
62
|
+
if items and all(i in _SCALARS for i in items) and len(set(items)) == 1:
|
|
63
|
+
return {"type": items[0], "nargs": len(items)}
|
|
64
|
+
if annotation in _SCALARS:
|
|
65
|
+
return {"type": annotation}
|
|
66
|
+
raise TypeError(f"Campo {name!r}: tipo {annotation!r} no soportado por la CLI")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _help(field: FieldInfo) -> str:
|
|
70
|
+
text = field.description or ""
|
|
71
|
+
if not field.is_required() and field.default not in (None, ""):
|
|
72
|
+
default = field.default
|
|
73
|
+
if isinstance(default, tuple):
|
|
74
|
+
default = " ".join(str(v) for v in default)
|
|
75
|
+
text += f" (por defecto: {default})"
|
|
76
|
+
return text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def add_params(parser: argparse.ArgumentParser, params: type[BaseParams]) -> None:
|
|
80
|
+
for name, field in params.model_fields.items():
|
|
81
|
+
if name in RESERVED:
|
|
82
|
+
raise TypeError(f"Campo {name!r}: nombre reservado por la CLI")
|
|
83
|
+
if _is_secret(field):
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
f"{_flag(name)}-stdin",
|
|
86
|
+
dest=f"{name}_stdin",
|
|
87
|
+
action="store_true",
|
|
88
|
+
help=f"{field.description or name} Se lee de la primera línea de "
|
|
89
|
+
f"stdin (o de la variable {_env_var(name)}).",
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
_flag(name),
|
|
94
|
+
dest=name,
|
|
95
|
+
default=None,
|
|
96
|
+
required=field.is_required(),
|
|
97
|
+
help=_help(field),
|
|
98
|
+
**_type_kwargs(name, field.annotation),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _secret_value(
|
|
103
|
+
name: str, ns: argparse.Namespace, env: Mapping[str, str], stdin: TextIO
|
|
104
|
+
) -> str | None:
|
|
105
|
+
if getattr(ns, f"{name}_stdin", False):
|
|
106
|
+
return stdin.readline().rstrip("\r\n")
|
|
107
|
+
return env.get(_env_var(name))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_params(
|
|
111
|
+
ns: argparse.Namespace,
|
|
112
|
+
params: type[BaseParams],
|
|
113
|
+
*,
|
|
114
|
+
env: Mapping[str, str],
|
|
115
|
+
stdin: TextIO,
|
|
116
|
+
) -> BaseParams:
|
|
117
|
+
values: dict[str, Any] = {}
|
|
118
|
+
for name, field in params.model_fields.items():
|
|
119
|
+
value: Any
|
|
120
|
+
if _is_secret(field):
|
|
121
|
+
value = _secret_value(name, ns, env, stdin)
|
|
122
|
+
if value is None and field.is_required():
|
|
123
|
+
raise UserError(
|
|
124
|
+
f"Falta {name}: pásalo con {_flag(name)}-stdin "
|
|
125
|
+
f"o la variable {_env_var(name)}"
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
value = getattr(ns, name, None)
|
|
129
|
+
if isinstance(value, list):
|
|
130
|
+
value = tuple(value)
|
|
131
|
+
if value is not None:
|
|
132
|
+
values[name] = value
|
|
133
|
+
try:
|
|
134
|
+
return params(**values)
|
|
135
|
+
except ValidationError as exc:
|
|
136
|
+
error = exc.errors()[0]
|
|
137
|
+
field_name = str(error["loc"][0]) if error["loc"] else ""
|
|
138
|
+
raise UserError(f"{_flag(field_name)}: {error['msg']}") from exc
|
pdftool/cli/report.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Presenta un `ToolResult` como texto para personas o JSON para agentes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pdftool.core.errors import user_message
|
|
9
|
+
from pdftool.core.plugin import FileResult, ToolResult
|
|
10
|
+
|
|
11
|
+
# Algún archivo del lote falló, pero otros salieron (ver `exit_code`).
|
|
12
|
+
EXIT_PARTIAL = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _path(path: Path | None) -> str | None:
|
|
16
|
+
return None if path is None else str(path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _item_to_dict(item: FileResult) -> dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"input": _path(item.input_path),
|
|
22
|
+
"output": _path(item.output_path),
|
|
23
|
+
"ok": item.ok,
|
|
24
|
+
"message": item.message,
|
|
25
|
+
"warnings": list(item.warnings),
|
|
26
|
+
"metrics": dict(item.metrics),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _all_ok(result: ToolResult) -> bool:
|
|
31
|
+
return all(item.ok for item in result.items or [])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def result_to_dict(tool_id: str, result: ToolResult) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"ok": _all_ok(result),
|
|
37
|
+
"tool": tool_id,
|
|
38
|
+
"summary": result.summary,
|
|
39
|
+
"outputs": [str(p) for p in result.outputs],
|
|
40
|
+
"items": None
|
|
41
|
+
if result.items is None
|
|
42
|
+
else [_item_to_dict(i) for i in result.items],
|
|
43
|
+
"warnings": list(result.warnings),
|
|
44
|
+
"metrics": dict(result.metrics),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def error_to_dict(tool_id: str | None, exc: BaseException) -> dict[str, Any]:
|
|
49
|
+
message, detail = user_message(exc)
|
|
50
|
+
return {
|
|
51
|
+
"ok": False,
|
|
52
|
+
"tool": tool_id,
|
|
53
|
+
"error": {"message": message, "detail": detail},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_text(result: ToolResult) -> str:
|
|
58
|
+
lines = [result.summary]
|
|
59
|
+
if result.items is None:
|
|
60
|
+
lines += [str(p) for p in result.outputs]
|
|
61
|
+
else:
|
|
62
|
+
for item in result.items:
|
|
63
|
+
mark = "✓" if item.ok else "✗"
|
|
64
|
+
lines.append(f"{mark} {item.input_path.name}: {item.message}")
|
|
65
|
+
lines += [f" aviso: {w}" for w in item.warnings]
|
|
66
|
+
lines += [f"aviso: {w}" for w in result.warnings]
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def exit_code(result: ToolResult) -> int:
|
|
71
|
+
return 0 if _all_ok(result) else EXIT_PARTIAL
|