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.
- pseudonimizar-0.1.0/.claude/commands/pseudonimizar.md +54 -0
- pseudonimizar-0.1.0/.github/workflows/ci.yml +116 -0
- pseudonimizar-0.1.0/.gitignore +16 -0
- pseudonimizar-0.1.0/CHANGELOG.md +53 -0
- pseudonimizar-0.1.0/LICENSE +21 -0
- pseudonimizar-0.1.0/PKG-INFO +182 -0
- pseudonimizar-0.1.0/README.md +146 -0
- pseudonimizar-0.1.0/pyproject.toml +83 -0
- pseudonimizar-0.1.0/scripts/crear_gold.py +374 -0
- pseudonimizar-0.1.0/scripts/medir.py +173 -0
- pseudonimizar-0.1.0/src/pseudonimizar/__init__.py +18 -0
- pseudonimizar-0.1.0/src/pseudonimizar/cli.py +277 -0
- pseudonimizar-0.1.0/src/pseudonimizar/docx_io.py +120 -0
- pseudonimizar-0.1.0/src/pseudonimizar/engine.py +175 -0
- pseudonimizar-0.1.0/src/pseudonimizar/mapping.py +148 -0
- pseudonimizar-0.1.0/src/pseudonimizar/modelo.py +116 -0
- pseudonimizar-0.1.0/src/pseudonimizar/nlp.py +106 -0
- pseudonimizar-0.1.0/src/pseudonimizar/pdf_io.py +99 -0
- pseudonimizar-0.1.0/src/pseudonimizar/recognizers_es.py +660 -0
- pseudonimizar-0.1.0/tests/__init__.py +0 -0
- pseudonimizar-0.1.0/tests/fixtures/acta_consejo_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/acta_consejo_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/acta_junta_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/acta_junta_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/carta_encargo_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/carta_encargo_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/contrato_mercantil_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/contrato_mercantil_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/escritura_compraventa_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/escritura_compraventa_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/expediente_1.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/expediente_2.docx +0 -0
- pseudonimizar-0.1.0/tests/fixtures/gold.json +998 -0
- pseudonimizar-0.1.0/tests/test_adversarial.py +163 -0
- pseudonimizar-0.1.0/tests/test_cli.py +286 -0
- pseudonimizar-0.1.0/tests/test_docx_io.py +162 -0
- pseudonimizar-0.1.0/tests/test_e2e.py +134 -0
- pseudonimizar-0.1.0/tests/test_engine.py +350 -0
- pseudonimizar-0.1.0/tests/test_gold.py +42 -0
- pseudonimizar-0.1.0/tests/test_mapping.py +77 -0
- pseudonimizar-0.1.0/tests/test_modelo.py +139 -0
- pseudonimizar-0.1.0/tests/test_nlp.py +60 -0
- pseudonimizar-0.1.0/tests/test_pdf_io.py +223 -0
- pseudonimizar-0.1.0/tests/test_recognizers.py +491 -0
- 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"]
|