pdf-anonymizer-api 0.29.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.
- pdf_anonymizer_api-0.29.0/PKG-INFO +29 -0
- pdf_anonymizer_api-0.29.0/README.md +16 -0
- pdf_anonymizer_api-0.29.0/pyproject.toml +29 -0
- pdf_anonymizer_api-0.29.0/setup.cfg +4 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api/__init__.py +5 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api/__main__.py +4 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api/app.py +239 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api/server.py +31 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/PKG-INFO +29 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/SOURCES.txt +12 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/dependency_links.txt +1 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/entry_points.txt +2 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/requires.txt +3 -0
- pdf_anonymizer_api-0.29.0/src/pdf_anonymizer_api.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdf-anonymizer-api
|
|
3
|
+
Version: 0.29.0
|
|
4
|
+
Summary: Local HTTP service for pdf-anonymizer-core. No authentication.
|
|
5
|
+
Author-email: Leonid Ganeline <leo.gan.57@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: repository, https://github.com/leo-gan/anonymizer
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pdf-anonymizer-core>=0.29.0
|
|
11
|
+
Requires-Dist: fastapi>=0.115
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
13
|
+
|
|
14
|
+
# PDF Anonymizer HTTP service
|
|
15
|
+
|
|
16
|
+
A thin FastAPI wrapper around `pdf-anonymizer-core`. It does **not** depend on the CLI. There is no authentication.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pdf-anonymizer-api
|
|
20
|
+
pdf-anonymizer-api --host 127.0.0.1 --port 8000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Docker files live in this package. From the repository root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker compose -f packages/pdf-anonymizer-api/docker-compose.yml up --build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Docs: [HTTP service and Docker](https://leo-gan.github.io/anonymizer/project/http-service/).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# PDF Anonymizer HTTP service
|
|
2
|
+
|
|
3
|
+
A thin FastAPI wrapper around `pdf-anonymizer-core`. It does **not** depend on the CLI. There is no authentication.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install pdf-anonymizer-api
|
|
7
|
+
pdf-anonymizer-api --host 127.0.0.1 --port 8000
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Docker files live in this package. From the repository root:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
docker compose -f packages/pdf-anonymizer-api/docker-compose.yml up --build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Docs: [HTTP service and Docker](https://leo-gan.github.io/anonymizer/project/http-service/).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pdf-anonymizer-api"
|
|
3
|
+
version = "0.29.0"
|
|
4
|
+
description = "Local HTTP service for pdf-anonymizer-core. No authentication."
|
|
5
|
+
authors = [{ name = "Leonid Ganeline", email = "leo.gan.57@gmail.com" }]
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"pdf-anonymizer-core>=0.29.0",
|
|
11
|
+
"fastapi>=0.115",
|
|
12
|
+
"uvicorn[standard]>=0.30",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
repository = "https://github.com/leo-gan/anonymizer"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
pdf-anonymizer-api = "pdf_anonymizer_api.server:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=61.0"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = {"" = "src"}
|
|
27
|
+
|
|
28
|
+
[tool.uv.sources]
|
|
29
|
+
pdf-anonymizer-core = { workspace = true }
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Thin HTTP service: anonymize, deanonymize, verify, report.
|
|
2
|
+
|
|
3
|
+
Install ``pdf-anonymizer-api``. Auth is out of scope. Bind to localhost
|
|
4
|
+
or a compose network. This package calls ``pdf-anonymizer-core`` only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from pdf_anonymizer_core.conf import filter_regex_patterns
|
|
14
|
+
from pdf_anonymizer_core.core import (
|
|
15
|
+
build_mapping,
|
|
16
|
+
collect_entities_from_chunks,
|
|
17
|
+
finalize_entities,
|
|
18
|
+
)
|
|
19
|
+
from pdf_anonymizer_core.prompts import detailed, hipaa, simple
|
|
20
|
+
from pdf_anonymizer_core.risk import assess_linkage_risk
|
|
21
|
+
from pdf_anonymizer_core.spans import replace_entities
|
|
22
|
+
from pdf_anonymizer_core.operators import restore_encrypt_tokens
|
|
23
|
+
from pdf_anonymizer_core.utils import (
|
|
24
|
+
mapping_to_placeholder_original,
|
|
25
|
+
restore_placeholders_in_text,
|
|
26
|
+
)
|
|
27
|
+
from pdf_anonymizer_core.verify import verify_anonymized_text
|
|
28
|
+
|
|
29
|
+
API_INSTALL_MESSAGE = "HTTP service requires: pip install pdf-anonymizer-api"
|
|
30
|
+
MAX_TEXT_CHARS = 5_000_000
|
|
31
|
+
_PROMPTS = {
|
|
32
|
+
"simple": simple.prompt_template,
|
|
33
|
+
"detailed": detailed.prompt_template,
|
|
34
|
+
"hipaa": hipaa.prompt_template,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AnonymizeBody(BaseModel):
|
|
39
|
+
text: str
|
|
40
|
+
use_llm: bool = False
|
|
41
|
+
use_ner: bool = False
|
|
42
|
+
min_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
43
|
+
keep_list: Optional[List[str]] = None
|
|
44
|
+
deny_list: Optional[List[str]] = None
|
|
45
|
+
operators: Optional[Dict[str, str]] = None
|
|
46
|
+
seed_mapping: Optional[Dict[str, str]] = None
|
|
47
|
+
fake_secret: Optional[str] = None
|
|
48
|
+
encrypt_secret: Optional[str] = None
|
|
49
|
+
model_name: Optional[str] = None
|
|
50
|
+
prompt_name: str = "simple"
|
|
51
|
+
anonymized_entities: Optional[List[str]] = None
|
|
52
|
+
countries: Optional[List[str]] = None
|
|
53
|
+
opt_in: Optional[List[str]] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DeanonymizeBody(BaseModel):
|
|
57
|
+
text: str
|
|
58
|
+
mapping: Dict[str, str]
|
|
59
|
+
encrypt_secret: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TextBody(BaseModel):
|
|
63
|
+
text: str
|
|
64
|
+
countries: Optional[List[str]] = None
|
|
65
|
+
opt_in: Optional[List[str]] = None
|
|
66
|
+
use_llm: bool = False
|
|
67
|
+
model_name: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _public_entity(entity: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"text": entity.get("text", ""),
|
|
73
|
+
"type": str(entity.get("type", "")).upper(),
|
|
74
|
+
"base_form": entity.get("base_form") or entity.get("text", ""),
|
|
75
|
+
"score": float(entity.get("score", 1.0)),
|
|
76
|
+
"source": entity.get("source") or "regex",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def anonymize_text_request(
|
|
81
|
+
text: str,
|
|
82
|
+
*,
|
|
83
|
+
use_llm: bool = False,
|
|
84
|
+
use_ner: bool = False,
|
|
85
|
+
min_confidence: float = 0.0,
|
|
86
|
+
keep_list: Optional[List[str]] = None,
|
|
87
|
+
deny_list: Optional[List[str]] = None,
|
|
88
|
+
operators: Optional[Dict[str, str]] = None,
|
|
89
|
+
seed_mapping: Optional[Dict[str, str]] = None,
|
|
90
|
+
fake_secret: Optional[str] = None,
|
|
91
|
+
encrypt_secret: Optional[str] = None,
|
|
92
|
+
model_name: Optional[str] = None,
|
|
93
|
+
prompt_name: str = "simple",
|
|
94
|
+
anonymized_entities: Optional[List[str]] = None,
|
|
95
|
+
countries: Optional[List[str]] = None,
|
|
96
|
+
opt_in: Optional[List[str]] = None,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
"""Run the same engine as the CLI on an in-memory string."""
|
|
99
|
+
if len(text) > MAX_TEXT_CHARS:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"text is {len(text):,} characters; the HTTP limit is {MAX_TEXT_CHARS:,}."
|
|
102
|
+
)
|
|
103
|
+
template = _PROMPTS.get(prompt_name)
|
|
104
|
+
if template is None:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Unknown prompt_name {prompt_name!r}. Use simple, detailed, or hipaa."
|
|
107
|
+
)
|
|
108
|
+
if not 0.0 <= min_confidence <= 1.0:
|
|
109
|
+
raise ValueError("min_confidence must be between 0 and 1.")
|
|
110
|
+
|
|
111
|
+
collected = collect_entities_from_chunks(
|
|
112
|
+
[text],
|
|
113
|
+
prompt_template=template,
|
|
114
|
+
model_name=model_name or "gemini-2.5-flash",
|
|
115
|
+
regex_patterns=filter_regex_patterns(countries, opt_in=opt_in),
|
|
116
|
+
max_retries=3,
|
|
117
|
+
base_retry_delay=1.0,
|
|
118
|
+
max_retry_delay=10.0,
|
|
119
|
+
use_llm=use_llm,
|
|
120
|
+
use_ner=use_ner,
|
|
121
|
+
)
|
|
122
|
+
entities = finalize_entities(
|
|
123
|
+
collected,
|
|
124
|
+
text,
|
|
125
|
+
anonymized_entities=anonymized_entities,
|
|
126
|
+
keep_list=keep_list,
|
|
127
|
+
deny_list=deny_list,
|
|
128
|
+
min_confidence=min_confidence,
|
|
129
|
+
seed_mapping=seed_mapping,
|
|
130
|
+
)
|
|
131
|
+
mapping = build_mapping(
|
|
132
|
+
entities,
|
|
133
|
+
seed_mapping=seed_mapping,
|
|
134
|
+
operators=operators,
|
|
135
|
+
fake_secret=fake_secret,
|
|
136
|
+
encrypt_secret=encrypt_secret,
|
|
137
|
+
)
|
|
138
|
+
anonymized = text
|
|
139
|
+
if entities:
|
|
140
|
+
anonymized = replace_entities(
|
|
141
|
+
text, (entity["text"] for entity in entities), mapping
|
|
142
|
+
)
|
|
143
|
+
return {
|
|
144
|
+
"anonymized_text": anonymized,
|
|
145
|
+
"mapping": mapping,
|
|
146
|
+
"entities": [_public_entity(entity) for entity in entities],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def deanonymize_text_request(
|
|
151
|
+
text: str,
|
|
152
|
+
mapping: Dict[str, str],
|
|
153
|
+
*,
|
|
154
|
+
encrypt_secret: Optional[str] = None,
|
|
155
|
+
) -> Dict[str, Any]:
|
|
156
|
+
if len(text) > MAX_TEXT_CHARS:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"text is {len(text):,} characters; the HTTP limit is {MAX_TEXT_CHARS:,}."
|
|
159
|
+
)
|
|
160
|
+
placeholder_to_original = mapping_to_placeholder_original(mapping)
|
|
161
|
+
restored, used = restore_placeholders_in_text(text, placeholder_to_original)
|
|
162
|
+
if encrypt_secret:
|
|
163
|
+
restored = restore_encrypt_tokens(restored, encrypt_secret)
|
|
164
|
+
return {
|
|
165
|
+
"text": restored,
|
|
166
|
+
"restored_count": len(used),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_app():
|
|
171
|
+
"""Build the FastAPI app."""
|
|
172
|
+
from fastapi import FastAPI, HTTPException
|
|
173
|
+
|
|
174
|
+
application = FastAPI(
|
|
175
|
+
title="PDF Anonymizer",
|
|
176
|
+
description=(
|
|
177
|
+
"Local HTTP wrapper around pdf-anonymizer-core. "
|
|
178
|
+
"No authentication. Bind to localhost or a compose network."
|
|
179
|
+
),
|
|
180
|
+
version="0.26.0",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@application.get("/health")
|
|
184
|
+
def health() -> Dict[str, str]:
|
|
185
|
+
return {"status": "ok", "version": "0.26.0"}
|
|
186
|
+
|
|
187
|
+
@application.post("/anonymize")
|
|
188
|
+
def anonymize(body: AnonymizeBody) -> Dict[str, Any]:
|
|
189
|
+
try:
|
|
190
|
+
return anonymize_text_request(
|
|
191
|
+
body.text,
|
|
192
|
+
use_llm=body.use_llm,
|
|
193
|
+
use_ner=body.use_ner,
|
|
194
|
+
min_confidence=body.min_confidence,
|
|
195
|
+
keep_list=body.keep_list,
|
|
196
|
+
deny_list=body.deny_list,
|
|
197
|
+
operators=body.operators,
|
|
198
|
+
seed_mapping=body.seed_mapping,
|
|
199
|
+
fake_secret=body.fake_secret,
|
|
200
|
+
encrypt_secret=body.encrypt_secret,
|
|
201
|
+
model_name=body.model_name,
|
|
202
|
+
prompt_name=body.prompt_name,
|
|
203
|
+
anonymized_entities=body.anonymized_entities,
|
|
204
|
+
countries=body.countries,
|
|
205
|
+
opt_in=body.opt_in,
|
|
206
|
+
)
|
|
207
|
+
except ValueError as exc:
|
|
208
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
209
|
+
|
|
210
|
+
@application.post("/deanonymize")
|
|
211
|
+
def deanonymize(body: DeanonymizeBody) -> Dict[str, Any]:
|
|
212
|
+
try:
|
|
213
|
+
return deanonymize_text_request(
|
|
214
|
+
body.text,
|
|
215
|
+
body.mapping,
|
|
216
|
+
encrypt_secret=body.encrypt_secret,
|
|
217
|
+
)
|
|
218
|
+
except ValueError as exc:
|
|
219
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
220
|
+
|
|
221
|
+
@application.post("/verify")
|
|
222
|
+
def verify(body: TextBody) -> Dict[str, Any]:
|
|
223
|
+
try:
|
|
224
|
+
return verify_anonymized_text(
|
|
225
|
+
body.text,
|
|
226
|
+
regex_patterns=filter_regex_patterns(
|
|
227
|
+
body.countries, opt_in=body.opt_in
|
|
228
|
+
),
|
|
229
|
+
use_llm=body.use_llm,
|
|
230
|
+
model_name=body.model_name,
|
|
231
|
+
)
|
|
232
|
+
except ValueError as exc:
|
|
233
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
234
|
+
|
|
235
|
+
@application.post("/report")
|
|
236
|
+
def report(body: TextBody) -> Dict[str, Any]:
|
|
237
|
+
return assess_linkage_risk(body.text)
|
|
238
|
+
|
|
239
|
+
return application
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Process entry point. Does not import the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from pdf_anonymizer_api.app import create_app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description=(
|
|
13
|
+
"Local HTTP service for pdf-anonymizer-core. "
|
|
14
|
+
"No authentication. Default bind is this machine only."
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--host",
|
|
19
|
+
default="127.0.0.1",
|
|
20
|
+
help="Bind address (default 127.0.0.1).",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--port",
|
|
24
|
+
type=int,
|
|
25
|
+
default=8000,
|
|
26
|
+
help="TCP port (default 8000).",
|
|
27
|
+
)
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
import uvicorn
|
|
30
|
+
|
|
31
|
+
uvicorn.run(create_app(), host=args.host, port=args.port)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdf-anonymizer-api
|
|
3
|
+
Version: 0.29.0
|
|
4
|
+
Summary: Local HTTP service for pdf-anonymizer-core. No authentication.
|
|
5
|
+
Author-email: Leonid Ganeline <leo.gan.57@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: repository, https://github.com/leo-gan/anonymizer
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pdf-anonymizer-core>=0.29.0
|
|
11
|
+
Requires-Dist: fastapi>=0.115
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
13
|
+
|
|
14
|
+
# PDF Anonymizer HTTP service
|
|
15
|
+
|
|
16
|
+
A thin FastAPI wrapper around `pdf-anonymizer-core`. It does **not** depend on the CLI. There is no authentication.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pdf-anonymizer-api
|
|
20
|
+
pdf-anonymizer-api --host 127.0.0.1 --port 8000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Docker files live in this package. From the repository root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker compose -f packages/pdf-anonymizer-api/docker-compose.yml up --build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Docs: [HTTP service and Docker](https://leo-gan.github.io/anonymizer/project/http-service/).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/pdf_anonymizer_api/__init__.py
|
|
4
|
+
src/pdf_anonymizer_api/__main__.py
|
|
5
|
+
src/pdf_anonymizer_api/app.py
|
|
6
|
+
src/pdf_anonymizer_api/server.py
|
|
7
|
+
src/pdf_anonymizer_api.egg-info/PKG-INFO
|
|
8
|
+
src/pdf_anonymizer_api.egg-info/SOURCES.txt
|
|
9
|
+
src/pdf_anonymizer_api.egg-info/dependency_links.txt
|
|
10
|
+
src/pdf_anonymizer_api.egg-info/entry_points.txt
|
|
11
|
+
src/pdf_anonymizer_api.egg-info/requires.txt
|
|
12
|
+
src/pdf_anonymizer_api.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pdf_anonymizer_api
|