pdf-anonymizer-cli 0.3.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,201 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ import typer
8
+ from dotenv import load_dotenv
9
+ from pdf_anonymizer_core.conf import (
10
+ DEFAULT_CHARACTERS_TO_ANONYMIZE,
11
+ DEFAULT_MODEL_NAME,
12
+ DEFAULT_PROMPT_NAME,
13
+ ModelName,
14
+ ModelProvider,
15
+ PromptEnum,
16
+ get_enum_value,
17
+ )
18
+ from pdf_anonymizer_core.core import anonymize_file
19
+ from pdf_anonymizer_core.prompts import detailed, simple
20
+ from pdf_anonymizer_core.utils import (
21
+ consolidate_mapping,
22
+ deanonymize_file,
23
+ save_results,
24
+ )
25
+ from typing_extensions import Annotated
26
+
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
30
+ handlers=[logging.FileHandler("app.log"), logging.StreamHandler()],
31
+ )
32
+
33
+ app = typer.Typer()
34
+
35
+
36
+ def load_environment() -> None:
37
+ """Load environment variables from .env file if it exists."""
38
+ env_path = Path(__file__).parent.parent.parent / ".env"
39
+ if env_path.exists():
40
+ load_dotenv(env_path)
41
+
42
+
43
+ @app.command()
44
+ def run(
45
+ file_paths: Annotated[
46
+ List[Path],
47
+ typer.Argument(
48
+ help="A list of paths to files to anonymize.",
49
+ exists=True,
50
+ file_okay=True,
51
+ dir_okay=False,
52
+ writable=False,
53
+ readable=True,
54
+ resolve_path=True,
55
+ ),
56
+ ],
57
+ characters_to_anonymize: Annotated[
58
+ int,
59
+ typer.Option(help="Number of characters to send for anonymization in one go."),
60
+ ] = DEFAULT_CHARACTERS_TO_ANONYMIZE,
61
+ prompt_name: Annotated[
62
+ PromptEnum,
63
+ typer.Option(
64
+ help="The name of the prompt to use for anonymization.",
65
+ case_sensitive=False,
66
+ ),
67
+ ] = get_enum_value(PromptEnum, DEFAULT_PROMPT_NAME),
68
+ model_name: Annotated[
69
+ ModelName,
70
+ typer.Option(
71
+ help="The name of the model to use for anonymization.",
72
+ case_sensitive=False,
73
+ ),
74
+ ] = get_enum_value(ModelName, DEFAULT_MODEL_NAME),
75
+ anonymized_entities: Annotated[
76
+ Optional[Path],
77
+ typer.Option(
78
+ "--anonymized-entities",
79
+ help="A file with a list of entities to anonymize.",
80
+ exists=True,
81
+ file_okay=True,
82
+ dir_okay=False,
83
+ writable=False,
84
+ readable=True,
85
+ resolve_path=True,
86
+ ),
87
+ ] = None,
88
+ ) -> None:
89
+ """
90
+ Anonymize one or more files by replacing PII with anonymized placeholders.
91
+
92
+ Args:
93
+ file_paths: List of paths to files to process.
94
+ characters_to_anonymize: Number of characters to process in each chunk.
95
+ prompt_name: The prompt template to use for anonymization.
96
+ model_name: The language model to use for anonymization.
97
+ anonymized_entities: A file with a list of entities to anonymize.
98
+ """
99
+ load_environment()
100
+
101
+ if model_name.provider == ModelProvider.GOOGLE:
102
+ if "gemini" in model_name.value and not os.getenv("GOOGLE_API_KEY"):
103
+ logging.error(
104
+ "Error: GOOGLE_API_KEY not found. Please set it in the .env file."
105
+ )
106
+ sys.exit(1)
107
+
108
+ logging.info(f" --file-paths: {file_paths}")
109
+ logging.info(f" --characters-to-anonymize: {characters_to_anonymize}")
110
+ logging.info(f" --model-name: {model_name.value}")
111
+
112
+ # Select the appropriate prompt template
113
+ prompt_templates: Dict[str, str] = {
114
+ PromptEnum.simple: simple.prompt_template,
115
+ PromptEnum.detailed: detailed.prompt_template,
116
+ }
117
+ prompt_template: str = prompt_templates[prompt_name]
118
+ logging.info(f" --prompt-name: {prompt_name.value}")
119
+
120
+ entities_to_anonymize = None
121
+ if anonymized_entities:
122
+ with open(anonymized_entities, "r") as f:
123
+ entities_to_anonymize = [line.strip() for line in f.readlines()]
124
+ logging.info(f" --anonymized-entities: {entities_to_anonymize}")
125
+
126
+ logging.info(f"Found {len(file_paths)} file(s) to process.")
127
+
128
+ for i, file_path in enumerate(file_paths, 1):
129
+ logging.info("=" * 40)
130
+ logging.info(f"Processing file {i}/{len(file_paths)}: {file_path}")
131
+ full_anonymized_text, final_mapping = anonymize_file(
132
+ str(file_path),
133
+ characters_to_anonymize,
134
+ prompt_template,
135
+ model_name.value,
136
+ entities_to_anonymize,
137
+ )
138
+
139
+ if full_anonymized_text and final_mapping:
140
+ # The mapping from anonymize_file is original -> placeholder.
141
+ # We will standardize on placeholder -> original for subsequent steps.
142
+ placeholder_to_original = {v: k for k, v in final_mapping.items()}
143
+
144
+ logging.info("Consolidating mapping...")
145
+ full_anonymized_text, consolidated_placeholder_map = consolidate_mapping(
146
+ full_anonymized_text, placeholder_to_original
147
+ )
148
+
149
+ anonymized_output_file, mapping_file = save_results(
150
+ full_anonymized_text, consolidated_placeholder_map, str(file_path)
151
+ )
152
+ logging.info(f"Anonymization for {file_path} complete!")
153
+ logging.info(f"Anonymized text saved into '{anonymized_output_file}'")
154
+ logging.info(f"Mapping vocabulary saved into '{mapping_file}'")
155
+
156
+
157
+ @app.command()
158
+ def deanonymize(
159
+ anonymized_file: Annotated[
160
+ Path,
161
+ typer.Argument(
162
+ help="Path to the anonymized file.",
163
+ exists=True,
164
+ file_okay=True,
165
+ dir_okay=False,
166
+ writable=False,
167
+ readable=True,
168
+ resolve_path=True,
169
+ ),
170
+ ],
171
+ mapping_file: Annotated[
172
+ Path,
173
+ typer.Argument(
174
+ help="Path to the mapping file.",
175
+ exists=True,
176
+ file_okay=True,
177
+ dir_okay=False,
178
+ writable=False,
179
+ readable=True,
180
+ resolve_path=True,
181
+ ),
182
+ ],
183
+ ) -> None:
184
+ """
185
+ Deanonymize a file using a mapping file.
186
+
187
+ Args:
188
+ anonymized_file: Path to the anonymized file.
189
+ mapping_file: Path to the mapping file.
190
+ """
191
+ logging.info(f"Deanonymizing '{anonymized_file}' using '{mapping_file}'")
192
+ deanonymized_output_file, stats_file = deanonymize_file(
193
+ str(anonymized_file), str(mapping_file)
194
+ )
195
+ logging.info("Deanonymization complete!")
196
+ logging.info(f"Deanonymized text saved into '{deanonymized_output_file}'")
197
+ logging.info(f"Deanonymization statistics saved into '{stats_file}'")
198
+
199
+
200
+ if __name__ == "__main__":
201
+ app()
@@ -0,0 +1,4 @@
1
+ from pdf_anonymizer_cli.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdf-anonymizer-cli
3
+ Version: 0.3.0
4
+ Summary: CLI for a tool to anonymize PDF, Markdown, and plain text files using LLMs.
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: typer
11
+ Requires-Dist: rich
12
+ Requires-Dist: pdf-anonymizer-core
13
+
14
+ # PDF Anonymizer CLI
15
+
16
+ A command-line interface for anonymizing PDF, Markdown, and plain text files using LLMs.
17
+
18
+ ## Installation
19
+
20
+ This project uses `uv` and is structured as a monorepo. The dependencies for the CLI and its core library are managed at the root of the project.
21
+
22
+ 1. **Install `uv`**: Follow the [official installation instructions](https://astral.sh/docs/uv#installation).
23
+ 2. **Install dependencies from the repository root**:
24
+ ```bash
25
+ # From the repository root
26
+ uv sync
27
+ ```
28
+ This installs the `pdf-anonymizer` executable.
29
+
30
+ ## Environment Variables
31
+
32
+ The CLI will automatically load a `.env` file from the current directory or any parent directory. For consistency, it's recommended to place a single `.env` file at the root of the repository.
33
+
34
+ - `GOOGLE_API_KEY`: Required when using Google's Gemini models.
35
+ - `OLLAMA_HOST`: Optional, defaults to `http://localhost:11434` when using local Ollama models.
36
+
37
+ Example `.env` file:
38
+ ```env
39
+ GOOGLE_API_KEY="YOUR_API_KEY_HERE"
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### Anonymize
45
+
46
+ The `run` command anonymizes one or more files.
47
+
48
+ ```bash
49
+ pdf-anonymizer run FILE_PATH [FILE_PATH ...] \
50
+ [--characters-to-anonymize INTEGER] \
51
+ [--prompt-name {simple|detailed}] \
52
+ [--model-name TEXT] \
53
+ [--anonymized-entities PATH]
54
+ ```
55
+
56
+ **Arguments**:
57
+ - `FILE_PATH`: Path to one or several PDF, Markdown, or text files for anonymization.
58
+
59
+ **Options**:
60
+ - `--characters-to-anonymize INTEGER`: Number of characters to process in each chunk (default: `100000`).
61
+ - `--prompt-name [simple|detailed]`: The prompt template to use (default: `detailed`).
62
+ - `--model-name TEXT`: The language model to use.
63
+ - `--anonymized-entities PATH`: Path to a file with a list of entities to anonymize.
64
+
65
+ **Models**:
66
+ - **Google**: `gemini-2.5-pro`, `gemini-2.5-flash` (default), `gemini-2.5-flash-lite`.
67
+ - **Ollama**: `gemma:7b`, `phi4-mini`.
68
+
69
+ ### Examples
70
+
71
+ **Basic anonymization**:
72
+ ```bash
73
+ pdf-anonymizer run document.pdf
74
+ ```
75
+
76
+ **Custom model and prompt**:
77
+ ```bash
78
+ pdf-anonymizer run notes.md --model-name phi4-mini --prompt-name simple
79
+ ```
80
+
81
+ ### Deanonymize
82
+
83
+ The `deanonymize` command reverts anonymization using a mapping file.
84
+
85
+ ```bash
86
+ pdf-anonymizer deanonymize ANONYMIZED_FILE MAPPING_FILE
87
+ ```
88
+
89
+ **Arguments**:
90
+ - `ANONYMIZED_FILE`: Path to the anonymized text file.
91
+ - `MAPPING_FILE`: Path to the JSON mapping file.
92
+
93
+ **Example**:
94
+ ```bash
95
+ pdf-anonymizer deanonymize \
96
+ data/anonymized/document.anonymized.md \
97
+ data/mappings/document.mapping.json
98
+ ```
99
+
100
+ This will create a deanonymized version of the file at `data/deanonymized/document.deanonymized.md`.
@@ -0,0 +1,8 @@
1
+ pdf_anonymizer_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pdf_anonymizer_cli/cli.py,sha256=4UGhtVMoJf6eCB3_uX7iLl1yGNJ2bbllx47AP-6uCvA,6520
3
+ pdf_anonymizer_cli/main.py,sha256=Mt7A2eMn_VBHS9Ys0j9Op5sQ3g9qglzBFOIySYYWAWk,77
4
+ pdf_anonymizer_cli-0.3.0.dist-info/METADATA,sha256=vMaZmpDHH3btOxE21OQQ2DsSDdLKU5tjduQE7QOuAeE,2989
5
+ pdf_anonymizer_cli-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ pdf_anonymizer_cli-0.3.0.dist-info/entry_points.txt,sha256=jy60k1AqhUvt_At4lCoNmyYE1_xPb8rCeXNxPDBqW9E,62
7
+ pdf_anonymizer_cli-0.3.0.dist-info/top_level.txt,sha256=kkJTXhriYdwOxJRultk1V1CfPL-i0AmIBOPUNGm3-Ys,19
8
+ pdf_anonymizer_cli-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pdf-anonymizer = pdf_anonymizer_cli.cli:app
@@ -0,0 +1 @@
1
+ pdf_anonymizer_cli