|
6 | 6 |
|
7 | 7 | from __future__ import annotations
|
8 | 8 |
|
| 9 | +import dataclasses |
9 | 10 | import functools
|
10 | 11 | import logging
|
11 | 12 | import warnings
|
|
27 | 28 | logger = logging.getLogger(__name__)
|
28 | 29 |
|
29 | 30 |
|
| 31 | +def prepare_data(data: Any, _visited: set[int] | None = None) -> Any: |
| 32 | + """ |
| 33 | + Recursively convert complex objects into dictionaries or simple types. |
| 34 | + Handles dataclasses, Pydantic models, and prevents circular references. |
| 35 | + """ |
| 36 | + _visited = _visited or set() |
| 37 | + |
| 38 | + # Handle circular references and primitive types |
| 39 | + data_id = id(data) |
| 40 | + if data_id in _visited or isinstance(data, (str, int, float, bool, type(None))): |
| 41 | + return data |
| 42 | + |
| 43 | + _visited.add(data_id) |
| 44 | + |
| 45 | + # Define handlers as (condition, transformer) pairs |
| 46 | + handlers: list[tuple[Callable[[Any], bool], Callable[[Any], Any]]] = [ |
| 47 | + # Dataclasses |
| 48 | + (lambda x: hasattr(x, "__dataclass_fields__"), lambda x: prepare_data(dataclasses.asdict(x), _visited)), |
| 49 | + # Pydantic models |
| 50 | + (lambda x: callable(getattr(x, "model_dump", None)), lambda x: prepare_data(x.model_dump(), _visited)), |
| 51 | + # Objects with dict() method |
| 52 | + ( |
| 53 | + lambda x: callable(getattr(x, "dict", None)) and not isinstance(x, dict), |
| 54 | + lambda x: prepare_data(x.dict(), _visited), |
| 55 | + ), |
| 56 | + # Dictionaries |
| 57 | + ( |
| 58 | + lambda x: isinstance(x, dict), |
| 59 | + lambda x: {prepare_data(k, _visited): prepare_data(v, _visited) for k, v in x.items()}, |
| 60 | + ), |
| 61 | + # Lists, tuples, sets |
| 62 | + (lambda x: isinstance(x, (list, tuple, set)), lambda x: type(x)(prepare_data(item, _visited) for item in x)), |
| 63 | + # Objects with __dict__ |
| 64 | + (lambda x: hasattr(x, "__dict__"), lambda x: prepare_data(vars(x), _visited)), |
| 65 | + ] |
| 66 | + |
| 67 | + # Find and apply the first matching handler |
| 68 | + for condition, transformer in handlers: |
| 69 | + if condition(data): |
| 70 | + return transformer(data) |
| 71 | + |
| 72 | + # Default fallback |
| 73 | + return data |
| 74 | + |
| 75 | + |
30 | 76 | class DataMasking:
|
31 | 77 | """
|
32 | 78 | The DataMasking class orchestrates erasing, encrypting, and decrypting
|
@@ -93,6 +139,7 @@ def encrypt(
|
93 | 139 | data_masker = DataMasking(provider=encryption_provider)
|
94 | 140 | encrypted = data_masker.encrypt({"secret": "value"})
|
95 | 141 | """
|
| 142 | + data = prepare_data(data) |
96 | 143 | return self._apply_action(
|
97 | 144 | data=data,
|
98 | 145 | fields=None,
|
@@ -135,7 +182,7 @@ def decrypt(
|
135 | 182 | data_masker = DataMasking(provider=encryption_provider)
|
136 | 183 | encrypted = data_masker.decrypt(encrypted_data)
|
137 | 184 | """
|
138 |
| - |
| 185 | + data = prepare_data(data) |
139 | 186 | return self._apply_action(
|
140 | 187 | data=data,
|
141 | 188 | fields=None,
|
@@ -184,6 +231,7 @@ def erase(
|
184 | 231 | Any
|
185 | 232 | The data with sensitive information erased or masked.
|
186 | 233 | """
|
| 234 | + data = prepare_data(data) |
187 | 235 | if masking_rules:
|
188 | 236 | return self._apply_masking_rules(data=data, masking_rules=masking_rules)
|
189 | 237 | else:
|
|
0 commit comments