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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Local HTTP service for pdf-anonymizer-core."""
2
+
3
+ from pdf_anonymizer_api.app import create_app
4
+
5
+ __all__ = ["create_app"]
@@ -0,0 +1,4 @@
1
+ from pdf_anonymizer_api.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ pdf-anonymizer-api = pdf_anonymizer_api.server:main
@@ -0,0 +1,3 @@
1
+ pdf-anonymizer-core>=0.29.0
2
+ fastapi>=0.115
3
+ uvicorn[standard]>=0.30