gdpr-pseudonymizer 2.1.3__tar.gz → 2.2.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.
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/PKG-INFO +13 -8
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/README.md +11 -6
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/batch.py +2 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/process.py +2 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/config.py +19 -2
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/main.py +6 -2
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/validators.py +17 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/core/document_processor.py +21 -17
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/models.py +1 -6
- gdpr_pseudonymizer-2.2.0/gdpr_pseudonymizer/locale/fr/LC_MESSAGES/cli.mo +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/locale/fr/LC_MESSAGES/cli.po +2 -2
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/hybrid_detector.py +95 -4
- gdpr_pseudonymizer-2.2.0/gdpr_pseudonymizer/nlp/model_names.py +46 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/regex_matcher.py +31 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/detection_patterns.yaml +12 -7
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/models.py +32 -1
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/ui.py +7 -2
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/workflow.py +51 -2
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/pyproject.toml +2 -2
- gdpr_pseudonymizer-2.1.3/gdpr_pseudonymizer/locale/fr/LC_MESSAGES/cli.mo +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/LICENSE +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/config_show.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/delete_mapping.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/destroy_table.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/export.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/import_mappings.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/init.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/list_entities.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/list_mappings.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/stats.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/validate_mappings.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/formatters.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/i18n.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/passphrase.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/progress.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/validation_stub.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/core/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/core/naive_processor.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/database.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/encryption.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/naive_data.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/repositories/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/repositories/audit_repository.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/repositories/mapping_repository.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/data/repositories/metadata_repository.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/exceptions.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/accessibility/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/accessibility/focus_manager.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/accessibility/shortcuts.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/app.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/config.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/dialogs/shortcuts_help.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/error_handler.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/en.qm +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/en.ts +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/fr.qm +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/fr.ts +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/i18n/translator.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/main_window.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/models/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/models/validation_state.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/app.ico +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/icon_16.png +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/icon_256.png +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/icon_32.png +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/icon_48.png +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/icons/icon_512.png +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/themes/dark.qss +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/themes/high-contrast.qss +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/resources/themes/light.qss +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/batch.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/database.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/home.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/processing.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/results.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/settings.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/stub.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/screens/validation.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/confirm_dialog.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/drop_zone.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/entity_editor.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/entity_panel.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/passphrase_dialog.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/step_indicator.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/widgets/toast.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/batch_worker.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/database_worker.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/detection_worker.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/finalization_worker.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/model_manager.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/processing_worker.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/gui/workers/signals.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/locale/en/LC_MESSAGES/cli.mo +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/locale/en/LC_MESSAGES/cli.po +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/entity_detector.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/entity_grouping.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/geography_dictionary.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/name_dictionary.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/spacy_detector.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/stanza_detector.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/pseudonym/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/pseudonym/assignment_engine.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/pseudonym/gender_detector.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/pseudonym/library_manager.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/pseudonym/neutral_id_generator.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/french_gender_lookup.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/french_geography.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/french_names.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/pseudonyms/lotr.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/pseudonyms/neutral.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/resources/pseudonyms/star_wars.json +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/config_manager.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/file_handler.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/french_patterns.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/logger.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/tabular_reader.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/utils/tabular_writer.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/__init__.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/actions.py +0 -0
- {gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/validation/context_precomputer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gdpr-pseudonymizer
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: CLI and GUI tool for GDPR-compliant pseudonymization of French text documents using NLP-based entity detection and reversible mapping
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -30,7 +30,7 @@ Provides-Extra: gui
|
|
|
30
30
|
Provides-Extra: pdf
|
|
31
31
|
Requires-Dist: PySide6-Essentials (>=6.7.0,<6.8.0) ; (python_version >= "3.10" and python_version < "3.13") and (extra == "gui" or extra == "all")
|
|
32
32
|
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
33
|
-
Requires-Dist: SQLAlchemy (>=2.0.0,<
|
|
33
|
+
Requires-Dist: SQLAlchemy (>=2.0.0,<2.1.0)
|
|
34
34
|
Requires-Dist: click (>=8.0.0,<8.2.0)
|
|
35
35
|
Requires-Dist: cryptography (>=50.0.1,<51.0.0)
|
|
36
36
|
Requires-Dist: markdown-it-py (>=3.0.0,<4.0.0)
|
|
@@ -85,10 +85,14 @@ Pre-built standalone executables are available for Windows, macOS, and Linux. No
|
|
|
85
85
|
|
|
86
86
|
| Platform | File | Notes |
|
|
87
87
|
|----------|------|-------|
|
|
88
|
-
| **Windows** | `gdpr-pseudonymizer-2.1.
|
|
89
|
-
| **macOS (Apple Silicon)** | `gdpr-pseudonymizer-2.1.
|
|
90
|
-
| **
|
|
91
|
-
|
|
88
|
+
| **Windows** | `gdpr-pseudonymizer-2.1.3-windows-setup.exe` | Run the installer. Adds Start Menu shortcut. |
|
|
89
|
+
| **macOS (Apple Silicon)** | `gdpr-pseudonymizer-2.1.3-macos-arm64.dmg` | Open DMG, drag to Applications. |
|
|
90
|
+
| **Linux** | `gdpr-pseudonymizer-2.1.3-linux.AppImage` | `chmod +x` then run. |
|
|
91
|
+
|
|
92
|
+
> **Intel Macs:** no DMG is published. GitHub retired its Intel macOS runners, and an
|
|
93
|
+
> Apple Silicon runner cannot cross-build a working x86_64 bundle. Install with
|
|
94
|
+
> `pip install gdpr-pseudonymizer` instead — the CLI and GUI both work on Intel.
|
|
95
|
+
|
|
92
96
|
|
|
93
97
|
### Platform Notes
|
|
94
98
|
|
|
@@ -370,7 +374,8 @@ After comprehensive benchmarking on 25 French interview/business documents (1,73
|
|
|
370
374
|
|----------|----------|-----------|--------|-------|
|
|
371
375
|
| **spaCy only** `fr_core_news_lg` | 29.5% | 27.0% | 32.7% | Story 1.2 baseline |
|
|
372
376
|
| **Hybrid** (spaCy + regex) | 59.97% | 48.17% | 79.45% | Story 5.3 |
|
|
373
|
-
| **Hybrid + expanded patterns** | 31.79% |
|
|
377
|
+
| **Hybrid + expanded patterns** | 31.79% | 25.38% | 42.54% | Story 7.5 |
|
|
378
|
+
| **Span-bleed fixes** | 32.34% | 26.75% | 40.88% | 2026-09-29 (current) — recall drop is a ground-truth artefact, see QA report |
|
|
374
379
|
|
|
375
380
|
**Accuracy trajectory:** spaCy-only baseline → hybrid approach with annotation cleanup, expanded regex patterns, and French geography dictionary doubled F1 score. Story 7.5 added 12 ORG pattern keywords, POS-tag disambiguation for geography matching, and 7 international locations — reducing LOCATION false-negative rate from 27.42% to 12.90%.
|
|
376
381
|
|
|
@@ -389,7 +394,7 @@ The validation UI provides an intuitive keyboard-driven interface for reviewing
|
|
|
389
394
|
- ✅ **Entity-by-type grouping** - Review PERSON → ORG → LOCATION in logical order
|
|
390
395
|
- ✅ **Context display** - See 10 words before/after each entity with highlighting
|
|
391
396
|
- ✅ **Confidence scores** - Color-coded confidence from spaCy NER (green >80%, yellow 60-80%, red <60%)
|
|
392
|
-
- ✅ **Keyboard shortcuts** - Single-key actions: [Space] Confirm, [R] Reject, [E] Modify, [A] Add, [C] Change pseudonym
|
|
397
|
+
- ✅ **Keyboard shortcuts** - Single-key actions: [Space] Confirm, [R] Reject, [E] Modify, [T] Change type, [A] Add, [C] Change pseudonym
|
|
393
398
|
- ✅ **Batch operations** - Accept/reject all entities of a type at once (Shift+A/R) with entity count feedback
|
|
394
399
|
- ✅ **Context cycling indicator** - Dot indicator (`● ○ ○ ○ ○`) shows current context position; `[Press X to cycle]` hint improves discoverability
|
|
395
400
|
- ✅ **Help overlay** - Press [H] for full command reference
|
|
@@ -36,10 +36,14 @@ Pre-built standalone executables are available for Windows, macOS, and Linux. No
|
|
|
36
36
|
|
|
37
37
|
| Platform | File | Notes |
|
|
38
38
|
|----------|------|-------|
|
|
39
|
-
| **Windows** | `gdpr-pseudonymizer-2.1.
|
|
40
|
-
| **macOS (Apple Silicon)** | `gdpr-pseudonymizer-2.1.
|
|
41
|
-
| **
|
|
42
|
-
|
|
39
|
+
| **Windows** | `gdpr-pseudonymizer-2.1.3-windows-setup.exe` | Run the installer. Adds Start Menu shortcut. |
|
|
40
|
+
| **macOS (Apple Silicon)** | `gdpr-pseudonymizer-2.1.3-macos-arm64.dmg` | Open DMG, drag to Applications. |
|
|
41
|
+
| **Linux** | `gdpr-pseudonymizer-2.1.3-linux.AppImage` | `chmod +x` then run. |
|
|
42
|
+
|
|
43
|
+
> **Intel Macs:** no DMG is published. GitHub retired its Intel macOS runners, and an
|
|
44
|
+
> Apple Silicon runner cannot cross-build a working x86_64 bundle. Install with
|
|
45
|
+
> `pip install gdpr-pseudonymizer` instead — the CLI and GUI both work on Intel.
|
|
46
|
+
|
|
43
47
|
|
|
44
48
|
### Platform Notes
|
|
45
49
|
|
|
@@ -321,7 +325,8 @@ After comprehensive benchmarking on 25 French interview/business documents (1,73
|
|
|
321
325
|
|----------|----------|-----------|--------|-------|
|
|
322
326
|
| **spaCy only** `fr_core_news_lg` | 29.5% | 27.0% | 32.7% | Story 1.2 baseline |
|
|
323
327
|
| **Hybrid** (spaCy + regex) | 59.97% | 48.17% | 79.45% | Story 5.3 |
|
|
324
|
-
| **Hybrid + expanded patterns** | 31.79% |
|
|
328
|
+
| **Hybrid + expanded patterns** | 31.79% | 25.38% | 42.54% | Story 7.5 |
|
|
329
|
+
| **Span-bleed fixes** | 32.34% | 26.75% | 40.88% | 2026-09-29 (current) — recall drop is a ground-truth artefact, see QA report |
|
|
325
330
|
|
|
326
331
|
**Accuracy trajectory:** spaCy-only baseline → hybrid approach with annotation cleanup, expanded regex patterns, and French geography dictionary doubled F1 score. Story 7.5 added 12 ORG pattern keywords, POS-tag disambiguation for geography matching, and 7 international locations — reducing LOCATION false-negative rate from 27.42% to 12.90%.
|
|
327
332
|
|
|
@@ -340,7 +345,7 @@ The validation UI provides an intuitive keyboard-driven interface for reviewing
|
|
|
340
345
|
- ✅ **Entity-by-type grouping** - Review PERSON → ORG → LOCATION in logical order
|
|
341
346
|
- ✅ **Context display** - See 10 words before/after each entity with highlighting
|
|
342
347
|
- ✅ **Confidence scores** - Color-coded confidence from spaCy NER (green >80%, yellow 60-80%, red <60%)
|
|
343
|
-
- ✅ **Keyboard shortcuts** - Single-key actions: [Space] Confirm, [R] Reject, [E] Modify, [A] Add, [C] Change pseudonym
|
|
348
|
+
- ✅ **Keyboard shortcuts** - Single-key actions: [Space] Confirm, [R] Reject, [E] Modify, [T] Change type, [A] Add, [C] Change pseudonym
|
|
344
349
|
- ✅ **Batch operations** - Accept/reject all entities of a type at once (Shift+A/R) with entity count feedback
|
|
345
350
|
- ✅ **Context cycling indicator** - Dot indicator (`● ○ ○ ○ ○`) shows current context position; `[Press X to cycle]` hint improves discoverability
|
|
346
351
|
- ✅ **Help overlay** - Press [H] for full command reference
|
{gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/batch.py
RENAMED
|
@@ -37,6 +37,7 @@ from gdpr_pseudonymizer.cli.progress import ETAColumn, ProgressTracker
|
|
|
37
37
|
from gdpr_pseudonymizer.cli.validators import (
|
|
38
38
|
ensure_database,
|
|
39
39
|
parse_entity_type_filter,
|
|
40
|
+
validate_model_or_exit,
|
|
40
41
|
validate_theme_or_exit,
|
|
41
42
|
)
|
|
42
43
|
from gdpr_pseudonymizer.core.document_processor import DocumentProcessor
|
|
@@ -438,6 +439,7 @@ def batch_command(
|
|
|
438
439
|
|
|
439
440
|
# Validate theme
|
|
440
441
|
validate_theme_or_exit(effective_theme)
|
|
442
|
+
validate_model_or_exit(effective_model)
|
|
441
443
|
|
|
442
444
|
# Get passphrase
|
|
443
445
|
resolved_passphrase = resolve_passphrase(
|
{gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/cli/commands/process.py
RENAMED
|
@@ -25,6 +25,7 @@ from gdpr_pseudonymizer.cli.passphrase import resolve_passphrase
|
|
|
25
25
|
from gdpr_pseudonymizer.cli.validators import (
|
|
26
26
|
ensure_database,
|
|
27
27
|
parse_entity_type_filter,
|
|
28
|
+
validate_model_or_exit,
|
|
28
29
|
validate_theme_or_exit,
|
|
29
30
|
)
|
|
30
31
|
from gdpr_pseudonymizer.core.document_processor import DocumentProcessor
|
|
@@ -161,6 +162,7 @@ def process_command(
|
|
|
161
162
|
|
|
162
163
|
# Validate theme
|
|
163
164
|
validate_theme_or_exit(effective_theme)
|
|
165
|
+
validate_model_or_exit(effective_model)
|
|
164
166
|
|
|
165
167
|
# Get passphrase (with warning if --passphrase flag used)
|
|
166
168
|
passphrase = resolve_passphrase(cli_passphrase=passphrase)
|
|
@@ -22,6 +22,7 @@ from gdpr_pseudonymizer.exceptions import (
|
|
|
22
22
|
ConfigValidationError,
|
|
23
23
|
PassphraseInConfigError,
|
|
24
24
|
)
|
|
25
|
+
from gdpr_pseudonymizer.nlp.model_names import is_spacy_package_name
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@dataclass
|
|
@@ -73,6 +74,21 @@ VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR"]
|
|
|
73
74
|
VALID_MODELS = ["spacy"]
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
def is_valid_model(model: str) -> bool:
|
|
78
|
+
"""Check if a model value is accepted.
|
|
79
|
+
|
|
80
|
+
"spacy" selects the default French model; any well-formed spaCy package
|
|
81
|
+
name (e.g., "en_core_web_trf") selects that model.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
model: Model value from config or CLI
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if the value is accepted
|
|
88
|
+
"""
|
|
89
|
+
return model in VALID_MODELS or is_spacy_package_name(model)
|
|
90
|
+
|
|
91
|
+
|
|
76
92
|
def get_default_config() -> AppConfig:
|
|
77
93
|
"""Get default application configuration.
|
|
78
94
|
|
|
@@ -117,10 +133,11 @@ def validate_config_dict(config_dict: dict[str, Any], source: str = "config") ->
|
|
|
117
133
|
)
|
|
118
134
|
|
|
119
135
|
model = pseudonymization.get("model")
|
|
120
|
-
if model is not None and
|
|
136
|
+
if model is not None and not is_valid_model(str(model)):
|
|
121
137
|
raise ConfigValidationError(
|
|
122
138
|
f"Invalid model '{model}' in {source}. "
|
|
123
|
-
f"Valid models: {', '.join(VALID_MODELS)}"
|
|
139
|
+
f"Valid models: {', '.join(VALID_MODELS)}, "
|
|
140
|
+
f"or a spaCy package name (e.g. en_core_web_trf)"
|
|
124
141
|
)
|
|
125
142
|
|
|
126
143
|
# Validate logging level
|
|
@@ -220,7 +220,9 @@ def _process(
|
|
|
220
220
|
None,
|
|
221
221
|
"--model",
|
|
222
222
|
"-m",
|
|
223
|
-
help=_(
|
|
223
|
+
help=_(
|
|
224
|
+
"NLP model: 'spacy' (default French model) or a spaCy package name, e.g. en_core_web_trf. Default from config."
|
|
225
|
+
),
|
|
224
226
|
),
|
|
225
227
|
db_path: Optional[str] = typer.Option(
|
|
226
228
|
None,
|
|
@@ -287,7 +289,9 @@ def _batch(
|
|
|
287
289
|
None,
|
|
288
290
|
"--model",
|
|
289
291
|
"-m",
|
|
290
|
-
help=_(
|
|
292
|
+
help=_(
|
|
293
|
+
"NLP model: 'spacy' (default French model) or a spaCy package name, e.g. en_core_web_trf. Default from config."
|
|
294
|
+
),
|
|
291
295
|
),
|
|
292
296
|
db_path: Optional[str] = typer.Option(
|
|
293
297
|
None,
|
|
@@ -12,6 +12,7 @@ from typing import Optional
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
14
14
|
|
|
15
|
+
from gdpr_pseudonymizer.cli.config import VALID_MODELS, is_valid_model
|
|
15
16
|
from gdpr_pseudonymizer.cli.formatters import (
|
|
16
17
|
ErrorCode,
|
|
17
18
|
format_error_message,
|
|
@@ -302,6 +303,22 @@ def validate_theme_or_exit(theme: str) -> None:
|
|
|
302
303
|
sys.exit(1)
|
|
303
304
|
|
|
304
305
|
|
|
306
|
+
def validate_model_or_exit(model: str) -> None:
|
|
307
|
+
"""Validate NLP model value and exit(1) if invalid.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
model: "spacy" or a spaCy package name (e.g., "en_core_web_trf")
|
|
311
|
+
"""
|
|
312
|
+
if not is_valid_model(model):
|
|
313
|
+
format_error_message(
|
|
314
|
+
"Invalid Model",
|
|
315
|
+
f"Model '{model}' is not recognized.",
|
|
316
|
+
f"Use {', '.join(VALID_MODELS)} (default French model) or a spaCy "
|
|
317
|
+
f"package name, e.g. en_core_web_trf",
|
|
318
|
+
)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
305
322
|
def ensure_database(db_path: str, passphrase: str, console: Console) -> None:
|
|
306
323
|
"""Initialize the database if it does not exist, with a progress spinner.
|
|
307
324
|
|
{gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/core/document_processor.py
RENAMED
|
@@ -27,6 +27,7 @@ from gdpr_pseudonymizer.data.repositories.mapping_repository import (
|
|
|
27
27
|
)
|
|
28
28
|
from gdpr_pseudonymizer.nlp.entity_detector import DetectedEntity
|
|
29
29
|
from gdpr_pseudonymizer.nlp.hybrid_detector import HybridDetector
|
|
30
|
+
from gdpr_pseudonymizer.nlp.model_names import resolve_spacy_model
|
|
30
31
|
from gdpr_pseudonymizer.pseudonym.assignment_engine import (
|
|
31
32
|
CompositionalPseudonymEngine,
|
|
32
33
|
)
|
|
@@ -132,7 +133,8 @@ class DocumentProcessor:
|
|
|
132
133
|
db_path: Path to SQLite database file
|
|
133
134
|
passphrase: Encryption passphrase for database access
|
|
134
135
|
theme: Pseudonym library theme (neutral/star_wars/lotr)
|
|
135
|
-
model_name: NLP model
|
|
136
|
+
model_name: NLP model: "spacy" (default French model) or a spaCy
|
|
137
|
+
package name (e.g., "en_core_web_trf")
|
|
136
138
|
notifier: Optional callback for user-facing messages.
|
|
137
139
|
Decouples core from CLI presentation layer.
|
|
138
140
|
|
|
@@ -162,7 +164,9 @@ class DocumentProcessor:
|
|
|
162
164
|
OSError: If spaCy model not installed
|
|
163
165
|
"""
|
|
164
166
|
if self._detector is None:
|
|
165
|
-
self._detector = HybridDetector(
|
|
167
|
+
self._detector = HybridDetector(
|
|
168
|
+
default_model=resolve_spacy_model(self.model_name)
|
|
169
|
+
)
|
|
166
170
|
return self._detector
|
|
167
171
|
|
|
168
172
|
def _detect_and_filter_entities(
|
|
@@ -1100,21 +1104,21 @@ class DocumentProcessor:
|
|
|
1100
1104
|
Returns:
|
|
1101
1105
|
Model version (e.g., "fr_core_news_lg-3.8.0")
|
|
1102
1106
|
"""
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
return f"{
|
|
1107
|
+
model = resolve_spacy_model(self.model_name)
|
|
1108
|
+
try:
|
|
1109
|
+
info = self._get_detector().spacy_detector.get_model_info()
|
|
1110
|
+
if isinstance(info, dict) and "version" in info:
|
|
1111
|
+
# spaCy meta names omit the language prefix ("core_news_lg")
|
|
1112
|
+
lang, name = info.get("language", ""), info.get("name", "")
|
|
1113
|
+
if lang and not name.startswith(f"{lang}_"):
|
|
1114
|
+
name = f"{lang}_{name}"
|
|
1115
|
+
return f"{name}-{info['version']}"
|
|
1116
|
+
except (OSError, AttributeError, ImportError) as e:
|
|
1117
|
+
logger.warning(
|
|
1118
|
+
"model_version_unavailable",
|
|
1119
|
+
error_type=type(e).__name__,
|
|
1120
|
+
)
|
|
1121
|
+
return f"{model}-unknown"
|
|
1118
1122
|
|
|
1119
1123
|
def _log_failed_operation(
|
|
1120
1124
|
self,
|
|
@@ -45,7 +45,6 @@ class Entity(Base):
|
|
|
45
45
|
DateTime,
|
|
46
46
|
nullable=False,
|
|
47
47
|
default=datetime.utcnow,
|
|
48
|
-
insert_default=datetime.utcnow,
|
|
49
48
|
)
|
|
50
49
|
gender: Mapped[str | None] = mapped_column(
|
|
51
50
|
String, nullable=True
|
|
@@ -54,9 +53,7 @@ class Entity(Base):
|
|
|
54
53
|
theme: Mapped[str] = mapped_column(String, nullable=False) # neutral/star_wars/lotr
|
|
55
54
|
|
|
56
55
|
# Validation support fields
|
|
57
|
-
is_ambiguous: Mapped[bool] = mapped_column(
|
|
58
|
-
Boolean, nullable=False, default=False, insert_default=False
|
|
59
|
-
)
|
|
56
|
+
is_ambiguous: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
60
57
|
ambiguity_reason: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
61
58
|
|
|
62
59
|
def __init__(self, **kwargs: Any) -> None:
|
|
@@ -84,7 +81,6 @@ class Operation(Base):
|
|
|
84
81
|
DateTime,
|
|
85
82
|
nullable=False,
|
|
86
83
|
default=datetime.utcnow,
|
|
87
|
-
insert_default=datetime.utcnow,
|
|
88
84
|
)
|
|
89
85
|
operation_type: Mapped[str] = mapped_column(
|
|
90
86
|
String, nullable=False
|
|
@@ -132,7 +128,6 @@ class Metadata(Base):
|
|
|
132
128
|
DateTime,
|
|
133
129
|
nullable=False,
|
|
134
130
|
default=datetime.utcnow,
|
|
135
|
-
insert_default=datetime.utcnow,
|
|
136
131
|
onupdate=datetime.utcnow,
|
|
137
132
|
)
|
|
138
133
|
|
|
Binary file
|
|
@@ -64,8 +64,8 @@ msgstr "Chemin du fichier de sortie (par défaut : <entrée>_pseudonymized.ext)"
|
|
|
64
64
|
msgid "Pseudonym library theme (neutral/star_wars/lotr). Default from config."
|
|
65
65
|
msgstr "Thème de la bibliothèque de pseudonymes (neutral/star_wars/lotr). Par défaut depuis la configuration."
|
|
66
66
|
|
|
67
|
-
msgid "NLP model
|
|
68
|
-
msgstr "
|
|
67
|
+
msgid "NLP model: 'spacy' (default French model) or a spaCy package name, e.g. en_core_web_trf. Default from config."
|
|
68
|
+
msgstr "Modèle NLP : « spacy » (modèle français par défaut) ou un nom de paquet spaCy, p. ex. en_core_web_trf. Par défaut depuis la configuration."
|
|
69
69
|
|
|
70
70
|
msgid "Database file path. Default from config."
|
|
71
71
|
msgstr "Chemin du fichier de base de données. Par défaut depuis la configuration."
|
{gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/hybrid_detector.py
RENAMED
|
@@ -10,7 +10,10 @@ Implements EntityDetector interface using a hybrid approach:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import re
|
|
14
|
+
|
|
13
15
|
from gdpr_pseudonymizer.nlp.entity_detector import DetectedEntity, EntityDetector
|
|
16
|
+
from gdpr_pseudonymizer.nlp.model_names import DEFAULT_SPACY_MODEL
|
|
14
17
|
from gdpr_pseudonymizer.nlp.regex_matcher import RegexMatcher
|
|
15
18
|
from gdpr_pseudonymizer.nlp.spacy_detector import SpaCyDetector
|
|
16
19
|
from gdpr_pseudonymizer.utils.french_patterns import strip_french_titles
|
|
@@ -38,10 +41,16 @@ class HybridDetector(EntityDetector):
|
|
|
38
41
|
regex_matcher: RegexMatcher instance for pattern-based detection
|
|
39
42
|
"""
|
|
40
43
|
|
|
41
|
-
def __init__(self) -> None:
|
|
42
|
-
"""Initialize hybrid detector with spaCy and regex components.
|
|
44
|
+
def __init__(self, default_model: str = DEFAULT_SPACY_MODEL) -> None:
|
|
45
|
+
"""Initialize hybrid detector with spaCy and regex components.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
default_model: spaCy model loaded lazily on first detection when
|
|
49
|
+
load_model() was not called (e.g., "en_core_web_trf")
|
|
50
|
+
"""
|
|
43
51
|
self.spacy_detector = SpaCyDetector()
|
|
44
52
|
self.regex_matcher = RegexMatcher()
|
|
53
|
+
self.default_model = default_model
|
|
45
54
|
self._model_loaded = False
|
|
46
55
|
|
|
47
56
|
def load_model(self, model_name: str) -> None:
|
|
@@ -87,14 +96,20 @@ class HybridDetector(EntityDetector):
|
|
|
87
96
|
|
|
88
97
|
if not self._model_loaded:
|
|
89
98
|
# Lazy load with default model
|
|
90
|
-
logger.warning(
|
|
91
|
-
|
|
99
|
+
logger.warning(
|
|
100
|
+
"hybrid_detector_lazy_loading_model", model=self.default_model
|
|
101
|
+
)
|
|
102
|
+
self.load_model(self.default_model)
|
|
92
103
|
|
|
93
104
|
# Step 1: spaCy NER
|
|
94
105
|
spacy_entities = self.spacy_detector.detect_entities(text)
|
|
95
106
|
for entity in spacy_entities:
|
|
96
107
|
entity.source = "spacy"
|
|
97
108
|
|
|
109
|
+
# Trim non-name tokens (timestamps, punctuation) off span edges before
|
|
110
|
+
# merging, so dedup and mapping lookup see the bare name
|
|
111
|
+
spacy_entities = self._trim_entity_boundaries(spacy_entities)
|
|
112
|
+
|
|
98
113
|
logger.debug("spacy_detection_complete", entities_found=len(spacy_entities))
|
|
99
114
|
|
|
100
115
|
# Step 2: Regex pattern matching (pass spaCy Doc for POS disambiguation)
|
|
@@ -236,6 +251,82 @@ class HybridDetector(EntityDetector):
|
|
|
236
251
|
|
|
237
252
|
return False
|
|
238
253
|
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _is_edge_junk_token(token: str, entity_type: str) -> bool:
|
|
256
|
+
"""Check if a span-edge token cannot be part of an entity name.
|
|
257
|
+
|
|
258
|
+
Tokens with letters are always kept. PERSON names never contain
|
|
259
|
+
letterless tokens, so any is junk. ORG/LOCATION keep bare numbers
|
|
260
|
+
("Studio 54", "Paris 2024") and only lose timestamps and punctuation.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
token: Whitespace-delimited token at the edge of an entity span
|
|
264
|
+
entity_type: PERSON, LOCATION, or ORG
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if the token should be trimmed off the span
|
|
268
|
+
"""
|
|
269
|
+
if any(char.isalpha() for char in token):
|
|
270
|
+
return False
|
|
271
|
+
if entity_type == "PERSON":
|
|
272
|
+
return True
|
|
273
|
+
is_timestamp = ":" in token and any(char.isdigit() for char in token)
|
|
274
|
+
is_punctuation = not any(char.isalnum() for char in token)
|
|
275
|
+
return is_timestamp or is_punctuation
|
|
276
|
+
|
|
277
|
+
def _trim_entity_boundaries(
|
|
278
|
+
self, entities: list[DetectedEntity]
|
|
279
|
+
) -> list[DetectedEntity]:
|
|
280
|
+
"""Trim timestamps and punctuation that NER models glue to entity spans.
|
|
281
|
+
|
|
282
|
+
Transformer models (e.g. en_core_web_trf) often draw spans too wide on
|
|
283
|
+
transcripts: "Aino Virtanen 24:85:01" comes back as one PERSON.
|
|
284
|
+
Since mappings are keyed on the full entity text, every timestamp would
|
|
285
|
+
otherwise create a new entity with its own pseudonym.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
entities: List of detected entities
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Entities with edge junk trimmed; entities that are only junk are dropped
|
|
292
|
+
"""
|
|
293
|
+
trimmed = []
|
|
294
|
+
for entity in entities:
|
|
295
|
+
tokens = list(re.finditer(r"\S+", entity.text))
|
|
296
|
+
first, last = 0, len(tokens) - 1
|
|
297
|
+
while first <= last and self._is_edge_junk_token(
|
|
298
|
+
tokens[first].group(), entity.entity_type
|
|
299
|
+
):
|
|
300
|
+
first += 1
|
|
301
|
+
while last >= first and self._is_edge_junk_token(
|
|
302
|
+
tokens[last].group(), entity.entity_type
|
|
303
|
+
):
|
|
304
|
+
last -= 1
|
|
305
|
+
|
|
306
|
+
if first > last:
|
|
307
|
+
logger.debug(
|
|
308
|
+
"junk_only_entity_filtered",
|
|
309
|
+
text=entity.text,
|
|
310
|
+
entity_type=entity.entity_type,
|
|
311
|
+
reason="no_name_tokens",
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
start = tokens[first].start()
|
|
316
|
+
end = tokens[last].end()
|
|
317
|
+
if start != 0 or end != len(entity.text):
|
|
318
|
+
logger.debug(
|
|
319
|
+
"entity_boundary_trimmed",
|
|
320
|
+
original=entity.text,
|
|
321
|
+
trimmed=entity.text[start:end],
|
|
322
|
+
)
|
|
323
|
+
entity.start_pos += start
|
|
324
|
+
entity.end_pos = entity.start_pos + (end - start)
|
|
325
|
+
entity.text = entity.text[start:end]
|
|
326
|
+
trimmed.append(entity)
|
|
327
|
+
|
|
328
|
+
return trimmed
|
|
329
|
+
|
|
239
330
|
def _filter_title_only_entities(
|
|
240
331
|
self, entities: list[DetectedEntity]
|
|
241
332
|
) -> list[DetectedEntity]:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
spaCy model name resolution
|
|
3
|
+
|
|
4
|
+
Lightweight on purpose: imported by CLI config validation, so it must not
|
|
5
|
+
import spaCy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
DEFAULT_SPACY_MODEL = "fr_core_news_lg"
|
|
13
|
+
|
|
14
|
+
# spaCy package names: language code + underscore-separated parts
|
|
15
|
+
# (fr_core_news_lg, en_core_web_trf, xx_ent_wiki_sm). Strict on purpose: the
|
|
16
|
+
# name is passed to `python -m spacy download` when the model is missing.
|
|
17
|
+
_SPACY_PACKAGE_NAME = re.compile(r"^[a-z]{2,3}(_[a-z0-9]+)+$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_spacy_package_name(name: str) -> bool:
|
|
21
|
+
"""Check if a string is a well-formed spaCy model package name.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: Candidate model name (e.g., "en_core_web_trf")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
True if the name looks like a spaCy package name
|
|
28
|
+
"""
|
|
29
|
+
return bool(_SPACY_PACKAGE_NAME.match(name))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_spacy_model(model_name: str) -> str:
|
|
33
|
+
"""Map a configured model value to the spaCy package to load.
|
|
34
|
+
|
|
35
|
+
The legacy backend name "spacy" (and any other non-package value) maps to
|
|
36
|
+
the default French model, so existing configurations keep working.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
model_name: Value from config or --model (e.g., "spacy", "en_core_web_trf")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
spaCy package name to load
|
|
43
|
+
"""
|
|
44
|
+
if is_spacy_package_name(model_name):
|
|
45
|
+
return model_name
|
|
46
|
+
return DEFAULT_SPACY_MODEL
|
{gdpr_pseudonymizer-2.1.3 → gdpr_pseudonymizer-2.2.0}/gdpr_pseudonymizer/nlp/regex_matcher.py
RENAMED
|
@@ -121,6 +121,9 @@ class RegexMatcher:
|
|
|
121
121
|
"entity_type": config.get("entity_type", "PERSON"),
|
|
122
122
|
"confidence": config.get("confidence", 0.5),
|
|
123
123
|
"description": pattern_def.get("description", ""),
|
|
124
|
+
"require_known_first_name": bool(
|
|
125
|
+
config.get("require_known_first_name", False)
|
|
126
|
+
),
|
|
124
127
|
}
|
|
125
128
|
)
|
|
126
129
|
except re.error as e:
|
|
@@ -169,6 +172,11 @@ class RegexMatcher:
|
|
|
169
172
|
for pattern_def in pattern_list:
|
|
170
173
|
matches = pattern_def["regex"].finditer(text)
|
|
171
174
|
for match in matches:
|
|
175
|
+
if pattern_def[
|
|
176
|
+
"require_known_first_name"
|
|
177
|
+
] and not self._has_known_first_name(match):
|
|
178
|
+
continue
|
|
179
|
+
|
|
172
180
|
# Extract entity text (full match or last capturing group)
|
|
173
181
|
entity_text = match.group(0)
|
|
174
182
|
start_pos = match.start()
|
|
@@ -198,6 +206,29 @@ class RegexMatcher:
|
|
|
198
206
|
logger.debug("regex_matching_complete", entities_found=len(entities))
|
|
199
207
|
return entities
|
|
200
208
|
|
|
209
|
+
def _has_known_first_name(self, match: re.Match[str]) -> bool:
|
|
210
|
+
"""Check that the match's last group contains a known first name.
|
|
211
|
+
|
|
212
|
+
Guards "Last, First" matching: without it, any "Word, Word" pair
|
|
213
|
+
("Merci, Jean", "Thanks, Aino", "Weber, Directeur") becomes a PERSON.
|
|
214
|
+
Compound first names pass if any part is known ("Jean-Marc").
|
|
215
|
+
Without a loaded name dictionary the match is kept.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
match: Regex match whose last capturing group is the first name
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if the first name is in the name dictionary (or no dictionary)
|
|
222
|
+
"""
|
|
223
|
+
if self.name_dictionary is None:
|
|
224
|
+
return True
|
|
225
|
+
first_name = match.group(match.re.groups)
|
|
226
|
+
return any(
|
|
227
|
+
self.name_dictionary.is_first_name(part)
|
|
228
|
+
for part in first_name.split("-")
|
|
229
|
+
if part
|
|
230
|
+
)
|
|
231
|
+
|
|
201
232
|
def _is_full_names_enabled(self) -> bool:
|
|
202
233
|
"""Check if full names pattern matching is enabled."""
|
|
203
234
|
pattern_config = self._config.get("patterns", {})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# Regex Pattern Library for French Entity Detection
|
|
2
|
+
# Letter classes cover Latin-1 + Latin Extended-A (í ñ ø å ş ı ł č š ž ő...),
|
|
3
|
+
# not only French accents, so non-French names are not cut mid-word.
|
|
2
4
|
# Each pattern category can be enabled/disabled for hybrid detection
|
|
3
5
|
|
|
4
6
|
patterns:
|
|
@@ -8,8 +10,8 @@ patterns:
|
|
|
8
10
|
confidence: 0.85
|
|
9
11
|
entity_type: PERSON
|
|
10
12
|
patterns:
|
|
11
|
-
- pattern: '\b(M\.|Mme|Mlle|Dr\.|Pr\.|Maître|Me\.?)\s+([A-
|
|
12
|
-
description: "Matches titles + names: M. Dupont, Dr. Marie Dubois, Mme Laurent, Maître Dupont, Me Mercier"
|
|
13
|
+
- pattern: '\b(M\.|Mme|Mlle|Dr\.|Pr\.|Maître|Me\.?)\s+([A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+(?:-[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)*(?:\s+[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+(?:-[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)*)?)'
|
|
14
|
+
description: "Matches titles + names: M. Dupont, Dr. Marie Dubois, Mme Laurent, Maître Dupont, Me Mercier, Dr. Marja-Liisa Mikkola"
|
|
13
15
|
examples:
|
|
14
16
|
- "M. Dupont"
|
|
15
17
|
- "Mme Laurent"
|
|
@@ -25,7 +27,7 @@ patterns:
|
|
|
25
27
|
confidence: 0.80
|
|
26
28
|
entity_type: PERSON
|
|
27
29
|
patterns:
|
|
28
|
-
- pattern: '\b([A-
|
|
30
|
+
- pattern: '\b([A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+-[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)\b'
|
|
29
31
|
description: "Matches hyphenated first names: Jean-Pierre, Marie-Claire"
|
|
30
32
|
examples:
|
|
31
33
|
- "Jean-Pierre"
|
|
@@ -38,8 +40,11 @@ patterns:
|
|
|
38
40
|
enabled: true
|
|
39
41
|
confidence: 0.80
|
|
40
42
|
entity_type: PERSON
|
|
43
|
+
# First name (after the comma) must be in the name dictionary, otherwise
|
|
44
|
+
# any "Word, Word" pair matches ("Merci, Jean", "Weber, Directeur")
|
|
45
|
+
require_known_first_name: true
|
|
41
46
|
patterns:
|
|
42
|
-
- pattern: '\b([A-
|
|
47
|
+
- pattern: '\b([A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+(?:-[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)?)\s*,\s*(?!Cabinet|Société|Association|Fondation|Institut|Groupe|Consortium|Fédération|Entreprise|Compagnie|Direction|Service|Département|Commission|Comité|Syndicat|Chambre|Mutuelle|Coopérative|Ordre|Caisse|Union|Confédération|Agence|Ligue)([A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+(?:-[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)?)'
|
|
43
48
|
description: "Last, First format (e.g., Dubois, Jean-Marc) with exclusion of ORG/function words"
|
|
44
49
|
examples:
|
|
45
50
|
- "Dubois, Jean-Marc"
|
|
@@ -52,7 +57,7 @@ patterns:
|
|
|
52
57
|
confidence: 0.65
|
|
53
58
|
entity_type: LOCATION
|
|
54
59
|
patterns:
|
|
55
|
-
- pattern: '\b(à|en|dans|près de|ville de)\s+([A-
|
|
60
|
+
- pattern: '\b(à|en|dans|près de|ville de)\s+([A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+(?:\s+[A-ZÀ-ÖØ-ÞĀ-ſ][a-zß-öø-ÿĀ-ſ]+)?)'
|
|
56
61
|
description: "Matches location indicators: à Paris, en France, près de Lyon"
|
|
57
62
|
examples:
|
|
58
63
|
- "à Paris"
|
|
@@ -67,7 +72,7 @@ patterns:
|
|
|
67
72
|
confidence: 0.70
|
|
68
73
|
entity_type: ORG
|
|
69
74
|
patterns:
|
|
70
|
-
- pattern: '\b([A-
|
|
75
|
+
- pattern: '\b([A-ZÀ-ÖØ-ÞĀ-ſ][A-Za-zß-öø-ÿĀ-ſ\s&,''\.\-]+?)\s+(SA|SARL|SAS|SASU|EURL|SNC|SCM|SCI|GIE|EI|SCOP|SEL|Association|Fondation|Institut|Groupe|Consortium|Fédération|Syndicat|Chambre|Mutuelle|Coopérative|Ordre|Caisse|Union|Confédération|Agence|Comité|Commission|Ligue)\b'
|
|
71
76
|
description: "Organization with legal form suffix (30 forms)"
|
|
72
77
|
examples:
|
|
73
78
|
- "TechCorp SA"
|
|
@@ -78,7 +83,7 @@ patterns:
|
|
|
78
83
|
- "Solutions SCOP"
|
|
79
84
|
- "TechCorp Association"
|
|
80
85
|
- "Marie Curie Fondation"
|
|
81
|
-
- pattern: '\b(Société|Entreprise|Cabinet|Groupe|Compagnie|Association|Fondation|Institut|Consortium|Fédération|Syndicat|Chambre|Mutuelle|Coopérative|Ordre|Caisse|Union|Confédération|Agence|Comité|Commission|Ligue)\s+([A-
|
|
86
|
+
- pattern: '\b(Société|Entreprise|Cabinet|Groupe|Compagnie|Association|Fondation|Institut|Consortium|Fédération|Syndicat|Chambre|Mutuelle|Coopérative|Ordre|Caisse|Union|Confédération|Agence|Comité|Commission|Ligue)\s+([A-ZÀ-ÖØ-ÞĀ-ſ][A-Za-zß-öø-ÿĀ-ſ\s&,\-]+)'
|
|
82
87
|
description: "Organization with prefix keyword (22 prefixes)"
|
|
83
88
|
examples:
|
|
84
89
|
- "Société TechCorp"
|