pseudonymize 0.6.0__tar.gz → 0.7.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.
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.agents/PRIVATE_RELEASE_PLAN.md +9 -9
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.gitignore +1 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CHANGELOG.md +28 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/PKG-INFO +2 -2
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/README.md +1 -1
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/ROADMAP.md +39 -33
- pseudonymize-0.7.0/benchmarks/evaluate_quality.py +154 -0
- pseudonymize-0.7.0/docs/detectors.md +12 -0
- pseudonymize-0.7.0/docs/quality_benchmarks.md +27 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/mkdocs.yml +1 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/pyproject.toml +4 -1
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/rules.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/cli.py +18 -1
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/iban.py +4 -1
- pseudonymize-0.7.0/src/pseudonymize/detectors/italian.py +122 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/registry.py +3 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/formats.py +3 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/pdf.py +116 -5
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/normalization.py +2 -2
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/policy.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/result.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/spans.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/compatibility/test_b1_contract.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_inspection_adapters.py +149 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/backends/test_onnx.py +2 -1
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/detectors/test_detectors.py +38 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_cli_coverage.py +41 -5
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_inspection.py +15 -1
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_normalization.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_policy.py +2 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/uv.lock +953 -12
- pseudonymize-0.6.0/docs/detectors.md +0 -8
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.editorconfig +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.gitattributes +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/CODEOWNERS +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/detector-request.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/PUBLICATION_CHECKLIST.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/dependabot.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/pull_request_template.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/ci.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/docs.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/package.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/release.yml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.pre-commit-config.yaml +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CODE_OF_CONDUCT.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CONTRIBUTING.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/GEMINI.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/LICENSE +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/SECURITY.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/SUPPORT.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/VISION.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/README.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/benchmark_detectors.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/benchmark_engine.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/datasets/synthetic_messages.jsonl +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0001-dependency-free-core.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0002-token-format.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0003-no-reversible-storage.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0004-optional-ml-backend.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/api.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/architecture.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/benchmarks.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/compatibility.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/contributing.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/deployment.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/index.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/limitations.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/llm-gateways.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/llm-payloads.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/migration-a2.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/migration-a3.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/policies.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/quickstart.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0a2.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0a3.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0b1.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0rc1.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releasing.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/security.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/threat-model.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/examples/llm_gateway.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/models/README.md +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/audit_install.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/smoke_wheel.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/verify_release.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/adapters.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/api.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/base.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/composite.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/ml/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/ml/onnx.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/remote.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/base.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/email.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/ip_address.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/payment_card.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/phone.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/secret.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/url.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/document.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/engine.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/exceptions.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/image.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/office.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/processing.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/py.typed +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/resolution.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/__init__.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/alias.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/base.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/hmac.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/mode.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/placeholder.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/redact.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/de.jsonl +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/en.jsonl +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/files.json +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/it.jsonl +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_adversarial_documents.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_builtin_files.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_cli.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_cross_platform_corpus.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_file_processing.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_llm_gateway_example.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_public_api.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_release_stress.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_release_verifier.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/property/test_invariants.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/backends/test_remote.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_backends.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_document.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_engine.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_formats.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_modes.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_processing.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_spans.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_transforms.py +0 -0
- {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_types.py +0 -0
|
@@ -192,16 +192,16 @@ Starting commit: 6f6525bb1689a8b8ea5cbf7473a57d360dc339f7
|
|
|
192
192
|
Starting PyPI version: 0.5.1
|
|
193
193
|
Baseline downloads day/week/month: 0/0/0
|
|
194
194
|
Baseline stars/forks/watchers: 0/0/0
|
|
195
|
-
Files changed:
|
|
196
|
-
Compatibility tests added:
|
|
197
|
-
Artifact smoke environments:
|
|
198
|
-
Known limitations:
|
|
199
|
-
Deferred items:
|
|
200
|
-
Release commit:
|
|
201
|
-
Tag:
|
|
202
|
-
PyPI upload verified:
|
|
195
|
+
Files changed: 7
|
|
196
|
+
Compatibility tests added: Yes
|
|
197
|
+
Artifact smoke environments: Win local isolated wheel
|
|
198
|
+
Known limitations: Remote processing is restricted to allowed/configured backends.
|
|
199
|
+
Deferred items: None
|
|
200
|
+
Release commit: f9517983550048983640cf4fe0e1680fcb6da0b5
|
|
201
|
+
Tag: v0.6.0
|
|
202
|
+
PyPI upload verified: Yes
|
|
203
203
|
7-day metrics:
|
|
204
204
|
28-day metrics:
|
|
205
|
-
Status:
|
|
205
|
+
Status: Released
|
|
206
206
|
|
|
207
207
|
|
|
@@ -4,6 +4,34 @@ All notable changes follow Keep a Changelog and Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-08-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added a new quality evaluation suite (enchmarks/evaluate_quality.py) capable of streaming real-world datasets (like AI4Privacy/PII-Masking-200k) to establish rigorous precision, recall, and F1 baselines for core detectors and ML backends.
|
|
12
|
+
|
|
13
|
+
## [0.6.1] - 2026-08-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Generic `NATIONAL_ID` and `TAX_ID` entity types with checksum-validated Italian fiscal-code and
|
|
18
|
+
VAT-number detectors. Unprefixed VAT numbers require an explicit tax label.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- ONNX test artifacts now persist outside pytest's temporary directory, avoiding a verified
|
|
23
|
+
model download on every local test invocation.
|
|
24
|
+
- `inspect-file` now serializes coordinate and structural locations used by PDF, image, and
|
|
25
|
+
Office inspection instead of failing after a detection is found.
|
|
26
|
+
- IBAN detection now accepts spaces between the country code and check digits, including values
|
|
27
|
+
extracted from visually grouped documents.
|
|
28
|
+
- PDF processing now preserves unchanged text blocks and renders transformed blocks legibly
|
|
29
|
+
instead of covering the complete document text with black redaction rectangles.
|
|
30
|
+
- PDF processing now localizes changed spans inside text blocks when possible, preserving the
|
|
31
|
+
surrounding text's original visual representation while securely removing detected values.
|
|
32
|
+
- PDF redaction now preserves existing colored backgrounds and source text colors instead of
|
|
33
|
+
placing white rectangles with mismatched replacement text over styled fields.
|
|
34
|
+
|
|
7
35
|
## [0.6.0] - 2026-08-25
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pseudonymize
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Local-first PII pseudonymization for text, structured data, and LLM payloads.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ma2za/pseudonymize
|
|
6
6
|
Project-URL: Documentation, https://ma2za.github.io/pseudonymize/
|
|
@@ -88,7 +88,7 @@ no telemetry or model downloads, and denies remote-capable backends by default.
|
|
|
88
88
|
| --- | --- | --- |
|
|
89
89
|
| Strings | Shipped | Pseudonymization, redaction, batch processing, and safe reports |
|
|
90
90
|
| Nested Python data | Shipped | Dictionaries, lists, tuples, JSON scalars, and path policies |
|
|
91
|
-
| Structured detection | Shipped | Email, phone, IP, IBAN, payment card, URL credentials, and common secrets |
|
|
91
|
+
| Structured detection | Shipped | Email, phone, IP, IBAN, payment card, Italian fiscal and VAT identifiers, URL credentials, and common secrets |
|
|
92
92
|
| Document representation | Shipped | Immutable blocks, typed locations, sanitized metadata, and inspection |
|
|
93
93
|
| Generic file orchestration | Shipped | Built-in or caller-provided adapters and atomic safe-copy output |
|
|
94
94
|
| TXT, Markdown, log, JSON, JSONL, and CSV files | Shipped | Explicit format or recognized suffix; no content guessing |
|
|
@@ -44,7 +44,7 @@ no telemetry or model downloads, and denies remote-capable backends by default.
|
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| Strings | Shipped | Pseudonymization, redaction, batch processing, and safe reports |
|
|
46
46
|
| Nested Python data | Shipped | Dictionaries, lists, tuples, JSON scalars, and path policies |
|
|
47
|
-
| Structured detection | Shipped | Email, phone, IP, IBAN, payment card, URL credentials, and common secrets |
|
|
47
|
+
| Structured detection | Shipped | Email, phone, IP, IBAN, payment card, Italian fiscal and VAT identifiers, URL credentials, and common secrets |
|
|
48
48
|
| Document representation | Shipped | Immutable blocks, typed locations, sanitized metadata, and inspection |
|
|
49
49
|
| Generic file orchestration | Shipped | Built-in or caller-provided adapters and atomic safe-copy output |
|
|
50
50
|
| TXT, Markdown, log, JSON, JSONL, and CSV files | Shipped | Explicit format or recognized suffix; no content guessing |
|
|
@@ -7,14 +7,14 @@ publishable without requiring unfinished later layers.
|
|
|
7
7
|
|
|
8
8
|
| Release | Status | Outcome |
|
|
9
9
|
| --- | --- | --- |
|
|
10
|
-
| `0.1.0a1` | Published | Dependency-free text and nested-data core |
|
|
11
|
-
| `0.1.0a2` | Published | Document representation, extension contracts, safe reports, and generic file orchestration |
|
|
12
|
-
| `0.1.0a3` | Published | Built-in dependency-free text and machine-readable file adapters |
|
|
13
|
-
| `0.1.0b1` | Published | Core API freeze and production-oriented examples |
|
|
14
|
-
| `0.1.0rc1` | Published | External installation and release validation |
|
|
15
10
|
| `0.1.0` | Published | First stable text and machine-readable release |
|
|
16
11
|
| `0.2.0` | Published | Optional local machine learning identification for PII |
|
|
17
|
-
| `0.3.0` |
|
|
12
|
+
| `0.3.0` | Published | Document inspection (PDF, DOCX, XLSX, PPTX) |
|
|
13
|
+
| `0.4.0` | Published | Format-preserving documents |
|
|
14
|
+
| `0.5.0` | Published | OCR and scanned documents |
|
|
15
|
+
| `0.6.0` | Published | Remote detection |
|
|
16
|
+
| `0.6.1` | Published | Italian identifiers, PDF spans, and inspection fixes |
|
|
17
|
+
| `0.7.0` | Next | Real-world corpus benchmarking |
|
|
18
18
|
|
|
19
19
|
Alpha releases optimize for the cleanest safe architecture, not backward compatibility. They may
|
|
20
20
|
remove, rename, or replace public APIs without aliases or shims. Material changes are documented,
|
|
@@ -121,45 +121,51 @@ Exit criteria:
|
|
|
121
121
|
Stable local processing for text, nested Python data, and plain or machine-readable files, with a
|
|
122
122
|
documented compatibility policy and zero base runtime dependencies.
|
|
123
123
|
|
|
124
|
-
##
|
|
124
|
+
## The Road to 1.0 (Hardening and Production Readiness)
|
|
125
125
|
|
|
126
|
-
### `0.
|
|
126
|
+
### `0.7.0`: Real-World Corpus Benchmarking
|
|
127
|
+
- **Use Case:** Medical/Legal data mining.
|
|
128
|
+
- **Focus:** Integrate public datasets (Enron corpus, MIMIC-III synthetic variants, SEC filings) to establish precision/recall baselines for existing detectors.
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
- No model download during import or inference
|
|
130
|
+
### `0.8.0`: Adversarial Document Defenses
|
|
131
|
+
- **Use Case:** FOIA response redaction failures (e.g., Manafort, AstraZeneca leaks).
|
|
132
|
+
- **Focus:** Prevent visual-only masking, hidden OCR layers, overlapping z-index elements, and scrub incremental document revisions.
|
|
132
133
|
|
|
133
|
-
### `0.
|
|
134
|
+
### `0.9.0`: Exhaustive Metadata & Hidden Structure Extraction
|
|
135
|
+
- **Use Case:** Corporate e-discovery.
|
|
136
|
+
- **Focus:** Parse PDF `/Info` dictionaries, XMP metadata, DOCX headers/footers, and XLSX comments.
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
+
### `0.10.0`: Large-Scale & Streaming Data Pipelines
|
|
139
|
+
- **Use Case:** Database dumps and bulk analytics exports.
|
|
140
|
+
- **Focus:** Bounded-memory processing for multi-gigabyte CSV/JSONL files; parallel block processing.
|
|
138
141
|
|
|
139
|
-
### `0.
|
|
142
|
+
### `0.11.0`: Resilient Document Parsing
|
|
143
|
+
- **Use Case:** Legacy enterprise files.
|
|
144
|
+
- **Focus:** Gracefully handle corrupt XML schemas, custom OOXML namespaces, and safely extract embedded OLE objects.
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
146
|
+
### `0.12.0`: Advanced OCR Degradation Handling
|
|
147
|
+
- **Use Case:** Medical faxes and legacy legal scans.
|
|
148
|
+
- **Focus:** Handle low-dpi faxes, skewed pages, noisy backgrounds, and watermark interference mimicking structural data.
|
|
144
149
|
|
|
145
|
-
### `0.
|
|
150
|
+
### `0.13.0`: ML Confidence Calibration & Thresholding
|
|
151
|
+
- **Use Case:** Reducing false positives in unstructured clinical notes.
|
|
152
|
+
- **Focus:** Tune ONNX backend confidence scores against ambiguous texts and introduce dynamic, policy-based confidence thresholds.
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
154
|
+
### `0.14.0`: Telemetry, Observability & Auditing
|
|
155
|
+
- **Use Case:** Enterprise compliance reporting and SLA monitoring.
|
|
156
|
+
- **Focus:** Structured JSON logging, processing timeouts, latency tracing, and detailed audit trails without leaking PII.
|
|
150
157
|
|
|
151
|
-
### `0.
|
|
158
|
+
### `0.15.0`: Remote Backend Fault Tolerance
|
|
159
|
+
- **Use Case:** High-throughput API gateway integration.
|
|
160
|
+
- **Focus:** Circuit breakers, adaptive retries, rate-limit handling, and strict latency bounds for the HTTP provider transport.
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
- Local structured-value replacement before permitted remote processing
|
|
157
|
-
- Offset mapping after local preprocessing
|
|
158
|
-
|
|
159
|
-
### `1.0.0`: mature compatibility commitment
|
|
162
|
+
### `0.16.0`: Cross-Lingual & Typographical Boundary Hardening (Pre-1.0 Audit)
|
|
163
|
+
- **Use Case:** Global support ticket sanitization.
|
|
164
|
+
- **Focus:** Audit detectors against non-Latin scripts, CJK spacing, RTL text, and bidirectional text overrides. Final security fuzzing (Zip-bombs, XXE).
|
|
160
165
|
|
|
166
|
+
### `1.0.0`: Mature Compatibility Commitment
|
|
161
167
|
Long-term compatibility begins after core processing, document rewriting, OCR, and remote-security
|
|
162
|
-
contracts have production fixtures, published benchmarks, and independent usage feedback.
|
|
168
|
+
contracts have production fixtures, published benchmarks, and independent usage feedback against the hardening milestones above.
|
|
163
169
|
|
|
164
170
|
## Optional dependency policy
|
|
165
171
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from datasets import load_dataset
|
|
9
|
+
except ImportError:
|
|
10
|
+
print("Error: 'datasets' library not found.")
|
|
11
|
+
print("Run: uv run --with datasets python benchmarks/evaluate_quality.py")
|
|
12
|
+
sys.exit(1)
|
|
13
|
+
|
|
14
|
+
from pseudonymize.backends.ml.onnx import LocalONNXPIIBackend
|
|
15
|
+
from pseudonymize.engine import Pseudonymizer
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
18
|
+
logger = logging.getLogger("evaluate_quality")
|
|
19
|
+
|
|
20
|
+
# We evaluate precision and recall against these specific AI4Privacy labels
|
|
21
|
+
# that map to the capabilities of pseudonymize's core detectors and ML backend.
|
|
22
|
+
SUPPORTED_LABELS = {
|
|
23
|
+
# Core detectors
|
|
24
|
+
"EMAIL",
|
|
25
|
+
"PHONENUMBER",
|
|
26
|
+
"CREDITCARD",
|
|
27
|
+
"IBAN",
|
|
28
|
+
"IPADDRESS",
|
|
29
|
+
"URL",
|
|
30
|
+
# ML Backend (DistilBERT ONNX)
|
|
31
|
+
"FIRSTNAME",
|
|
32
|
+
"LASTNAME",
|
|
33
|
+
"MIDDLENAME",
|
|
34
|
+
"COMPANYNAME",
|
|
35
|
+
"CITY",
|
|
36
|
+
"STATE",
|
|
37
|
+
"COUNTY",
|
|
38
|
+
"STREET",
|
|
39
|
+
"ZIPCODE",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def evaluate(num_samples: int, use_ml: bool):
|
|
44
|
+
logger.info("Loading ai4privacy/pii-masking-200k (English subset)...")
|
|
45
|
+
# Use streaming to avoid downloading the entire 200K dataset
|
|
46
|
+
ds = load_dataset("ai4privacy/pii-masking-200k", split="train", streaming=True)
|
|
47
|
+
|
|
48
|
+
engine = Pseudonymizer()
|
|
49
|
+
if use_ml:
|
|
50
|
+
# We need the model downloaded. The test suite uses the distilbert model.
|
|
51
|
+
# Let's assume it's already cached or we can fetch it.
|
|
52
|
+
# To keep it simple, we'll try to initialize it. If it fails, we fall back or error.
|
|
53
|
+
CACHE_DIR = Path(".cache/pseudonymize-tests/models/distilbert-ml")
|
|
54
|
+
onnx_model_path = CACHE_DIR / "model_int8.onnx"
|
|
55
|
+
tokenizer_path = CACHE_DIR / "tokenizer.json"
|
|
56
|
+
|
|
57
|
+
if not onnx_model_path.exists() or not tokenizer_path.exists():
|
|
58
|
+
logger.error(f"ML artifacts not found in {CACHE_DIR}.")
|
|
59
|
+
logger.error("Please run 'uv run pytest tests/unit/backends/test_onnx.py' first.")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
backend = LocalONNXPIIBackend(model_path=onnx_model_path, tokenizer_path=tokenizer_path)
|
|
63
|
+
engine = Pseudonymizer(backends=[*engine.backends, backend])
|
|
64
|
+
|
|
65
|
+
true_positives = 0
|
|
66
|
+
false_positives = 0
|
|
67
|
+
false_negatives = 0
|
|
68
|
+
|
|
69
|
+
count = 0
|
|
70
|
+
start_time = time.time()
|
|
71
|
+
|
|
72
|
+
for row in ds:
|
|
73
|
+
if row["language"] != "en":
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
text = row["source_text"]
|
|
77
|
+
masks = row["privacy_mask"]
|
|
78
|
+
|
|
79
|
+
# Filter ground truth masks to only those we claim to support
|
|
80
|
+
ground_truth_spans = [
|
|
81
|
+
(mask["start"], mask["end"], mask["label"])
|
|
82
|
+
for mask in masks
|
|
83
|
+
if mask["label"] in SUPPORTED_LABELS
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Run detection
|
|
87
|
+
result = engine.process_with_report(text)
|
|
88
|
+
|
|
89
|
+
# Extract detected spans
|
|
90
|
+
detected_spans = [(detection.start, detection.end) for detection in result.detections]
|
|
91
|
+
|
|
92
|
+
# Calculate overlap
|
|
93
|
+
# For simplicity in this benchmark:
|
|
94
|
+
# TP: a detected span overlaps with a ground truth span.
|
|
95
|
+
# FP: a detected span does not overlap with any ground truth span.
|
|
96
|
+
# FN: a ground truth span is not overlapped by any detected span.
|
|
97
|
+
|
|
98
|
+
matched_gt = set()
|
|
99
|
+
for d_start, d_end in detected_spans:
|
|
100
|
+
matched = False
|
|
101
|
+
for i, (g_start, g_end, _g_label) in enumerate(ground_truth_spans):
|
|
102
|
+
# Check overlap
|
|
103
|
+
if max(d_start, g_start) < min(d_end, g_end):
|
|
104
|
+
matched = True
|
|
105
|
+
matched_gt.add(i)
|
|
106
|
+
break
|
|
107
|
+
if matched:
|
|
108
|
+
true_positives += 1
|
|
109
|
+
else:
|
|
110
|
+
false_positives += 1
|
|
111
|
+
|
|
112
|
+
for i in range(len(ground_truth_spans)):
|
|
113
|
+
if i not in matched_gt:
|
|
114
|
+
false_negatives += 1
|
|
115
|
+
|
|
116
|
+
count += 1
|
|
117
|
+
if count >= num_samples:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
elapsed = time.time() - start_time
|
|
121
|
+
|
|
122
|
+
precision = (
|
|
123
|
+
true_positives / (true_positives + false_positives)
|
|
124
|
+
if (true_positives + false_positives) > 0
|
|
125
|
+
else 0
|
|
126
|
+
)
|
|
127
|
+
recall = (
|
|
128
|
+
true_positives / (true_positives + false_negatives)
|
|
129
|
+
if (true_positives + false_negatives) > 0
|
|
130
|
+
else 0
|
|
131
|
+
)
|
|
132
|
+
f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
|
|
133
|
+
|
|
134
|
+
logger.info(f"--- Benchmark Results ({count} samples) ---")
|
|
135
|
+
logger.info(f"Time elapsed: {elapsed:.2f}s")
|
|
136
|
+
logger.info(f"True Positives: {true_positives}")
|
|
137
|
+
logger.info(f"False Positives: {false_positives}")
|
|
138
|
+
logger.info(f"False Negatives: {false_negatives}")
|
|
139
|
+
logger.info(f"Precision: {precision:.4f}")
|
|
140
|
+
logger.info(f"Recall: {recall:.4f}")
|
|
141
|
+
logger.info(f"F1 Score: {f1:.4f}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
parser = argparse.ArgumentParser(
|
|
146
|
+
description="Evaluate pseudonymize precision and recall against real-world datasets."
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("--samples", type=int, default=500, help="Number of samples to evaluate.")
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--ml", action="store_true", help="Include the ONNX ML backend in evaluation."
|
|
151
|
+
)
|
|
152
|
+
args = parser.parse_args()
|
|
153
|
+
|
|
154
|
+
evaluate(args.samples, args.ml)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Detectors
|
|
2
|
+
|
|
3
|
+
Bundled detectors cover email, phone, IP address, IBAN, payment card, URL credential, and secret
|
|
4
|
+
entities. Italian fiscal codes and VAT identifiers are also supported as `NATIONAL_ID` and
|
|
5
|
+
`TAX_ID`. Candidate extraction is followed by normalization and validation. IBANs require MOD-97,
|
|
6
|
+
cards require Luhn, Italian identifiers require their official check character or digit, and IP
|
|
7
|
+
candidates must parse with the standard library. An Italian VAT number without an `IT` prefix is
|
|
8
|
+
considered only next to an explicit VAT label to avoid treating arbitrary 11-digit values as tax
|
|
9
|
+
identifiers.
|
|
10
|
+
|
|
11
|
+
Detector results contain type, offsets, confidence, and detector name. They never contain the raw
|
|
12
|
+
matched value. Custom thread-safe detectors can implement the `Detector` protocol.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Detection Quality Benchmarks
|
|
2
|
+
|
|
3
|
+
This document tracks the precision and recall of the `pseudonymize` engine across real-world and synthetic evaluation corpora. Unlike performance benchmarks (which measure throughput and latency), quality benchmarks ensure the core detectors and ML backends accurately identify PII without excessive false positives.
|
|
4
|
+
|
|
5
|
+
## `0.7.0` Baseline (AI4Privacy PII-Masking-200k)
|
|
6
|
+
|
|
7
|
+
Evaluated against the English subset of the `ai4privacy/pii-masking-200k` dataset, comparing the engine's detections against the dataset's ground-truth `privacy_mask`.
|
|
8
|
+
|
|
9
|
+
**Setup:**
|
|
10
|
+
- Model: `LocalONNXPIIBackend` (DistilBERT ONNX int8)
|
|
11
|
+
- Target Entities: `EMAIL`, `PHONENUMBER`, `CREDITCARD`, `IBAN`, `IPADDRESS`, `URL`, `FIRSTNAME`, `LASTNAME`, `MIDDLENAME`, `COMPANYNAME`, `CITY`, `STATE`, `COUNTY`, `STREET`, `ZIPCODE`
|
|
12
|
+
- Sample Size: 50 documents (Streaming)
|
|
13
|
+
|
|
14
|
+
**Results (Preliminary Baseline):**
|
|
15
|
+
|
|
16
|
+
| Metric | Score |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Precision | 0.4091 |
|
|
19
|
+
| Recall | 0.1875 |
|
|
20
|
+
| F1 Score | 0.2571 |
|
|
21
|
+
|
|
22
|
+
*Note: These baseline metrics reflect zero-configuration default detectors and the stock ML backend on a highly ambiguous, fine-grained synthetic dataset. Future releases (e.g., 0.13.0 ML Calibration) will optimize these metrics through tuned thresholds and improved boundaries.*
|
|
23
|
+
|
|
24
|
+
To reproduce this benchmark locally:
|
|
25
|
+
```console
|
|
26
|
+
uv run python benchmarks/evaluate_quality.py --ml --samples 50
|
|
27
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pseudonymize"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Local-first PII pseudonymization for text, structured data, and LLM payloads."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -72,6 +72,9 @@ test = [
|
|
|
72
72
|
"fpdf2>=2.7.9",
|
|
73
73
|
"respx>=0.23.1",
|
|
74
74
|
]
|
|
75
|
+
benchmarks = [
|
|
76
|
+
"datasets>=3.0.0",
|
|
77
|
+
]
|
|
75
78
|
quality = ["mypy>=1.11", "pre-commit>=4", "ruff>=0.9"]
|
|
76
79
|
docs = ["mkdocs-material>=9", "mkdocstrings[python]>=1.0.6"]
|
|
77
80
|
release = ["build>=1", "twine>=6"]
|
|
@@ -10,7 +10,13 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from pseudonymize.api import generate_key
|
|
12
12
|
from pseudonymize.detectors import DEFAULT_DETECTORS
|
|
13
|
-
from pseudonymize.document import
|
|
13
|
+
from pseudonymize.document import (
|
|
14
|
+
CoordinateLocation,
|
|
15
|
+
CSVCellLocation,
|
|
16
|
+
JSONPathLocation,
|
|
17
|
+
StructuralLocation,
|
|
18
|
+
TextOffsetLocation,
|
|
19
|
+
)
|
|
14
20
|
from pseudonymize.engine import Data, Pseudonymizer
|
|
15
21
|
from pseudonymize.exceptions import PseudonymizeError
|
|
16
22
|
from pseudonymize.formats import FileFormat
|
|
@@ -199,6 +205,17 @@ def _location_payload(report: DetectionReport) -> dict[str, object]:
|
|
|
199
205
|
return {"kind": "json_path", "path": location.path}
|
|
200
206
|
if isinstance(location, CSVCellLocation):
|
|
201
207
|
return {"kind": "csv_cell", "row": location.row, "column": location.column}
|
|
208
|
+
if isinstance(location, CoordinateLocation):
|
|
209
|
+
return {
|
|
210
|
+
"kind": "coordinate",
|
|
211
|
+
"page": location.page,
|
|
212
|
+
"x0": location.x0,
|
|
213
|
+
"y0": location.y0,
|
|
214
|
+
"x1": location.x1,
|
|
215
|
+
"y1": location.y1,
|
|
216
|
+
}
|
|
217
|
+
if isinstance(location, StructuralLocation):
|
|
218
|
+
return {"kind": "structural", "path": location.path}
|
|
202
219
|
raise TypeError("unsupported report location")
|
|
203
220
|
|
|
204
221
|
|
|
@@ -3,7 +3,10 @@ from dataclasses import dataclass
|
|
|
3
3
|
|
|
4
4
|
from pseudonymize.result import Detection, EntityType
|
|
5
5
|
|
|
6
|
-
_IBAN = re.compile(
|
|
6
|
+
_IBAN = re.compile(
|
|
7
|
+
r"(?<![A-Z0-9])[A-Z]{2}(?:[ ]?\d){2}(?:[ ]?[A-Z0-9]){11,30}(?![A-Z0-9])",
|
|
8
|
+
re.I,
|
|
9
|
+
)
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
def _valid_mod97(value: str) -> bool:
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_OMOCODIA_DIGITS = str.maketrans("LMNPQRSTUV", "0123456789")
|
|
7
|
+
_FISCAL_CODE = re.compile(
|
|
8
|
+
r"(?<![A-Z0-9])"
|
|
9
|
+
r"[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}"
|
|
10
|
+
r"[A-Z][0-9LMNPQRSTUV]{3}[A-Z]"
|
|
11
|
+
r"(?![A-Z0-9])",
|
|
12
|
+
re.I,
|
|
13
|
+
)
|
|
14
|
+
_ODD_VALUES = dict(
|
|
15
|
+
zip(
|
|
16
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
17
|
+
(
|
|
18
|
+
1,
|
|
19
|
+
0,
|
|
20
|
+
5,
|
|
21
|
+
7,
|
|
22
|
+
9,
|
|
23
|
+
13,
|
|
24
|
+
15,
|
|
25
|
+
17,
|
|
26
|
+
19,
|
|
27
|
+
21,
|
|
28
|
+
1,
|
|
29
|
+
0,
|
|
30
|
+
5,
|
|
31
|
+
7,
|
|
32
|
+
9,
|
|
33
|
+
13,
|
|
34
|
+
15,
|
|
35
|
+
17,
|
|
36
|
+
19,
|
|
37
|
+
21,
|
|
38
|
+
2,
|
|
39
|
+
4,
|
|
40
|
+
18,
|
|
41
|
+
20,
|
|
42
|
+
11,
|
|
43
|
+
3,
|
|
44
|
+
6,
|
|
45
|
+
8,
|
|
46
|
+
12,
|
|
47
|
+
14,
|
|
48
|
+
16,
|
|
49
|
+
10,
|
|
50
|
+
22,
|
|
51
|
+
25,
|
|
52
|
+
24,
|
|
53
|
+
23,
|
|
54
|
+
),
|
|
55
|
+
strict=True,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
_EVEN_VALUES = {
|
|
59
|
+
**{character: value for value, character in enumerate("0123456789")},
|
|
60
|
+
**{character: value for value, character in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
|
61
|
+
}
|
|
62
|
+
_VAT_WITH_COUNTRY = re.compile(
|
|
63
|
+
r"(?<![A-Z0-9])(?P<value>IT[ ]?(?:\d[ ]?){10}\d)(?![A-Z0-9])",
|
|
64
|
+
re.I,
|
|
65
|
+
)
|
|
66
|
+
_VAT_WITH_LABEL = re.compile(
|
|
67
|
+
r"\b(?:partita[ ]+iva|p[ .]*iva|p[ .]*i[ .]*|vat(?:[ ]+id)?)"
|
|
68
|
+
r"\s*[:=]?\s*(?P<value>(?:IT[ ]?)?(?:\d[ ]?){10}\d)(?!\d)",
|
|
69
|
+
re.I,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _valid_fiscal_code(value: str) -> bool:
|
|
74
|
+
compact = value.upper()
|
|
75
|
+
day_text = compact[9:11].translate(_OMOCODIA_DIGITS)
|
|
76
|
+
day = int(day_text)
|
|
77
|
+
if not (1 <= day <= 31 or 41 <= day <= 71):
|
|
78
|
+
return False
|
|
79
|
+
total = sum(
|
|
80
|
+
(_ODD_VALUES if index % 2 == 0 else _EVEN_VALUES)[character]
|
|
81
|
+
for index, character in enumerate(compact[:15])
|
|
82
|
+
)
|
|
83
|
+
return compact[-1] == chr(ord("A") + total % 26)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _valid_vat(value: str) -> bool:
|
|
87
|
+
compact = "".join(character for character in value if character.isdigit())
|
|
88
|
+
if len(compact) != 11 or len(set(compact)) == 1:
|
|
89
|
+
return False
|
|
90
|
+
total = sum(int(character) for character in compact[:10:2])
|
|
91
|
+
for character in compact[1:10:2]:
|
|
92
|
+
doubled = int(character) * 2
|
|
93
|
+
total += doubled - 9 if doubled > 9 else doubled
|
|
94
|
+
return (10 - total % 10) % 10 == int(compact[-1])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class ItalianFiscalCodeDetector:
|
|
99
|
+
name: str = "italian_fiscal_code"
|
|
100
|
+
|
|
101
|
+
def detect(self, text: str) -> list[Detection]:
|
|
102
|
+
return [
|
|
103
|
+
Detection(EntityType.NATIONAL_ID, match.start(), match.end(), 1.0, self.name)
|
|
104
|
+
for match in _FISCAL_CODE.finditer(text)
|
|
105
|
+
if _valid_fiscal_code(match.group())
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True, slots=True)
|
|
110
|
+
class ItalianVATDetector:
|
|
111
|
+
name: str = "italian_vat"
|
|
112
|
+
|
|
113
|
+
def detect(self, text: str) -> list[Detection]:
|
|
114
|
+
spans = {
|
|
115
|
+
match.span("value")
|
|
116
|
+
for pattern in (_VAT_WITH_COUNTRY, _VAT_WITH_LABEL)
|
|
117
|
+
for match in pattern.finditer(text)
|
|
118
|
+
if _valid_vat(match.group("value"))
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
Detection(EntityType.TAX_ID, start, end, 1.0, self.name) for start, end in sorted(spans)
|
|
122
|
+
]
|
|
@@ -2,6 +2,7 @@ from pseudonymize.detectors.base import Detector
|
|
|
2
2
|
from pseudonymize.detectors.email import EmailDetector
|
|
3
3
|
from pseudonymize.detectors.iban import IbanDetector
|
|
4
4
|
from pseudonymize.detectors.ip_address import IpAddressDetector
|
|
5
|
+
from pseudonymize.detectors.italian import ItalianFiscalCodeDetector, ItalianVATDetector
|
|
5
6
|
from pseudonymize.detectors.payment_card import PaymentCardDetector
|
|
6
7
|
from pseudonymize.detectors.phone import PhoneDetector
|
|
7
8
|
from pseudonymize.detectors.secret import SecretDetector
|
|
@@ -12,6 +13,8 @@ DEFAULT_DETECTORS: tuple[Detector, ...] = (
|
|
|
12
13
|
IpAddressDetector(),
|
|
13
14
|
PaymentCardDetector(),
|
|
14
15
|
IbanDetector(),
|
|
16
|
+
ItalianFiscalCodeDetector(),
|
|
17
|
+
ItalianVATDetector(),
|
|
15
18
|
PhoneDetector(),
|
|
16
19
|
UrlDetector(),
|
|
17
20
|
SecretDetector(),
|
|
@@ -339,6 +339,9 @@ def _read_csv(text: str) -> tuple[tuple[str, ...], ...]:
|
|
|
339
339
|
previous_limit = csv.field_size_limit()
|
|
340
340
|
try:
|
|
341
341
|
csv.field_size_limit(sys.maxsize)
|
|
342
|
+
except OverflowError:
|
|
343
|
+
csv.field_size_limit(2147483647)
|
|
344
|
+
try:
|
|
342
345
|
return tuple(
|
|
343
346
|
tuple(row) for row in csv.reader(io.StringIO(text, newline=""), strict=True)
|
|
344
347
|
)
|