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.
Files changed (66) hide show
  1. pdf_tool_cli-1.1.0.dist-info/METADATA +179 -0
  2. pdf_tool_cli-1.1.0.dist-info/RECORD +66 -0
  3. pdf_tool_cli-1.1.0.dist-info/WHEEL +4 -0
  4. pdf_tool_cli-1.1.0.dist-info/entry_points.txt +4 -0
  5. pdf_tool_cli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. pdftool/__init__.py +3 -0
  7. pdftool/cli/__init__.py +179 -0
  8. pdftool/cli/__main__.py +5 -0
  9. pdftool/cli/args.py +138 -0
  10. pdftool/cli/report.py +71 -0
  11. pdftool/core/__init__.py +0 -0
  12. pdftool/core/atomic.py +43 -0
  13. pdftool/core/batch.py +111 -0
  14. pdftool/core/config.py +89 -0
  15. pdftool/core/errors.py +36 -0
  16. pdftool/core/jobs.py +188 -0
  17. pdftool/core/logger.py +84 -0
  18. pdftool/core/naming.py +91 -0
  19. pdftool/core/plugin.py +96 -0
  20. pdftool/core/registry.py +59 -0
  21. pdftool/core/thumbnails.py +37 -0
  22. pdftool/core/updater.py +67 -0
  23. pdftool/main.py +30 -0
  24. pdftool/tools/__init__.py +0 -0
  25. pdftool/tools/compress/__init__.py +0 -0
  26. pdftool/tools/compress/logic.py +387 -0
  27. pdftool/tools/compress/panel.py +72 -0
  28. pdftool/tools/compress/params.py +18 -0
  29. pdftool/tools/compress/spec.py +18 -0
  30. pdftool/tools/images2pdf/__init__.py +0 -0
  31. pdftool/tools/images2pdf/logic.py +127 -0
  32. pdftool/tools/images2pdf/panel.py +22 -0
  33. pdftool/tools/images2pdf/params.py +6 -0
  34. pdftool/tools/images2pdf/spec.py +19 -0
  35. pdftool/tools/merge/__init__.py +0 -0
  36. pdftool/tools/merge/logic.py +54 -0
  37. pdftool/tools/merge/panel.py +44 -0
  38. pdftool/tools/merge/params.py +20 -0
  39. pdftool/tools/merge/spec.py +19 -0
  40. pdftool/tools/protect/__init__.py +0 -0
  41. pdftool/tools/protect/logic.py +85 -0
  42. pdftool/tools/protect/panel.py +72 -0
  43. pdftool/tools/protect/params.py +18 -0
  44. pdftool/tools/protect/spec.py +18 -0
  45. pdftool/tools/split/__init__.py +0 -0
  46. pdftool/tools/split/logic.py +139 -0
  47. pdftool/tools/split/panel.py +129 -0
  48. pdftool/tools/split/params.py +28 -0
  49. pdftool/tools/split/spec.py +19 -0
  50. pdftool/tools/watermark/__init__.py +0 -0
  51. pdftool/tools/watermark/logic.py +110 -0
  52. pdftool/tools/watermark/panel.py +67 -0
  53. pdftool/tools/watermark/params.py +19 -0
  54. pdftool/tools/watermark/spec.py +19 -0
  55. pdftool/ui/__init__.py +0 -0
  56. pdftool/ui/accessibility.py +44 -0
  57. pdftool/ui/app.py +425 -0
  58. pdftool/ui/errors.py +12 -0
  59. pdftool/ui/file_list.py +275 -0
  60. pdftool/ui/logs.py +58 -0
  61. pdftool/ui/output_dir.py +127 -0
  62. pdftool/ui/panel_base.py +631 -0
  63. pdftool/ui/platform.py +30 -0
  64. pdftool/ui/registry.py +61 -0
  65. pdftool/ui/theme.py +30 -0
  66. 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
+ [![Descargar](https://img.shields.io/badge/⬇️_Descargar-última_versión-2ea44f?style=for-the-badge)](https://github.com/juanMaAV92/pdf-tool/releases/latest)
42
+
43
+ [![CI](https://github.com/juanMaAV92/pdf-tool/actions/workflows/ci.yml/badge.svg)](https://github.com/juanMaAV92/pdf-tool/actions/workflows/ci.yml)
44
+ [![Release](https://img.shields.io/github/v/release/juanMaAV92/pdf-tool?label=versión)](https://github.com/juanMaAV92/pdf-tool/releases/latest)
45
+ [![License: MIT](https://img.shields.io/badge/Licencia-MIT-blue.svg)](LICENSE)
46
+ ![Platform](https://img.shields.io/badge/macOS_·_Windows-lightgrey)
47
+
48
+ </div>
49
+
50
+ <!-- 📸 Añade aquí un screenshot o GIF de la app:
51
+ ![PDF Tool](docs/screenshot.png)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ pdf-tool=pdftool.cli:main
3
+ pdftool=pdftool.main:main
4
+
@@ -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
@@ -0,0 +1,3 @@
1
+ # La versión real la hornea CI desde el tag git al construir el release
2
+ # (ver .github/workflows/release.yml). En desarrollo local queda como dev.
3
+ __version__ = "1.1.0"
@@ -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
+ )
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from pdftool.cli import main
4
+
5
+ sys.exit(main())
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