presidio-structured 0.0.7__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.
- presidio_structured-0.0.7/PKG-INFO +76 -0
- presidio_structured-0.0.7/README.md +53 -0
- presidio_structured-0.0.7/presidio_structured/__init__.py +26 -0
- presidio_structured-0.0.7/presidio_structured/analysis_builder.py +418 -0
- presidio_structured-0.0.7/presidio_structured/config/__init__.py +7 -0
- presidio_structured-0.0.7/presidio_structured/config/structured_analysis.py +20 -0
- presidio_structured-0.0.7/presidio_structured/data/__init__.py +11 -0
- presidio_structured-0.0.7/presidio_structured/data/data_processors.py +222 -0
- presidio_structured-0.0.7/presidio_structured/data/data_reader.py +70 -0
- presidio_structured-0.0.7/presidio_structured/py.typed +1 -0
- presidio_structured-0.0.7/presidio_structured/structured_engine.py +68 -0
- presidio_structured-0.0.7/pyproject.toml +43 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: presidio_structured
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Presidio structured package - analyzes and anonymizes structured and semi-structured data.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: presidio_structured
|
|
7
|
+
Author: Presidio
|
|
8
|
+
Author-email: presidio@microsoft.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
17
|
+
Requires-Dist: pandas (>=1.5.2,<4.0.0)
|
|
18
|
+
Requires-Dist: presidio-analyzer (>=2.2.0,<3.0.0)
|
|
19
|
+
Requires-Dist: presidio-anonymizer (>=2.2.0,<3.0.0)
|
|
20
|
+
Project-URL: Homepage, https://github.com/data-privacy-stack/presidio
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Presidio structured
|
|
24
|
+
|
|
25
|
+
## Description
|
|
26
|
+
|
|
27
|
+
The Presidio structured package is a flexible and customizable framework designed to identify and protect structured sensitive data. This tool extends the capabilities of Presidio, focusing on structured data formats such as tabular formats and semi-structured formats (JSON). It leverages the detection capabilities of Presidio-Analyzer to identify columns or keys containing personally identifiable information (PII), and establishes a mapping between these column/keys names and the detected PII entities. Following the detection, Presidio-Anonymizer is used to apply de-identification techniques to each value in columns identified as containing PII, ensuring the sensitive data is appropriately protected.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### As a python package
|
|
32
|
+
|
|
33
|
+
To install the `presidio-structured` package, run the following command:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install presidio-structured
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Getting started
|
|
40
|
+
|
|
41
|
+
Anonymizing Data Frames:
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
import pandas as pd
|
|
45
|
+
from presidio_structured import StructuredEngine, PandasAnalysisBuilder
|
|
46
|
+
from presidio_anonymizer.entities import OperatorConfig
|
|
47
|
+
from faker import Faker # optionally using faker as an example
|
|
48
|
+
|
|
49
|
+
# Initialize the engine with a Pandas data processor (default)
|
|
50
|
+
pandas_engine = StructuredEngine()
|
|
51
|
+
|
|
52
|
+
# Create a sample DataFrame
|
|
53
|
+
sample_df = pd.DataFrame({'name': ['John Doe', 'Jane Smith'], 'email': ['john.doe@example.com', 'jane.smith@example.com']})
|
|
54
|
+
|
|
55
|
+
# Generate a tabular analysis which describes PII entities in the DataFrame.
|
|
56
|
+
tabular_analysis = PandasAnalysisBuilder().generate_analysis(sample_df)
|
|
57
|
+
|
|
58
|
+
# Define anonymization operators
|
|
59
|
+
fake = Faker()
|
|
60
|
+
operators = {
|
|
61
|
+
"PERSON": OperatorConfig("replace", {"new_value": "REDACTED"}),
|
|
62
|
+
"EMAIL_ADDRESS": OperatorConfig("custom", {"lambda": lambda x: fake.safe_email()})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Anonymize DataFrame
|
|
66
|
+
anonymized_df = pandas_engine.anonymize(sample_df, tabular_analysis, operators=operators)
|
|
67
|
+
print(anonymized_df)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## More information
|
|
71
|
+
|
|
72
|
+
- [Docs](https://presidio.dataprivacystack.org/structured/)
|
|
73
|
+
- [Samples](https://github.com/data-privacy-stack/presidio/blob/main/docs/samples/python/example_structured.ipynb)
|
|
74
|
+
- [Join the discussion](https://github.com/data-privacy-stack/presidio/discussions?discussions_q=structured)
|
|
75
|
+
- [Review issues on Github](https://github.com/data-privacy-stack/presidio/issues?q=is%3Aissue+label%3Astructured-data)
|
|
76
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Presidio structured
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
The Presidio structured package is a flexible and customizable framework designed to identify and protect structured sensitive data. This tool extends the capabilities of Presidio, focusing on structured data formats such as tabular formats and semi-structured formats (JSON). It leverages the detection capabilities of Presidio-Analyzer to identify columns or keys containing personally identifiable information (PII), and establishes a mapping between these column/keys names and the detected PII entities. Following the detection, Presidio-Anonymizer is used to apply de-identification techniques to each value in columns identified as containing PII, ensuring the sensitive data is appropriately protected.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### As a python package
|
|
10
|
+
|
|
11
|
+
To install the `presidio-structured` package, run the following command:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install presidio-structured
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Getting started
|
|
18
|
+
|
|
19
|
+
Anonymizing Data Frames:
|
|
20
|
+
|
|
21
|
+
```py
|
|
22
|
+
import pandas as pd
|
|
23
|
+
from presidio_structured import StructuredEngine, PandasAnalysisBuilder
|
|
24
|
+
from presidio_anonymizer.entities import OperatorConfig
|
|
25
|
+
from faker import Faker # optionally using faker as an example
|
|
26
|
+
|
|
27
|
+
# Initialize the engine with a Pandas data processor (default)
|
|
28
|
+
pandas_engine = StructuredEngine()
|
|
29
|
+
|
|
30
|
+
# Create a sample DataFrame
|
|
31
|
+
sample_df = pd.DataFrame({'name': ['John Doe', 'Jane Smith'], 'email': ['john.doe@example.com', 'jane.smith@example.com']})
|
|
32
|
+
|
|
33
|
+
# Generate a tabular analysis which describes PII entities in the DataFrame.
|
|
34
|
+
tabular_analysis = PandasAnalysisBuilder().generate_analysis(sample_df)
|
|
35
|
+
|
|
36
|
+
# Define anonymization operators
|
|
37
|
+
fake = Faker()
|
|
38
|
+
operators = {
|
|
39
|
+
"PERSON": OperatorConfig("replace", {"new_value": "REDACTED"}),
|
|
40
|
+
"EMAIL_ADDRESS": OperatorConfig("custom", {"lambda": lambda x: fake.safe_email()})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Anonymize DataFrame
|
|
44
|
+
anonymized_df = pandas_engine.anonymize(sample_df, tabular_analysis, operators=operators)
|
|
45
|
+
print(anonymized_df)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## More information
|
|
49
|
+
|
|
50
|
+
- [Docs](https://presidio.dataprivacystack.org/structured/)
|
|
51
|
+
- [Samples](https://github.com/data-privacy-stack/presidio/blob/main/docs/samples/python/example_structured.ipynb)
|
|
52
|
+
- [Join the discussion](https://github.com/data-privacy-stack/presidio/discussions?discussions_q=structured)
|
|
53
|
+
- [Review issues on Github](https://github.com/data-privacy-stack/presidio/issues?q=is%3Aissue+label%3Astructured-data)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""presidio-structured root module."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .analysis_builder import JsonAnalysisBuilder, PandasAnalysisBuilder
|
|
6
|
+
from .config import StructuredAnalysis
|
|
7
|
+
from .data import (
|
|
8
|
+
CsvReader,
|
|
9
|
+
JsonDataProcessor,
|
|
10
|
+
JsonReader,
|
|
11
|
+
PandasDataProcessor,
|
|
12
|
+
)
|
|
13
|
+
from .structured_engine import StructuredEngine
|
|
14
|
+
|
|
15
|
+
logging.getLogger("presidio-structured").addHandler(logging.NullHandler())
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"StructuredEngine",
|
|
19
|
+
"JsonAnalysisBuilder",
|
|
20
|
+
"PandasAnalysisBuilder",
|
|
21
|
+
"StructuredAnalysis",
|
|
22
|
+
"CsvReader",
|
|
23
|
+
"JsonReader",
|
|
24
|
+
"PandasDataProcessor",
|
|
25
|
+
"JsonDataProcessor",
|
|
26
|
+
]
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Dict, Iterator, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from pandas import DataFrame
|
|
8
|
+
from presidio_analyzer import (
|
|
9
|
+
AnalyzerEngine,
|
|
10
|
+
BatchAnalyzerEngine,
|
|
11
|
+
DictAnalyzerResult,
|
|
12
|
+
RecognizerResult,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from presidio_structured.config import StructuredAnalysis
|
|
16
|
+
|
|
17
|
+
NON_PII_ENTITY_TYPE = "NON_PII"
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("presidio-structured")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AnalysisBuilder(ABC):
|
|
23
|
+
"""Abstract base class for a configuration generator."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
analyzer: Optional[AnalyzerEngine] = None,
|
|
28
|
+
analyzer_score_threshold: Optional[float] = None,
|
|
29
|
+
n_process: int = 1,
|
|
30
|
+
batch_size: int = 1
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Initialize the configuration generator.
|
|
33
|
+
|
|
34
|
+
:param analyzer: AnalyzerEngine instance
|
|
35
|
+
:param analyzer_score_threshold: threshold for filtering out results
|
|
36
|
+
:param batch_size: Batch size to process in a single iteration
|
|
37
|
+
:param n_process: Number of processors to use. Defaults to `1`
|
|
38
|
+
"""
|
|
39
|
+
default_score_threshold = (
|
|
40
|
+
analyzer_score_threshold if analyzer_score_threshold is not None else 0
|
|
41
|
+
)
|
|
42
|
+
self.analyzer = (
|
|
43
|
+
AnalyzerEngine(default_score_threshold=default_score_threshold)
|
|
44
|
+
if analyzer is None
|
|
45
|
+
else analyzer
|
|
46
|
+
)
|
|
47
|
+
self.batch_analyzer = BatchAnalyzerEngine(analyzer_engine=self.analyzer)
|
|
48
|
+
self.n_process = n_process
|
|
49
|
+
self.batch_size = batch_size
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def generate_analysis(
|
|
53
|
+
self,
|
|
54
|
+
data: Union[Dict, DataFrame],
|
|
55
|
+
language: str = "en",
|
|
56
|
+
score_threshold: Optional[float] = None,
|
|
57
|
+
) -> StructuredAnalysis:
|
|
58
|
+
"""
|
|
59
|
+
Abstract method to generate a configuration from the given data.
|
|
60
|
+
|
|
61
|
+
:param data: The input data. Can be a dictionary or DataFrame instance.
|
|
62
|
+
:return: The generated configuration.
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def _remove_low_scores(
|
|
67
|
+
self,
|
|
68
|
+
key_recognizer_result_map: Dict[str, RecognizerResult],
|
|
69
|
+
score_threshold: Optional[float] = None,
|
|
70
|
+
) -> Dict[str, RecognizerResult]:
|
|
71
|
+
"""
|
|
72
|
+
Remove results for which the confidence is lower than the threshold.
|
|
73
|
+
|
|
74
|
+
:param results: Dict of column names to RecognizerResult
|
|
75
|
+
:param score_threshold: float value for minimum possible confidence
|
|
76
|
+
:return: List[RecognizerResult]
|
|
77
|
+
"""
|
|
78
|
+
if score_threshold is None:
|
|
79
|
+
score_threshold = self.analyzer.default_score_threshold
|
|
80
|
+
|
|
81
|
+
new_key_recognizer_result_map = {}
|
|
82
|
+
for column, result in key_recognizer_result_map.items():
|
|
83
|
+
if result.score >= score_threshold:
|
|
84
|
+
new_key_recognizer_result_map[column] = result
|
|
85
|
+
|
|
86
|
+
return new_key_recognizer_result_map
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class JsonAnalysisBuilder(AnalysisBuilder):
|
|
90
|
+
"""Concrete configuration generator for JSON data."""
|
|
91
|
+
|
|
92
|
+
def generate_analysis(
|
|
93
|
+
self,
|
|
94
|
+
data: Dict,
|
|
95
|
+
language: str = "en",
|
|
96
|
+
) -> StructuredAnalysis:
|
|
97
|
+
"""
|
|
98
|
+
Generate a configuration from the given JSON data.
|
|
99
|
+
|
|
100
|
+
:param data: The input JSON data.
|
|
101
|
+
:return: The generated configuration.
|
|
102
|
+
"""
|
|
103
|
+
logger.debug("Starting JSON BatchAnalyzer analysis")
|
|
104
|
+
analyzer_results = self.batch_analyzer.analyze_dict(
|
|
105
|
+
input_dict=data,
|
|
106
|
+
language=language,
|
|
107
|
+
n_process=self.n_process,
|
|
108
|
+
batch_size=self.batch_size
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
key_recognizer_result_map = self._generate_analysis_from_results_json(
|
|
112
|
+
analyzer_results
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
key_entity_map = {
|
|
116
|
+
key: result.entity_type for key, result in key_recognizer_result_map.items()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return StructuredAnalysis(entity_mapping=key_entity_map)
|
|
120
|
+
|
|
121
|
+
def _generate_analysis_from_results_json(
|
|
122
|
+
self, analyzer_results: Iterator[DictAnalyzerResult], prefix: str = ""
|
|
123
|
+
) -> Dict[str, RecognizerResult]:
|
|
124
|
+
"""
|
|
125
|
+
Generate a configuration from the given analyzer results. Always uses the first recognizer result if there are more than one.
|
|
126
|
+
|
|
127
|
+
:param analyzer_results: The analyzer results.
|
|
128
|
+
:param prefix: The prefix for the configuration keys.
|
|
129
|
+
:return: The generated configuration.
|
|
130
|
+
""" # noqa: E501
|
|
131
|
+
key_recognizer_result_map = {}
|
|
132
|
+
|
|
133
|
+
if not isinstance(analyzer_results, Iterable):
|
|
134
|
+
logger.debug(
|
|
135
|
+
"No analyzer results found, returning empty StructuredAnalysis"
|
|
136
|
+
)
|
|
137
|
+
return key_recognizer_result_map
|
|
138
|
+
|
|
139
|
+
for result in analyzer_results:
|
|
140
|
+
current_key = prefix + result.key
|
|
141
|
+
|
|
142
|
+
if isinstance(result.value, dict) and isinstance(
|
|
143
|
+
result.recognizer_results, Iterator
|
|
144
|
+
):
|
|
145
|
+
nested_mappings = self._generate_analysis_from_results_json(
|
|
146
|
+
result.recognizer_results, prefix=current_key + "."
|
|
147
|
+
)
|
|
148
|
+
key_recognizer_result_map.update(nested_mappings)
|
|
149
|
+
first_recognizer_result = next(iter(result.recognizer_results), None)
|
|
150
|
+
if isinstance(first_recognizer_result, RecognizerResult):
|
|
151
|
+
logger.debug(
|
|
152
|
+
f"Found result with entity {first_recognizer_result.entity_type} \
|
|
153
|
+
in {current_key}"
|
|
154
|
+
)
|
|
155
|
+
key_recognizer_result_map[current_key] = first_recognizer_result
|
|
156
|
+
return key_recognizer_result_map
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TabularAnalysisBuilder(AnalysisBuilder):
|
|
160
|
+
"""Placeholder class for generalizing tabular data analysis builders (e.g. PySpark). Only implemented as PandasAnalysisBuilder for now.""" # noqa: E501
|
|
161
|
+
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PandasAnalysisBuilder(TabularAnalysisBuilder):
|
|
166
|
+
"""Concrete configuration generator for tabular data."""
|
|
167
|
+
|
|
168
|
+
entity_selection_strategies = {"highest_confidence", "mixed", "most_common"}
|
|
169
|
+
|
|
170
|
+
def generate_analysis(
|
|
171
|
+
self,
|
|
172
|
+
df: DataFrame,
|
|
173
|
+
n: Optional[int] = None,
|
|
174
|
+
language: str = "en",
|
|
175
|
+
selection_strategy: str = "most_common",
|
|
176
|
+
mixed_strategy_threshold: float = 0.5,
|
|
177
|
+
) -> StructuredAnalysis:
|
|
178
|
+
"""
|
|
179
|
+
Generate a configuration from the given tabular data.
|
|
180
|
+
|
|
181
|
+
:param df: The input tabular data (dataframe).
|
|
182
|
+
:param n: The number of samples to be taken from the dataframe.
|
|
183
|
+
:param language: The language to be used for analysis.
|
|
184
|
+
:param selection_strategy: A string that specifies the entity selection strategy
|
|
185
|
+
('highest_confidence', 'mixed', or default to most common).
|
|
186
|
+
:param mixed_strategy_threshold: A float value for the threshold to be used in
|
|
187
|
+
the entity selection mixed strategy.
|
|
188
|
+
:return: A StructuredAnalysis object containing the analysis results.
|
|
189
|
+
"""
|
|
190
|
+
if not n:
|
|
191
|
+
n = len(df)
|
|
192
|
+
elif n > len(df):
|
|
193
|
+
logger.debug(
|
|
194
|
+
f"Number of samples ({n}) is larger than the number of rows \
|
|
195
|
+
({len(df)}), using all rows"
|
|
196
|
+
)
|
|
197
|
+
n = len(df)
|
|
198
|
+
|
|
199
|
+
df = df.sample(n, random_state=123)
|
|
200
|
+
|
|
201
|
+
key_recognizer_result_map = self._generate_key_rec_results_map(
|
|
202
|
+
df, language, selection_strategy, mixed_strategy_threshold
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
key_entity_map = {
|
|
206
|
+
key: result.entity_type
|
|
207
|
+
for key, result in key_recognizer_result_map.items()
|
|
208
|
+
if result.entity_type != NON_PII_ENTITY_TYPE
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return StructuredAnalysis(entity_mapping=key_entity_map)
|
|
212
|
+
|
|
213
|
+
def _generate_key_rec_results_map(
|
|
214
|
+
self,
|
|
215
|
+
df: DataFrame,
|
|
216
|
+
language: str,
|
|
217
|
+
selection_strategy: str = "most_common",
|
|
218
|
+
mixed_strategy_threshold: float = 0.5,
|
|
219
|
+
) -> Dict[str, RecognizerResult]:
|
|
220
|
+
"""
|
|
221
|
+
Find the most common entity in a dataframe column.
|
|
222
|
+
|
|
223
|
+
If more than one entity is found in a cell, the first one is used.
|
|
224
|
+
|
|
225
|
+
:param df: The dataframe where entities will be searched.
|
|
226
|
+
:param language: Language to be used in the analysis engine.
|
|
227
|
+
:param selection_strategy: A string that specifies the entity selection strategy
|
|
228
|
+
('highest_confidence', 'mixed', or default to most common).
|
|
229
|
+
:param mixed_strategy_threshold: A float value for the threshold to be used in
|
|
230
|
+
the entity selection mixed strategy.
|
|
231
|
+
:return: A dictionary mapping column names to the most common RecognizerResult.
|
|
232
|
+
"""
|
|
233
|
+
column_analyzer_results_map = self._batch_analyze_df(df, language)
|
|
234
|
+
key_recognizer_result_map = {}
|
|
235
|
+
for column, analyzer_result in column_analyzer_results_map.items():
|
|
236
|
+
key_recognizer_result_map[column] = self._find_entity_based_on_strategy(
|
|
237
|
+
analyzer_result, selection_strategy, mixed_strategy_threshold
|
|
238
|
+
)
|
|
239
|
+
return key_recognizer_result_map
|
|
240
|
+
|
|
241
|
+
def _batch_analyze_df(
|
|
242
|
+
self, df: DataFrame, language: str
|
|
243
|
+
) -> Dict[str, List[List[RecognizerResult]]]:
|
|
244
|
+
"""
|
|
245
|
+
Analyze each column in the dataframe for entities using the batch analyzer.
|
|
246
|
+
|
|
247
|
+
:param df: The dataframe to be analyzed.
|
|
248
|
+
:param language: The language configuration for the analyzer.
|
|
249
|
+
:return: A dictionary mapping each column name to a \
|
|
250
|
+
list of lists of RecognizerResults.
|
|
251
|
+
"""
|
|
252
|
+
column_analyzer_results_map = {}
|
|
253
|
+
for column in df.columns:
|
|
254
|
+
logger.debug(f"Finding most common PII entity for column {column}")
|
|
255
|
+
analyzer_results = self.batch_analyzer.analyze_iterator(
|
|
256
|
+
[val for val in df[column]],
|
|
257
|
+
language=language,
|
|
258
|
+
n_process=self.n_process,
|
|
259
|
+
batch_size=self.batch_size
|
|
260
|
+
)
|
|
261
|
+
column_analyzer_results_map[column] = analyzer_results
|
|
262
|
+
|
|
263
|
+
return column_analyzer_results_map
|
|
264
|
+
|
|
265
|
+
def _find_entity_based_on_strategy(
|
|
266
|
+
self,
|
|
267
|
+
analyzer_results: List[List[RecognizerResult]],
|
|
268
|
+
selection_strategy: str,
|
|
269
|
+
mixed_strategy_threshold: float,
|
|
270
|
+
) -> RecognizerResult:
|
|
271
|
+
"""
|
|
272
|
+
Determine the most suitable entity based on the specified selection strategy.
|
|
273
|
+
|
|
274
|
+
:param analyzer_results: A nested list of RecognizerResult objects from the
|
|
275
|
+
analysis results.
|
|
276
|
+
:param selection_strategy: A string that specifies the entity selection strategy
|
|
277
|
+
('highest_confidence', 'mixed', or default to most common).
|
|
278
|
+
:return: A RecognizerResult object representing the selected entity based on the
|
|
279
|
+
given strategy.
|
|
280
|
+
"""
|
|
281
|
+
if selection_strategy not in self.entity_selection_strategies:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"Unsupported entity selection strategy: {selection_strategy}."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if not any(analyzer_results):
|
|
287
|
+
return RecognizerResult(
|
|
288
|
+
entity_type=NON_PII_ENTITY_TYPE, start=0, end=1, score=1.0
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
flat_results = self._flatten_results(analyzer_results)
|
|
292
|
+
|
|
293
|
+
# Select the entity based on the desired strategy
|
|
294
|
+
if selection_strategy == "highest_confidence":
|
|
295
|
+
return self._select_highest_confidence_entity(flat_results)
|
|
296
|
+
elif selection_strategy == "mixed":
|
|
297
|
+
return self._select_mixed_strategy_entity(
|
|
298
|
+
flat_results, mixed_strategy_threshold
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return self._select_most_common_entity(flat_results)
|
|
302
|
+
|
|
303
|
+
def _select_most_common_entity(self, flat_results):
|
|
304
|
+
"""
|
|
305
|
+
Select the most common entity from the flattened analysis results.
|
|
306
|
+
|
|
307
|
+
:param flat_results: A list of tuples containing index and RecognizerResult
|
|
308
|
+
objects from the flattened analysis results.
|
|
309
|
+
:return: A RecognizerResult object for the most commonly found entity type.
|
|
310
|
+
"""
|
|
311
|
+
# Count occurrences of each entity type
|
|
312
|
+
type_counter = Counter(res.entity_type for _, res in flat_results)
|
|
313
|
+
most_common_type, most_common_count = type_counter.most_common(1)[0]
|
|
314
|
+
|
|
315
|
+
# Calculate the score as the proportion of occurrences
|
|
316
|
+
score = most_common_count / len(flat_results)
|
|
317
|
+
|
|
318
|
+
return RecognizerResult(
|
|
319
|
+
entity_type=most_common_type, start=0, end=1, score=score
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _select_highest_confidence_entity(self, flat_results):
|
|
323
|
+
"""
|
|
324
|
+
Select the entity with the highest confidence score.
|
|
325
|
+
|
|
326
|
+
:param flat_results: A list of tuples containing index and RecognizerResult
|
|
327
|
+
objects from the flattened analysis results.
|
|
328
|
+
:return: A RecognizerResult object for the entity with the highest confidence
|
|
329
|
+
score.
|
|
330
|
+
"""
|
|
331
|
+
score_aggregator = self._aggregate_scores(flat_results)
|
|
332
|
+
|
|
333
|
+
# Find the highest score across all entities
|
|
334
|
+
highest_score = max(
|
|
335
|
+
max(scores) for scores in score_aggregator.values() if scores
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Find the entities with the highest score and count their occurrences
|
|
339
|
+
entities_highest_score = {
|
|
340
|
+
entity: scores.count(highest_score)
|
|
341
|
+
for entity, scores in score_aggregator.items()
|
|
342
|
+
if highest_score in scores
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Find the entity(ies) with the most number of high scores
|
|
346
|
+
max_occurrences = max(entities_highest_score.values())
|
|
347
|
+
highest_confidence_entities = [
|
|
348
|
+
entity
|
|
349
|
+
for entity, count in entities_highest_score.items()
|
|
350
|
+
if count == max_occurrences
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
return RecognizerResult(
|
|
354
|
+
entity_type=highest_confidence_entities[0],
|
|
355
|
+
start=0,
|
|
356
|
+
end=1,
|
|
357
|
+
score=highest_score,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _select_mixed_strategy_entity(self, flat_results, mixed_strategy_threshold):
|
|
361
|
+
"""
|
|
362
|
+
Select an entity using a mixed strategy.
|
|
363
|
+
|
|
364
|
+
Chooses an entity based on the highest confidence score if it is above the
|
|
365
|
+
threshold. Otherwise, it defaults to the most common entity.
|
|
366
|
+
|
|
367
|
+
:param flat_results: A list of tuples containing index and RecognizerResult
|
|
368
|
+
objects from the flattened analysis results.
|
|
369
|
+
:return: A RecognizerResult object selected based on the mixed strategy.
|
|
370
|
+
"""
|
|
371
|
+
# Check if mixed strategy threshold is within the valid range
|
|
372
|
+
if not 0 <= mixed_strategy_threshold <= 1:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Invalid mixed strategy threshold: {mixed_strategy_threshold}."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
score_aggregator = self._aggregate_scores(flat_results)
|
|
378
|
+
|
|
379
|
+
# Check if the highest score is greater than threshold and select accordingly
|
|
380
|
+
highest_score = max(
|
|
381
|
+
max(scores) for scores in score_aggregator.values() if scores
|
|
382
|
+
)
|
|
383
|
+
if highest_score > mixed_strategy_threshold:
|
|
384
|
+
return self._select_highest_confidence_entity(flat_results)
|
|
385
|
+
else:
|
|
386
|
+
return self._select_most_common_entity(flat_results)
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _aggregate_scores(flat_results):
|
|
390
|
+
"""
|
|
391
|
+
Aggregate the scores for each entity type from the flattened analysis results.
|
|
392
|
+
|
|
393
|
+
:param flat_results: A list of tuples containing index and RecognizerResult
|
|
394
|
+
objects from the flattened analysis results.
|
|
395
|
+
:return: A dictionary with entity types as keys and lists of scores as values.
|
|
396
|
+
"""
|
|
397
|
+
score_aggregator = {}
|
|
398
|
+
for _, res in flat_results:
|
|
399
|
+
if res.entity_type not in score_aggregator:
|
|
400
|
+
score_aggregator[res.entity_type] = []
|
|
401
|
+
score_aggregator[res.entity_type].append(res.score)
|
|
402
|
+
return score_aggregator
|
|
403
|
+
|
|
404
|
+
@staticmethod
|
|
405
|
+
def _flatten_results(analyzer_results):
|
|
406
|
+
"""
|
|
407
|
+
Flattens a nested lists of RecognizerResult objects into a list of tuples.
|
|
408
|
+
|
|
409
|
+
:param analyzer_results: A nested list of RecognizerResult objects from
|
|
410
|
+
the analysis results.
|
|
411
|
+
:return: A flattened list of tuples containing index and RecognizerResult
|
|
412
|
+
objects.
|
|
413
|
+
"""
|
|
414
|
+
return [
|
|
415
|
+
(cell_idx, res)
|
|
416
|
+
for cell_idx, cell_results in enumerate(analyzer_results)
|
|
417
|
+
for res in cell_results
|
|
418
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Structured Analysis module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class StructuredAnalysis:
|
|
9
|
+
"""
|
|
10
|
+
Dataclass containing entity analysis from structured data.
|
|
11
|
+
|
|
12
|
+
Currently, this class only contains entity mapping.
|
|
13
|
+
|
|
14
|
+
param entity_mapping : dict. Mapping column/key names to entity types, e.g., {
|
|
15
|
+
"person.name": "PERSON",
|
|
16
|
+
"person.address": "LOCATION"
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
entity_mapping: Dict[str, str]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Callable, Dict, List, Union
|
|
4
|
+
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
from presidio_anonymizer.entities import OperatorConfig
|
|
7
|
+
from presidio_anonymizer.operators import OperatorsFactory, OperatorType
|
|
8
|
+
|
|
9
|
+
from presidio_structured.config import StructuredAnalysis
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataProcessorBase(ABC):
|
|
13
|
+
"""Abstract class to handle logic of operations over text using the operators."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
"""Initialize DataProcessorBase object."""
|
|
17
|
+
self.logger = logging.getLogger("presidio-structured")
|
|
18
|
+
|
|
19
|
+
def operate(
|
|
20
|
+
self,
|
|
21
|
+
data: Any,
|
|
22
|
+
structured_analysis: StructuredAnalysis,
|
|
23
|
+
operators: Dict[str, OperatorConfig],
|
|
24
|
+
) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Perform operations over the text using the operators, as per the structured analysis.
|
|
27
|
+
|
|
28
|
+
:param data: Data to be operated on.
|
|
29
|
+
:param structured_analysis: Analysis schema as per the structured data.
|
|
30
|
+
:param operators: Dictionary containing operator configuration objects.
|
|
31
|
+
:return: Data after being operated upon.
|
|
32
|
+
""" # noqa: E501
|
|
33
|
+
key_to_operator_mapping = self._generate_operator_mapping(
|
|
34
|
+
structured_analysis, operators
|
|
35
|
+
)
|
|
36
|
+
return self._process(data, key_to_operator_mapping)
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def _process(
|
|
40
|
+
self,
|
|
41
|
+
data: Union[Dict, DataFrame],
|
|
42
|
+
key_to_operator_mapping: Dict[str, Callable],
|
|
43
|
+
) -> Union[Dict, DataFrame]:
|
|
44
|
+
"""
|
|
45
|
+
Abstract method for subclasses to provide operation implementation.
|
|
46
|
+
|
|
47
|
+
:param data: Data to be operated on.
|
|
48
|
+
:param key_to_operator_mapping: Mapping of keys to operators.
|
|
49
|
+
:return: Operated data.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _create_operator_callable(operator, params):
|
|
55
|
+
def operator_callable(text):
|
|
56
|
+
return operator.operate(params=params, text=text)
|
|
57
|
+
|
|
58
|
+
return operator_callable
|
|
59
|
+
|
|
60
|
+
def _generate_operator_mapping(
|
|
61
|
+
self, config, operators: Dict[str, OperatorConfig]
|
|
62
|
+
) -> Dict[str, Callable]:
|
|
63
|
+
"""
|
|
64
|
+
Generate a mapping of keys to operator callables.
|
|
65
|
+
|
|
66
|
+
:param config: Configuration object containing mapping of entity types to keys.
|
|
67
|
+
:param operators: Dictionary containing operator configuration objects.
|
|
68
|
+
:return: Dictionary mapping keys to operator callables.
|
|
69
|
+
"""
|
|
70
|
+
key_to_operator_mapping = {}
|
|
71
|
+
|
|
72
|
+
operators_factory = OperatorsFactory()
|
|
73
|
+
for key, entity in config.entity_mapping.items():
|
|
74
|
+
self.logger.debug(f"Creating operator for key {key} and entity {entity}")
|
|
75
|
+
operator_config = operators.get(entity, operators.get("DEFAULT", None))
|
|
76
|
+
if operator_config is None:
|
|
77
|
+
raise ValueError(f"Operator for entity {entity} not found")
|
|
78
|
+
# NOTE: hardcoded OperatorType.Anonymize, as this is the only one supported.
|
|
79
|
+
operator = operators_factory.create_operator_class(
|
|
80
|
+
operator_config.operator_name, OperatorType.Anonymize
|
|
81
|
+
)
|
|
82
|
+
operator_callable = self._create_operator_callable(
|
|
83
|
+
operator, operator_config.params
|
|
84
|
+
)
|
|
85
|
+
key_to_operator_mapping[key] = operator_callable
|
|
86
|
+
|
|
87
|
+
return key_to_operator_mapping
|
|
88
|
+
|
|
89
|
+
def _operate_on_text(
|
|
90
|
+
self,
|
|
91
|
+
text_to_operate_on: str,
|
|
92
|
+
operator_callable: Callable,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Operates on the provided text using the operator callable.
|
|
96
|
+
|
|
97
|
+
:param text_to_operate_on: Text to be operated on.
|
|
98
|
+
:param operator_callable: Callable that performs operation on the text.
|
|
99
|
+
:return: Text after operation.
|
|
100
|
+
"""
|
|
101
|
+
return operator_callable(text_to_operate_on)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PandasDataProcessor(DataProcessorBase):
|
|
105
|
+
"""Pandas Data Processor."""
|
|
106
|
+
|
|
107
|
+
def _process(
|
|
108
|
+
self, data: DataFrame, key_to_operator_mapping: Dict[str, Callable]
|
|
109
|
+
) -> DataFrame:
|
|
110
|
+
"""
|
|
111
|
+
Operates on the given pandas DataFrame based on the provided operators.
|
|
112
|
+
|
|
113
|
+
:param data: DataFrame to be operated on.
|
|
114
|
+
:param key_to_operator_mapping: Mapping of keys to operator callables.
|
|
115
|
+
:return: DataFrame after the operation.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
if not isinstance(data, DataFrame):
|
|
119
|
+
raise ValueError("Data must be a pandas DataFrame")
|
|
120
|
+
|
|
121
|
+
for key, operator_callable in key_to_operator_mapping.items():
|
|
122
|
+
self.logger.debug(f"Operating on column {key}")
|
|
123
|
+
for row in data.itertuples(index=True):
|
|
124
|
+
text_to_operate_on = getattr(row, key)
|
|
125
|
+
operated_text = self._operate_on_text(
|
|
126
|
+
text_to_operate_on, operator_callable
|
|
127
|
+
)
|
|
128
|
+
data.at[row.Index, key] = operated_text
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class JsonDataProcessor(DataProcessorBase):
|
|
133
|
+
"""JSON Data Processor, Supports arbitrary nesting of dictionaries and lists."""
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _get_nested_value(data: Union[Dict, List, None], path: List[str]) -> Any:
|
|
137
|
+
"""
|
|
138
|
+
Recursively retrieves the value from nested data using a given path.
|
|
139
|
+
|
|
140
|
+
:param data: Nested data (list or dictionary).
|
|
141
|
+
:param path: List of keys/indexes representing the path.
|
|
142
|
+
:return: Retrieved value.
|
|
143
|
+
"""
|
|
144
|
+
for i, key in enumerate(path):
|
|
145
|
+
if isinstance(data, list):
|
|
146
|
+
if key.isdigit():
|
|
147
|
+
data = data[int(key)]
|
|
148
|
+
else:
|
|
149
|
+
return [
|
|
150
|
+
JsonDataProcessor._get_nested_value(item, path[i:])
|
|
151
|
+
for item in data
|
|
152
|
+
]
|
|
153
|
+
elif isinstance(data, dict):
|
|
154
|
+
data = data.get(key)
|
|
155
|
+
else:
|
|
156
|
+
return data
|
|
157
|
+
return data
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _set_nested_value(data: Union[Dict, List], path: List[str], value: Any) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Recursively sets a value in nested data using a given path.
|
|
163
|
+
|
|
164
|
+
:param data: Nested data (JSON-like).
|
|
165
|
+
:param path: List of keys/indexes representing the path.
|
|
166
|
+
:param value: Value to be set.
|
|
167
|
+
"""
|
|
168
|
+
for i, key in enumerate(path):
|
|
169
|
+
if isinstance(data, list):
|
|
170
|
+
if i + 1 < len(path) and path[i + 1].isdigit():
|
|
171
|
+
idx = int(path[i + 1])
|
|
172
|
+
while len(data) <= idx:
|
|
173
|
+
data.append({})
|
|
174
|
+
data = data[idx]
|
|
175
|
+
continue
|
|
176
|
+
else:
|
|
177
|
+
for item in data:
|
|
178
|
+
JsonDataProcessor._set_nested_value(item, path[i:], value)
|
|
179
|
+
return
|
|
180
|
+
elif isinstance(data, dict):
|
|
181
|
+
if i == len(path) - 1:
|
|
182
|
+
data[key] = value
|
|
183
|
+
else:
|
|
184
|
+
data = data.setdefault(key, {})
|
|
185
|
+
|
|
186
|
+
def _process(
|
|
187
|
+
self,
|
|
188
|
+
data: Union[Dict, List],
|
|
189
|
+
key_to_operator_mapping: Dict[str, Callable],
|
|
190
|
+
) -> Union[Dict, List]:
|
|
191
|
+
"""
|
|
192
|
+
Operates on the given JSON-like data based on the provided configuration.
|
|
193
|
+
|
|
194
|
+
:param data: JSON-like data to be operated on.
|
|
195
|
+
:param key_to_operator_mapping: maps keys to Callable operators.
|
|
196
|
+
:return: JSON-like data after the operation.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
if not isinstance(data, (dict, list)):
|
|
200
|
+
raise ValueError("Data must be a JSON-like object")
|
|
201
|
+
|
|
202
|
+
for key, operator_callable in key_to_operator_mapping.items():
|
|
203
|
+
self.logger.debug(f"Operating on key {key}")
|
|
204
|
+
keys = key.split(".")
|
|
205
|
+
if isinstance(data, list):
|
|
206
|
+
for item in data:
|
|
207
|
+
self._process(item, key_to_operator_mapping)
|
|
208
|
+
else:
|
|
209
|
+
text_to_operate_on = self._get_nested_value(data, keys)
|
|
210
|
+
if text_to_operate_on:
|
|
211
|
+
if isinstance(text_to_operate_on, list):
|
|
212
|
+
for text in text_to_operate_on:
|
|
213
|
+
operated_text = self._operate_on_text(
|
|
214
|
+
text, operator_callable
|
|
215
|
+
)
|
|
216
|
+
self._set_nested_value(data, keys, operated_text)
|
|
217
|
+
else:
|
|
218
|
+
operated_text = self._operate_on_text(
|
|
219
|
+
text_to_operate_on, operator_callable
|
|
220
|
+
)
|
|
221
|
+
self._set_nested_value(data, keys, operated_text)
|
|
222
|
+
return data
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Helper data classes, mostly simple wrappers to ensure consistent user interface."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Union
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReaderBase(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Base class for data readers.
|
|
14
|
+
|
|
15
|
+
This class should not be instantiated directly, instead init a subclass.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def read(self, path: Union[str, Path], **kwargs) -> Any:
|
|
20
|
+
"""
|
|
21
|
+
Extract data from file located at path.
|
|
22
|
+
|
|
23
|
+
:param path: String defining the location of the file to read.
|
|
24
|
+
:return: The data read from the file.
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CsvReader(ReaderBase):
|
|
30
|
+
"""
|
|
31
|
+
Reader for reading csv files.
|
|
32
|
+
|
|
33
|
+
Usage::
|
|
34
|
+
|
|
35
|
+
reader = CsvReader()
|
|
36
|
+
data = reader.read(path="filepath.csv")
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def read(self, path: Union[str, Path], **kwargs) -> pd.DataFrame:
|
|
41
|
+
"""
|
|
42
|
+
Read csv file to pandas dataframe.
|
|
43
|
+
|
|
44
|
+
:param path: String defining the location of the csv file to read.
|
|
45
|
+
:return: Pandas DataFrame with the data read from the csv file.
|
|
46
|
+
"""
|
|
47
|
+
return pd.read_csv(path, **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class JsonReader(ReaderBase):
|
|
51
|
+
"""
|
|
52
|
+
Reader for reading json files.
|
|
53
|
+
|
|
54
|
+
Usage::
|
|
55
|
+
|
|
56
|
+
reader = JsonReader()
|
|
57
|
+
data = reader.read(path="filepath.json")
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def read(self, path: Union[str, Path], **kwargs) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Read json file to dict.
|
|
64
|
+
|
|
65
|
+
:param path: String defining the location of the json file to read.
|
|
66
|
+
:return: dictionary with the data read from the json file.
|
|
67
|
+
"""
|
|
68
|
+
with open(path) as f:
|
|
69
|
+
data = json.load(f, **kwargs)
|
|
70
|
+
return data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# py.typed
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pandas import DataFrame
|
|
5
|
+
from presidio_anonymizer.entities import OperatorConfig
|
|
6
|
+
|
|
7
|
+
from presidio_structured.config import StructuredAnalysis
|
|
8
|
+
from presidio_structured.data.data_processors import (
|
|
9
|
+
DataProcessorBase,
|
|
10
|
+
PandasDataProcessor,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
DEFAULT = "replace"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StructuredEngine:
|
|
17
|
+
"""Class to implement methods for anonymizing tabular data."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, data_processor: Optional[DataProcessorBase] = None) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize the class with a data processor.
|
|
22
|
+
|
|
23
|
+
:param data_processor: Instance of DataProcessorBase.
|
|
24
|
+
"""
|
|
25
|
+
if data_processor is None:
|
|
26
|
+
self.data_processor = PandasDataProcessor()
|
|
27
|
+
else:
|
|
28
|
+
self.data_processor = data_processor
|
|
29
|
+
|
|
30
|
+
self.logger = logging.getLogger("presidio-structured")
|
|
31
|
+
|
|
32
|
+
def anonymize(
|
|
33
|
+
self,
|
|
34
|
+
data: Union[Dict, DataFrame],
|
|
35
|
+
structured_analysis: StructuredAnalysis,
|
|
36
|
+
operators: Union[Dict[str, OperatorConfig], None] = None,
|
|
37
|
+
) -> Union[Dict, DataFrame]:
|
|
38
|
+
"""
|
|
39
|
+
Anonymize the given data using the given configuration.
|
|
40
|
+
|
|
41
|
+
:param data: input data as dictionary or pandas DataFrame.
|
|
42
|
+
:param structured_analysis: structured analysis configuration.
|
|
43
|
+
:param operators: a dictionary of operator configurations, optional.
|
|
44
|
+
:return: Anonymized dictionary or DataFrame.
|
|
45
|
+
"""
|
|
46
|
+
self.logger.debug("Starting anonymization")
|
|
47
|
+
operators = self.__check_or_add_default_operator(operators)
|
|
48
|
+
|
|
49
|
+
return self.data_processor.operate(data, structured_analysis, operators)
|
|
50
|
+
|
|
51
|
+
def __check_or_add_default_operator(
|
|
52
|
+
self, operators: Union[Dict[str, OperatorConfig], None]
|
|
53
|
+
) -> Dict[str, OperatorConfig]:
|
|
54
|
+
"""
|
|
55
|
+
Check if the provided operators dictionary has a default operator. If not, add a default operator.
|
|
56
|
+
|
|
57
|
+
:param operators: dictionary of operator configurations.
|
|
58
|
+
:return: operators dictionary with the default operator added \
|
|
59
|
+
if it was not initially present.
|
|
60
|
+
""" # noqa: E501
|
|
61
|
+
default_operator = OperatorConfig(DEFAULT)
|
|
62
|
+
if not operators:
|
|
63
|
+
self.logger.debug("No operators provided, using default operator")
|
|
64
|
+
return {"DEFAULT": default_operator}
|
|
65
|
+
if not operators.get("DEFAULT"):
|
|
66
|
+
self.logger.debug("No default operator provided, using default operator")
|
|
67
|
+
operators["DEFAULT"] = default_operator
|
|
68
|
+
return operators
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "poetry.core.masonry.api"
|
|
3
|
+
requires = ["poetry-core"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "presidio_structured"
|
|
7
|
+
version = "0.0.7"
|
|
8
|
+
description = "Presidio structured package - analyzes and anonymizes structured and semi-structured data."
|
|
9
|
+
authors = [{name = "Presidio", email = "presidio@microsoft.com"}]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
]
|
|
19
|
+
keywords = ["presidio_structured"]
|
|
20
|
+
urls = {Homepage = "https://github.com/data-privacy-stack/presidio"}
|
|
21
|
+
readme = "README.md"
|
|
22
|
+
requires-python = ">=3.10,<4.0"
|
|
23
|
+
|
|
24
|
+
dependencies = [
|
|
25
|
+
"presidio-analyzer (>=2.2.0,<3.0.0)",
|
|
26
|
+
"presidio-anonymizer (>=2.2.0,<3.0.0)",
|
|
27
|
+
"pandas (>=1.5.2,<4.0.0)",
|
|
28
|
+
"click (>=8.1.0,<9.0.0)",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.poetry.group.dev.dependencies]
|
|
32
|
+
pip = "*"
|
|
33
|
+
ruff = "*"
|
|
34
|
+
pytest = "*"
|
|
35
|
+
pytest-cov = "*"
|
|
36
|
+
pytest-mock = "*"
|
|
37
|
+
python-dotenv = "*"
|
|
38
|
+
pre_commit = "*"
|
|
39
|
+
diff-cover = "*"
|
|
40
|
+
|
|
41
|
+
[tool.coverage.run]
|
|
42
|
+
relative_files = true
|
|
43
|
+
source = ["."]
|