pseudonimizar 0.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.
Files changed (45) hide show
  1. pseudonimizar-0.1.0/.claude/commands/pseudonimizar.md +54 -0
  2. pseudonimizar-0.1.0/.github/workflows/ci.yml +116 -0
  3. pseudonimizar-0.1.0/.gitignore +16 -0
  4. pseudonimizar-0.1.0/CHANGELOG.md +53 -0
  5. pseudonimizar-0.1.0/LICENSE +21 -0
  6. pseudonimizar-0.1.0/PKG-INFO +182 -0
  7. pseudonimizar-0.1.0/README.md +146 -0
  8. pseudonimizar-0.1.0/pyproject.toml +83 -0
  9. pseudonimizar-0.1.0/scripts/crear_gold.py +374 -0
  10. pseudonimizar-0.1.0/scripts/medir.py +173 -0
  11. pseudonimizar-0.1.0/src/pseudonimizar/__init__.py +18 -0
  12. pseudonimizar-0.1.0/src/pseudonimizar/cli.py +277 -0
  13. pseudonimizar-0.1.0/src/pseudonimizar/docx_io.py +120 -0
  14. pseudonimizar-0.1.0/src/pseudonimizar/engine.py +175 -0
  15. pseudonimizar-0.1.0/src/pseudonimizar/mapping.py +148 -0
  16. pseudonimizar-0.1.0/src/pseudonimizar/modelo.py +116 -0
  17. pseudonimizar-0.1.0/src/pseudonimizar/nlp.py +106 -0
  18. pseudonimizar-0.1.0/src/pseudonimizar/pdf_io.py +99 -0
  19. pseudonimizar-0.1.0/src/pseudonimizar/recognizers_es.py +660 -0
  20. pseudonimizar-0.1.0/tests/__init__.py +0 -0
  21. pseudonimizar-0.1.0/tests/fixtures/acta_consejo_1.docx +0 -0
  22. pseudonimizar-0.1.0/tests/fixtures/acta_consejo_2.docx +0 -0
  23. pseudonimizar-0.1.0/tests/fixtures/acta_junta_1.docx +0 -0
  24. pseudonimizar-0.1.0/tests/fixtures/acta_junta_2.docx +0 -0
  25. pseudonimizar-0.1.0/tests/fixtures/carta_encargo_1.docx +0 -0
  26. pseudonimizar-0.1.0/tests/fixtures/carta_encargo_2.docx +0 -0
  27. pseudonimizar-0.1.0/tests/fixtures/contrato_mercantil_1.docx +0 -0
  28. pseudonimizar-0.1.0/tests/fixtures/contrato_mercantil_2.docx +0 -0
  29. pseudonimizar-0.1.0/tests/fixtures/escritura_compraventa_1.docx +0 -0
  30. pseudonimizar-0.1.0/tests/fixtures/escritura_compraventa_2.docx +0 -0
  31. pseudonimizar-0.1.0/tests/fixtures/expediente_1.docx +0 -0
  32. pseudonimizar-0.1.0/tests/fixtures/expediente_2.docx +0 -0
  33. pseudonimizar-0.1.0/tests/fixtures/gold.json +998 -0
  34. pseudonimizar-0.1.0/tests/test_adversarial.py +163 -0
  35. pseudonimizar-0.1.0/tests/test_cli.py +286 -0
  36. pseudonimizar-0.1.0/tests/test_docx_io.py +162 -0
  37. pseudonimizar-0.1.0/tests/test_e2e.py +134 -0
  38. pseudonimizar-0.1.0/tests/test_engine.py +350 -0
  39. pseudonimizar-0.1.0/tests/test_gold.py +42 -0
  40. pseudonimizar-0.1.0/tests/test_mapping.py +77 -0
  41. pseudonimizar-0.1.0/tests/test_modelo.py +139 -0
  42. pseudonimizar-0.1.0/tests/test_nlp.py +60 -0
  43. pseudonimizar-0.1.0/tests/test_pdf_io.py +223 -0
  44. pseudonimizar-0.1.0/tests/test_recognizers.py +491 -0
  45. pseudonimizar-0.1.0/tests/test_sin_red.py +52 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ description: Pseudonimiza en local un documento (.docx/.pdf) antes de subirlo a Claude. No lee el contenido del documento.
3
+ argument-hint: ruta/al/documento.docx
4
+ ---
5
+
6
+ # Regla de la casa: pseudonimizar antes de subir
7
+
8
+ Antes de trabajar con cualquier documento real del despacho, **pásalo por la
9
+ utilidad `pseudonimizar`**, que corre **en local** y no envía nada por red. Solo
10
+ se sube a Claude la copia `.anon` resultante.
11
+
12
+ > ⚠️ **Nunca** pegues ni adjuntes el documento original en el chat. Trabaja solo
13
+ > con la copia `.anon`. Claude **ejecuta el comando, no lee el documento**: el
14
+ > contenido crudo nunca entra en el modelo.
15
+
16
+ ## Qué hacer
17
+
18
+ El usuario quiere pseudonimizar este archivo: **$ARGUMENTS**
19
+
20
+ 1. **Comprueba que `uv` está instalado** (`uv --version`). Si no:
21
+ - macOS/Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`
22
+ - Windows (PowerShell): `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
23
+
24
+ 2. **Ejecuta la herramienta sobre el archivo indicado**, sin abrir ni leer su
25
+ contenido:
26
+
27
+ ```bash
28
+ uvx --with pip pseudonimizar "$ARGUMENTS"
29
+ ```
30
+
31
+ - El `--with pip` es necesario: el entorno efímero de `uvx` necesita `pip`
32
+ para instalar el modelo de lenguaje en runtime la primera vez.
33
+ - La **primera vez** descarga Python + el paquete + el modelo de lenguaje
34
+ (~0,5 GB, una sola vez; luego funciona sin conexión). Si tarda, es normal.
35
+ - Genera una copia segura junto al original: `documento.docx` →
36
+ `documento.anon.docx` (o `.anon.txt` para PDF) e imprime un resumen de lo
37
+ sustituido.
38
+
39
+ 3. **Muestra al usuario el resumen** que imprime el comando y dile el nombre del
40
+ archivo `.anon` generado. Recuérdale el **vistazo de 10 s** a lo indirecto
41
+ (fechas señaladas, importes singulares, fincas) antes de subir la copia.
42
+
43
+ 4. **No abras, no leas ni resumas el contenido** del documento original ni del
44
+ `.anon` en este paso. Si el usuario quiere trabajar el contenido, que adjunte
45
+ **solo** la copia `.anon` en un mensaje aparte.
46
+
47
+ ## Si algo falla
48
+
49
+ - **Formato no soportado / archivo inexistente** (código 1): pide la ruta correcta
50
+ a un `.docx` o `.pdf`.
51
+ - **«PDF sin texto extraíble»** (código 1): es un PDF escaneado (imagen). Hay que
52
+ convertirlo a texto u OCR antes; la herramienta no hace OCR.
53
+ - **Sin red la primera vez**: el modelo necesita descargarse una vez. Ejecuta
54
+ `uvx --with pip pseudonimizar preparar` con conexión y reintenta.
@@ -0,0 +1,116 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ # Permite cancelar runs antiguos del mismo ref.
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ lint:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ - name: Ruff
24
+ run: uvx ruff@0.15 check src tests scripts
25
+
26
+ test:
27
+ name: test (${{ matrix.os }} · py${{ matrix.python }})
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ os: [ubuntu-latest, macos-latest, windows-latest]
32
+ python: ["3.10", "3.11", "3.12"]
33
+ runs-on: ${{ matrix.os }}
34
+ defaults:
35
+ run:
36
+ shell: bash
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - uses: astral-sh/setup-uv@v5
41
+ with:
42
+ enable-cache: true
43
+
44
+ - name: Crear entorno e instalar
45
+ run: |
46
+ uv venv --python ${{ matrix.python }}
47
+ uv pip install -e ".[dev]"
48
+
49
+ # El modelo es_core_news_lg (~0,5 GB) NO es dependencia de PyPI: se descarga
50
+ # en runtime. Lo cacheamos entre runs por versión de modelo.
51
+ - name: Cache del modelo spaCy
52
+ id: cache-modelo
53
+ uses: actions/cache@v4
54
+ with:
55
+ path: |
56
+ .venv/lib/python*/site-packages/es_core_news_lg*
57
+ .venv/Lib/site-packages/es_core_news_lg*
58
+ key: es-core-news-lg-3.8.0-${{ matrix.os }}-py${{ matrix.python }}
59
+
60
+ - name: Descargar modelo (si no está cacheado)
61
+ if: steps.cache-modelo.outputs.cache-hit != 'true'
62
+ run: uv run python -m spacy download es_core_news_lg
63
+
64
+ - name: Generar gold-set
65
+ run: uv run python scripts/crear_gold.py
66
+
67
+ - name: Pruebas (incluye umbrales gold y test_sin_red)
68
+ run: uv run pytest -q
69
+
70
+ smoke:
71
+ name: smoke uvx (${{ matrix.os }})
72
+ needs: test
73
+ strategy:
74
+ fail-fast: false
75
+ matrix:
76
+ os: [ubuntu-latest, macos-latest, windows-latest]
77
+ runs-on: ${{ matrix.os }}
78
+ defaults:
79
+ run:
80
+ shell: bash
81
+ steps:
82
+ - uses: actions/checkout@v4
83
+ - uses: astral-sh/setup-uv@v5
84
+ with:
85
+ enable-cache: true
86
+
87
+ - name: Generar un documento de prueba
88
+ run: uv run --with python-docx --python 3.11 python scripts/crear_gold.py
89
+
90
+ # Smoke real de distribución: ejecuta el paquete tal y como lo hará el aula,
91
+ # construyéndolo desde el repo. `--with pip` es necesario porque el entorno
92
+ # efímero de uvx no trae pip y el modelo se instala en runtime con pip. La
93
+ # primera ejecución descarga el modelo (~0,5 GB).
94
+ - name: uvx smoke sobre un .docx
95
+ run: |
96
+ uvx --with pip --from . pseudonimizar tests/fixtures/contrato_mercantil_1.docx
97
+ test -f tests/fixtures/contrato_mercantil_1.anon.docx
98
+ echo "OK: copia .anon.docx generada"
99
+
100
+ publish:
101
+ name: Publicar en PyPI (por tag)
102
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
103
+ needs: [lint, test, smoke]
104
+ runs-on: ubuntu-latest
105
+ environment: pypi
106
+ permissions:
107
+ id-token: write # Trusted Publishing (OIDC): sin token en secrets.
108
+ contents: read # al declarar permissions, el resto cae a `none`; sin esto
109
+ # actions/checkout no puede clonar (git exit 128 «not found»).
110
+ steps:
111
+ - uses: actions/checkout@v4
112
+ - uses: astral-sh/setup-uv@v5
113
+ - name: Construir sdist y wheel
114
+ run: uv build
115
+ - name: Publicar (Trusted Publishing)
116
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ *.anon.docx
8
+ *.anon.txt
9
+ .DS_Store
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .claude/scheduled_tasks.lock
13
+
14
+ # Documentos internos (no se publican; se conservan en local)
15
+ SPEC.md
16
+ PROMPT-BUILD.md
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ Todas las novedades destacables de `pseudonimizar`. El formato sigue
4
+ [Keep a Changelog](https://keepachangelog.com/es/1.1.0/) y el versionado es
5
+ [SemVer](https://semver.org/lang/es/).
6
+
7
+ ## [No publicado]
8
+
9
+ ### Añadido
10
+ - Tests de integración end-to-end con el modelo real (`tests/test_e2e.py`): PDF
11
+ con texto, idempotencia y CLI completa sin mocks.
12
+
13
+ ### Corregido
14
+ - **Idempotencia**: reprocesar un documento ya pseudonimizado dejaba de ser
15
+ no-op (p. ej. `D. Persona 1` → `D. Persona 1 1`, al recapturar el recognizer de
16
+ honorífico un trozo de la etiqueta). El motor descarta ahora cualquier span que
17
+ solape una etiqueta ya presente (`mapping.PATRON_ETIQUETAS`).
18
+
19
+ ## [0.1.0] — 2026-06-24
20
+
21
+ Primera versión. Pseudonimización local de documentos jurídicos en español.
22
+
23
+ ### Añadido
24
+ - CLI de un comando sobre `.docx` y `.pdf` (`pseudonimizar <archivo>`), con
25
+ subcomando `pseudonimizar preparar` para cachear el modelo en el setup.
26
+ - Detección de identificadores directos (§8): personas y sociedades (spaCy
27
+ `es_core_news_lg`), razón social con forma jurídica, direcciones, expedientes y
28
+ protocolos (recognizers anclados que sustituyen solo el dato), DNI/NIE/CIF (con
29
+ validación de letra/dígito de control), IBAN (validación módulo 97), teléfonos,
30
+ emails, matrículas y topónimos.
31
+ - Sustitución consistente intra-documento por etiquetas ficticias (`Persona N`,
32
+ `Sociedad N`, `[DNI-N]`, `[IBAN-N]`…) y resumen de lo sustituido.
33
+ - Garantía «sin red» durante el procesado, verificada por test; sin mapa en disco
34
+ (solo ida); sin telemetría.
35
+ - Las instituciones públicas (juzgados, AEAT, registros, notarías, colegios…) se
36
+ mantienen visibles a propósito.
37
+ - Round-trip de DOCX (cuerpo, tablas, cabeceras/pies); PDF → `.anon.txt` con
38
+ detección de PDF escaneado (sin texto).
39
+ - Gold-set sintético anotado (12 documentos, 114 entidades) y medidor de
40
+ precision/recall (`scripts/medir.py`). Umbrales §14 cumplidos: recall
41
+ deterministas 1.00, personas 1.00, sociedades 1.00; precision 0.96.
42
+ - Empaquetado para PyPI + `uvx`, slash command `/pseudonimizar`, CI en
43
+ Ubuntu/macOS/Windows.
44
+
45
+ ### Limitaciones conocidas
46
+ - DOCX: se pierde el formato inline parcial en párrafos modificados; cuadros de
47
+ texto y SmartArt fuera de alcance.
48
+ - PDF: solo extracción de texto (no se conserva maquetación); OCR fuera de alcance.
49
+ - No se unifican variantes de superficie de un mismo nombre (sin correferencia).
50
+ - Idioma español; lenguas cooficiales fuera de alcance en v1.
51
+
52
+ [No publicado]: https://github.com/triari-partners/pseudonimizar/compare/v0.1.0...HEAD
53
+ [0.1.0]: https://github.com/triari-partners/pseudonimizar/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Triari Partners
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.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: pseudonimizar
3
+ Version: 0.1.0
4
+ Summary: Pseudonimización local de documentos jurídicos en español. No envía nada por red.
5
+ Project-URL: Homepage, https://github.com/triari-partners/pseudonimizar
6
+ Project-URL: Repository, https://github.com/triari-partners/pseudonimizar
7
+ Project-URL: Changelog, https://github.com/triari-partners/pseudonimizar/blob/main/CHANGELOG.md
8
+ Author: Triari Partners
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: anonimizacion,español,legal,pii,pseudonimizacion,rgpd
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Legal Industry
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: Spanish
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Text Processing :: Linguistic
24
+ Requires-Python: <3.13,>=3.10
25
+ Requires-Dist: click>=8.1
26
+ Requires-Dist: presidio-analyzer>=2.2.355
27
+ Requires-Dist: pypdf>=4.0
28
+ Requires-Dist: python-docx>=1.1
29
+ Requires-Dist: regex>=2024.0
30
+ Requires-Dist: spacy<3.9,>=3.7
31
+ Requires-Dist: typer>=0.12
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # pseudonimizar
38
+
39
+ **Pseudonimización local de documentos jurídicos en español.** Sustituye los
40
+ identificadores reales de un `.docx` o `.pdf` (nombres, DNI/NIE/CIF, IBAN,
41
+ teléfonos, direcciones, sociedades…) por **etiquetas ficticias consistentes**,
42
+ de modo que el documento sea seguro para subir a un LLM de consumo sin exponer
43
+ datos de cliente.
44
+
45
+ > **El contenido del documento NUNCA sale de tu máquina por red.** La única
46
+ > conexión admitida es la descarga del modelo de lenguaje la **primera vez**.
47
+ > No se guarda ninguna tabla de correspondencia real↔ficticio en disco
48
+ > (*solo ida, sin mapa*).
49
+
50
+ ---
51
+
52
+ ## Uso en un paso
53
+
54
+ ```bash
55
+ uvx --with pip pseudonimizar contrato.docx
56
+ ```
57
+
58
+ Esto descarga y ejecuta la herramienta al vuelo (necesitas solo
59
+ [`uv`](https://docs.astral.sh/uv/), un único binario que instala Python por ti).
60
+
61
+ > **¿Por qué `--with pip`?** El modelo de lenguaje (~0,5 GB) no es una dependencia
62
+ > de PyPI (límites de tamaño): se instala en runtime la primera vez, y para eso el
63
+ > entorno efímero de `uvx` necesita `pip`. Con `--with pip` el modelo se instala
64
+ > en el entorno cacheado y **persiste**: las siguientes ejecuciones van directas
65
+ > (~1 s). El slash command `/pseudonimizar` ya añade `--with pip` por ti.
66
+
67
+ Produce una copia segura junto al original:
68
+
69
+ ```
70
+ contrato.docx → contrato.anon.docx
71
+ escritura.pdf → escritura.anon.txt
72
+ ```
73
+
74
+ y un resumen de lo sustituido:
75
+
76
+ ```
77
+ ✓ Pseudonimizado en local. Nada se ha enviado por red.
78
+ Personas ................... 4
79
+ Sociedades / orgs .......... 2
80
+ DNI ........................ 3
81
+ IBAN / cuentas ............. 1
82
+ Teléfonos .................. 2
83
+ Emails ..................... 1
84
+ Direcciones ................ 2
85
+ Copia segura: contrato.anon.docx
86
+ Recuerda: revisa 10 s lo indirecto (fechas señaladas, importes singulares, fincas).
87
+ ```
88
+
89
+ **Primera vez:** se descarga el modelo `es_core_news_lg` (~0,5 GB, una sola vez,
90
+ queda cacheado). Para dejarlo listo en un setup (p. ej. de un aula) sin esperar
91
+ en directo:
92
+
93
+ ```bash
94
+ uvx --with pip pseudonimizar preparar
95
+ ```
96
+
97
+ A partir de ahí funciona **sin conexión**.
98
+
99
+ ## Opciones
100
+
101
+ ```
102
+ pseudonimizar <archivo> [opciones]
103
+ pseudonimizar preparar # descarga/cachea el modelo, sin tocar documentos
104
+ ```
105
+
106
+ | Opción | Efecto | Defecto |
107
+ |---|---|---|
108
+ | `<archivo>` | Documento de entrada (`.docx` o `.pdf`). | — (obligatorio) |
109
+ | `--salida, -s RUTA` | Ruta de salida. | junto al original |
110
+ | `--idioma, -i CODE` | Idioma del documento. | `es` |
111
+ | `--umbral, -u FLOAT` | Umbral de confianza mínimo (bajo a propósito: mejor sobre-redactar). | `0.35` |
112
+ | `--silencioso, -q` | No imprime el resumen. | desactivado |
113
+ | `--version` / `--help` | Versión / ayuda. | — |
114
+
115
+ **Códigos de salida:** `0` ok · `1` error de uso (archivo inexistente, formato no
116
+ soportado, PDF escaneado sin texto) · `2` error interno.
117
+
118
+ ## Qué detecta
119
+
120
+ **Identificadores directos** (se redactan automáticamente):
121
+
122
+ - Nombres de **personas** y **sociedades / organizaciones** (incl. razón social
123
+ con forma jurídica: S.L., S.A., S.L.U., S.Coop., A.I.E.…).
124
+ - **DNI, NIE, CIF/NIF** (con validación de letra/dígito de control).
125
+ - **IBAN / cuentas** (con validación módulo 97).
126
+ - **Teléfonos**, **emails**, **matrículas** de vehículo.
127
+ - **Direcciones** postales, **nº de expediente/procedimiento**, **nº de protocolo**.
128
+ - **Topónimos** (ciudades, provincias) → etiqueta `Lugar N`.
129
+
130
+ Los **organismos públicos** (juzgados, AEAT, registros, notarías, colegios…) se
131
+ **mantienen visibles** a propósito: no son datos confidenciales del cliente y dan
132
+ contexto.
133
+
134
+ **Identificadores indirectos** (NO se redactan; los cubre el vistazo humano de
135
+ 10 s): fechas muy señaladas, importes singulares, datos registrales de finca,
136
+ cargos poco comunes.
137
+
138
+ ## Garantías de privacidad
139
+
140
+ - **Sin red durante el procesado.** Verificado por test (`test_sin_red`): con la
141
+ red bloqueada y el modelo cacheado, pseudonimizar un documento completa con
142
+ éxito.
143
+ - **Sin mapa en disco.** El mapeo real↔ficticio vive en memoria y se descarta al
144
+ terminar. No hay comando de «rehidratar» (decisión de diseño: *solo ida*).
145
+ - **Sin telemetría.** El paquete no llama a casa ni registra uso.
146
+ - **Consistencia por archivo.** Dentro de un documento, el mismo valor recibe
147
+ siempre la misma etiqueta; cada documento tiene su mapeo independiente.
148
+
149
+ ## Limitaciones conocidas (v1)
150
+
151
+ - **DOCX:** al modificar un párrafo se colapsa su formato inline (negritas/
152
+ cursivas parciales se pierden en ESE párrafo; los párrafos sin cambios se
153
+ conservan intactos). Cuadros de texto y SmartArt quedan fuera de alcance.
154
+ - **PDF:** se extrae el texto a `.anon.txt` (no se conserva maquetación). Un PDF
155
+ **escaneado** (sin texto) se detecta y se avisa; **OCR fuera de alcance**.
156
+ - **Variantes de un mismo nombre:** «María González García» y «Sra. González»
157
+ pueden recibir etiquetas distintas (no hay correferencia en v1).
158
+ - **Idioma:** español. Catalán/euskera/gallego fuera de alcance en v1.
159
+ - **Detección por modelo:** los nombres/organizaciones dependen de
160
+ `es_core_news_lg`; el recall es muy alto pero no perfecto. De ahí el principio
161
+ «mejor sobre-redactar» y el vistazo humano de 10 s antes de subir.
162
+
163
+ ## Desarrollo
164
+
165
+ ```bash
166
+ uv venv --python 3.11
167
+ uv pip install -e ".[dev]"
168
+ python -m spacy download es_core_news_lg # o: pseudonimizar preparar
169
+ python scripts/crear_gold.py # genera el gold-set anotado
170
+ pytest # 157 pruebas
171
+ python scripts/medir.py # tabla de precision/recall vs gold
172
+ ruff check src tests scripts
173
+ ```
174
+
175
+ Arquitectura (módulos en `src/pseudonimizar/`): `cli` (typer) · `engine`
176
+ (análisis → solapes → sustitución) · `recognizers_es` (regex deterministas) ·
177
+ `nlp` (spaCy + Presidio) · `mapping` (etiquetas consistentes) · `docx_io` /
178
+ `pdf_io` (E/S) · `modelo` (bootstrap del modelo).
179
+
180
+ ## Licencia
181
+
182
+ [MIT](LICENSE) · © 2026 Triari Partners
@@ -0,0 +1,146 @@
1
+ # pseudonimizar
2
+
3
+ **Pseudonimización local de documentos jurídicos en español.** Sustituye los
4
+ identificadores reales de un `.docx` o `.pdf` (nombres, DNI/NIE/CIF, IBAN,
5
+ teléfonos, direcciones, sociedades…) por **etiquetas ficticias consistentes**,
6
+ de modo que el documento sea seguro para subir a un LLM de consumo sin exponer
7
+ datos de cliente.
8
+
9
+ > **El contenido del documento NUNCA sale de tu máquina por red.** La única
10
+ > conexión admitida es la descarga del modelo de lenguaje la **primera vez**.
11
+ > No se guarda ninguna tabla de correspondencia real↔ficticio en disco
12
+ > (*solo ida, sin mapa*).
13
+
14
+ ---
15
+
16
+ ## Uso en un paso
17
+
18
+ ```bash
19
+ uvx --with pip pseudonimizar contrato.docx
20
+ ```
21
+
22
+ Esto descarga y ejecuta la herramienta al vuelo (necesitas solo
23
+ [`uv`](https://docs.astral.sh/uv/), un único binario que instala Python por ti).
24
+
25
+ > **¿Por qué `--with pip`?** El modelo de lenguaje (~0,5 GB) no es una dependencia
26
+ > de PyPI (límites de tamaño): se instala en runtime la primera vez, y para eso el
27
+ > entorno efímero de `uvx` necesita `pip`. Con `--with pip` el modelo se instala
28
+ > en el entorno cacheado y **persiste**: las siguientes ejecuciones van directas
29
+ > (~1 s). El slash command `/pseudonimizar` ya añade `--with pip` por ti.
30
+
31
+ Produce una copia segura junto al original:
32
+
33
+ ```
34
+ contrato.docx → contrato.anon.docx
35
+ escritura.pdf → escritura.anon.txt
36
+ ```
37
+
38
+ y un resumen de lo sustituido:
39
+
40
+ ```
41
+ ✓ Pseudonimizado en local. Nada se ha enviado por red.
42
+ Personas ................... 4
43
+ Sociedades / orgs .......... 2
44
+ DNI ........................ 3
45
+ IBAN / cuentas ............. 1
46
+ Teléfonos .................. 2
47
+ Emails ..................... 1
48
+ Direcciones ................ 2
49
+ Copia segura: contrato.anon.docx
50
+ Recuerda: revisa 10 s lo indirecto (fechas señaladas, importes singulares, fincas).
51
+ ```
52
+
53
+ **Primera vez:** se descarga el modelo `es_core_news_lg` (~0,5 GB, una sola vez,
54
+ queda cacheado). Para dejarlo listo en un setup (p. ej. de un aula) sin esperar
55
+ en directo:
56
+
57
+ ```bash
58
+ uvx --with pip pseudonimizar preparar
59
+ ```
60
+
61
+ A partir de ahí funciona **sin conexión**.
62
+
63
+ ## Opciones
64
+
65
+ ```
66
+ pseudonimizar <archivo> [opciones]
67
+ pseudonimizar preparar # descarga/cachea el modelo, sin tocar documentos
68
+ ```
69
+
70
+ | Opción | Efecto | Defecto |
71
+ |---|---|---|
72
+ | `<archivo>` | Documento de entrada (`.docx` o `.pdf`). | — (obligatorio) |
73
+ | `--salida, -s RUTA` | Ruta de salida. | junto al original |
74
+ | `--idioma, -i CODE` | Idioma del documento. | `es` |
75
+ | `--umbral, -u FLOAT` | Umbral de confianza mínimo (bajo a propósito: mejor sobre-redactar). | `0.35` |
76
+ | `--silencioso, -q` | No imprime el resumen. | desactivado |
77
+ | `--version` / `--help` | Versión / ayuda. | — |
78
+
79
+ **Códigos de salida:** `0` ok · `1` error de uso (archivo inexistente, formato no
80
+ soportado, PDF escaneado sin texto) · `2` error interno.
81
+
82
+ ## Qué detecta
83
+
84
+ **Identificadores directos** (se redactan automáticamente):
85
+
86
+ - Nombres de **personas** y **sociedades / organizaciones** (incl. razón social
87
+ con forma jurídica: S.L., S.A., S.L.U., S.Coop., A.I.E.…).
88
+ - **DNI, NIE, CIF/NIF** (con validación de letra/dígito de control).
89
+ - **IBAN / cuentas** (con validación módulo 97).
90
+ - **Teléfonos**, **emails**, **matrículas** de vehículo.
91
+ - **Direcciones** postales, **nº de expediente/procedimiento**, **nº de protocolo**.
92
+ - **Topónimos** (ciudades, provincias) → etiqueta `Lugar N`.
93
+
94
+ Los **organismos públicos** (juzgados, AEAT, registros, notarías, colegios…) se
95
+ **mantienen visibles** a propósito: no son datos confidenciales del cliente y dan
96
+ contexto.
97
+
98
+ **Identificadores indirectos** (NO se redactan; los cubre el vistazo humano de
99
+ 10 s): fechas muy señaladas, importes singulares, datos registrales de finca,
100
+ cargos poco comunes.
101
+
102
+ ## Garantías de privacidad
103
+
104
+ - **Sin red durante el procesado.** Verificado por test (`test_sin_red`): con la
105
+ red bloqueada y el modelo cacheado, pseudonimizar un documento completa con
106
+ éxito.
107
+ - **Sin mapa en disco.** El mapeo real↔ficticio vive en memoria y se descarta al
108
+ terminar. No hay comando de «rehidratar» (decisión de diseño: *solo ida*).
109
+ - **Sin telemetría.** El paquete no llama a casa ni registra uso.
110
+ - **Consistencia por archivo.** Dentro de un documento, el mismo valor recibe
111
+ siempre la misma etiqueta; cada documento tiene su mapeo independiente.
112
+
113
+ ## Limitaciones conocidas (v1)
114
+
115
+ - **DOCX:** al modificar un párrafo se colapsa su formato inline (negritas/
116
+ cursivas parciales se pierden en ESE párrafo; los párrafos sin cambios se
117
+ conservan intactos). Cuadros de texto y SmartArt quedan fuera de alcance.
118
+ - **PDF:** se extrae el texto a `.anon.txt` (no se conserva maquetación). Un PDF
119
+ **escaneado** (sin texto) se detecta y se avisa; **OCR fuera de alcance**.
120
+ - **Variantes de un mismo nombre:** «María González García» y «Sra. González»
121
+ pueden recibir etiquetas distintas (no hay correferencia en v1).
122
+ - **Idioma:** español. Catalán/euskera/gallego fuera de alcance en v1.
123
+ - **Detección por modelo:** los nombres/organizaciones dependen de
124
+ `es_core_news_lg`; el recall es muy alto pero no perfecto. De ahí el principio
125
+ «mejor sobre-redactar» y el vistazo humano de 10 s antes de subir.
126
+
127
+ ## Desarrollo
128
+
129
+ ```bash
130
+ uv venv --python 3.11
131
+ uv pip install -e ".[dev]"
132
+ python -m spacy download es_core_news_lg # o: pseudonimizar preparar
133
+ python scripts/crear_gold.py # genera el gold-set anotado
134
+ pytest # 157 pruebas
135
+ python scripts/medir.py # tabla de precision/recall vs gold
136
+ ruff check src tests scripts
137
+ ```
138
+
139
+ Arquitectura (módulos en `src/pseudonimizar/`): `cli` (typer) · `engine`
140
+ (análisis → solapes → sustitución) · `recognizers_es` (regex deterministas) ·
141
+ `nlp` (spaCy + Presidio) · `mapping` (etiquetas consistentes) · `docx_io` /
142
+ `pdf_io` (E/S) · `modelo` (bootstrap del modelo).
143
+
144
+ ## Licencia
145
+
146
+ [MIT](LICENSE) · © 2026 Triari Partners
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "pseudonimizar"
3
+ version = "0.1.0"
4
+ description = "Pseudonimización local de documentos jurídicos en español. No envía nada por red."
5
+ readme = "README.md"
6
+ # Cota superior <3.13: spaCy 3.8 / sus deps (numpy, blis) no traen wheels
7
+ # fiables para 3.13. uvx elegirá (o descargará) 3.10-3.12, evitando builds desde
8
+ # fuente que fallarían en el portátil del aula.
9
+ requires-python = ">=3.10,<3.13"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Triari Partners" }]
12
+ keywords = ["pii", "anonimizacion", "pseudonimizacion", "rgpd", "legal", "español"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Legal Industry",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Natural Language :: Spanish",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Text Processing :: Linguistic",
25
+ "Topic :: Security",
26
+ ]
27
+ dependencies = [
28
+ "presidio-analyzer>=2.2.355",
29
+ "spacy>=3.7,<3.9",
30
+ "python-docx>=1.1",
31
+ "pypdf>=4.0",
32
+ "typer>=0.12",
33
+ # spaCy importa su CLI en el __init__ (descarga del modelo) y este requiere
34
+ # click; typer >=0.26 ya no lo arrastra de forma transitiva, así que lo
35
+ # fijamos explícitamente para que `import spacy` y `spacy download` funcionen.
36
+ "click>=8.1",
37
+ # recognizers_es.py usa el módulo `regex` (propiedades unicode \p{Lu}…) en los
38
+ # recognizers anclados; no lo cubre el `re` estándar. Es dependencia directa.
39
+ "regex>=2024.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/triari-partners/pseudonimizar"
44
+ Repository = "https://github.com/triari-partners/pseudonimizar"
45
+ Changelog = "https://github.com/triari-partners/pseudonimizar/blob/main/CHANGELOG.md"
46
+
47
+ [project.optional-dependencies]
48
+ dev = [
49
+ "pytest>=8.0",
50
+ "ruff>=0.6",
51
+ ]
52
+
53
+ [project.scripts]
54
+ pseudonimizar = "pseudonimizar.cli:run"
55
+
56
+ [build-system]
57
+ requires = ["hatchling"]
58
+ build-backend = "hatchling.build"
59
+
60
+ [tool.hatch.build.targets.wheel]
61
+ packages = ["src/pseudonimizar"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ addopts = "-q"
66
+ markers = [
67
+ "red: pruebas que verifican el aislamiento de red (sin sockets)",
68
+ "gold: métricas precision/recall contra el gold-set",
69
+ "e2e: integración end-to-end con el modelo real (docx/pdf/cli)",
70
+ ]
71
+
72
+ [tool.ruff]
73
+ line-length = 100
74
+ target-version = "py310"
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E", "F", "I", "W", "UP", "B"]
78
+ ignore = ["E501"]
79
+
80
+ [tool.ruff.lint.flake8-bugbear]
81
+ # typer requiere typer.Option()/Argument() como valor por defecto del parámetro;
82
+ # no es una llamada mutable en default al uso (B008 es falso positivo aquí).
83
+ extend-immutable-calls = ["typer.Option", "typer.Argument"]