11from __future__ import annotations
22
3+ import ast
34import functools
45import logging
56import warnings
@@ -94,15 +95,52 @@ def erase(self, data: tuple, fields: list[str]) -> tuple[str]: ...
9495 @overload
9596 def erase (self , data : dict , fields : list [str ]) -> dict : ...
9697
97- def erase (self , data : Sequence | Mapping , fields : list [str ] | None = None ) -> str | list [str ] | tuple [str ] | dict :
98- return self ._apply_action (data = data , fields = fields , action = self .provider .erase )
98+ @overload
99+ def erase (
100+ self ,
101+ data : dict ,
102+ fields : list [str ],
103+ custom_mask : bool | None = None ,
104+ mask_pattern : str | None = None ,
105+ regex_pattern : str | None = None ,
106+ mask_format : str | None = None ,
107+ ) -> dict : ...
108+
109+ def erase (
110+ self ,
111+ data : Sequence | Mapping ,
112+ fields : list [str ] | None = None ,
113+ custom_mask : bool | None = None ,
114+ mask_pattern : str | None = None ,
115+ regex_pattern : str | None = None ,
116+ mask_format : str | None = None ,
117+ masking_rules : dict | None = None ,
118+ ) -> str | list [str ] | tuple [str ] | dict :
119+ if not data :
120+ return data
121+ if masking_rules :
122+ return self ._apply_masking_rules (data , masking_rules )
123+ else :
124+ return self ._apply_action (
125+ data = data ,
126+ fields = fields ,
127+ action = self .provider .erase ,
128+ custom_mask = custom_mask ,
129+ mask_pattern = mask_pattern ,
130+ regex_pattern = regex_pattern ,
131+ mask_format = mask_format ,
132+ )
99133
100134 def _apply_action (
101135 self ,
102136 data ,
103137 fields : list [str ] | None ,
104138 action : Callable ,
105139 provider_options : dict | None = None ,
140+ custom_mask : bool | None = None ,
141+ mask_pattern : str | None = None ,
142+ regex_pattern : str | None = None ,
143+ mask_format : str | None = None ,
106144 ** encryption_context : str ,
107145 ):
108146 """
@@ -136,18 +174,34 @@ def _apply_action(
136174 fields = fields ,
137175 action = action ,
138176 provider_options = provider_options ,
177+ custom_mask = custom_mask ,
178+ mask_pattern = mask_pattern ,
179+ regex_pattern = regex_pattern ,
180+ mask_format = mask_format ,
139181 ** encryption_context ,
140182 )
141183 else :
142184 logger .debug (f"Running action { action .__name__ } with the entire data" )
143- return action (data = data , provider_options = provider_options , ** encryption_context )
185+ return action (
186+ data = data ,
187+ provider_options = provider_options ,
188+ custom_mask = custom_mask ,
189+ mask_pattern = mask_pattern ,
190+ regex_pattern = regex_pattern ,
191+ mask_format = mask_format ,
192+ ** encryption_context ,
193+ )
144194
145195 def _apply_action_to_fields (
146196 self ,
147197 data : dict | str ,
148198 fields : list ,
149199 action : Callable ,
150200 provider_options : dict | None = None ,
201+ custom_mask : bool | None = None ,
202+ mask_pattern : str | None = None ,
203+ regex_pattern : str | None = None ,
204+ mask_format : str | None = None ,
151205 ** encryption_context : str ,
152206 ) -> dict | str :
153207 """
@@ -194,6 +248,8 @@ def _apply_action_to_fields(
194248 new_dict = {'a': {'b': {'c': '*****'}}, 'x': {'y': '*****'}}
195249 ```
196250 """
251+ if not fields :
252+ raise ValueError ("Fields parameter cannot be empty" )
197253
198254 data_parsed : dict = self ._normalize_data_to_parse (fields , data )
199255
@@ -204,6 +260,10 @@ def _apply_action_to_fields(
204260 self ._call_action ,
205261 action = action ,
206262 provider_options = provider_options ,
263+ custom_mask = custom_mask ,
264+ mask_pattern = mask_pattern ,
265+ regex_pattern = regex_pattern ,
266+ mask_format = mask_format ,
207267 ** encryption_context , # type: ignore[arg-type]
208268 )
209269
@@ -225,12 +285,6 @@ def _apply_action_to_fields(
225285 # For in-place updates, json_parse accepts a callback function
226286 # that receives 3 args: field_value, fields, field_name
227287 # We create a partial callback to pre-populate known provider options (action, provider opts, enc ctx)
228- update_callback = functools .partial (
229- self ._call_action ,
230- action = action ,
231- provider_options = provider_options ,
232- ** encryption_context , # type: ignore[arg-type]
233- )
234288
235289 json_parse .update (
236290 data_parsed ,
@@ -239,13 +293,60 @@ def _apply_action_to_fields(
239293
240294 return data_parsed
241295
296+ def _apply_masking_rules (self , data : dict , masking_rules : dict ) -> dict :
297+ """
298+ Apply masking rules to data, supporting different rules for each field.
299+ """
300+ result = data .copy ()
301+
302+ for path , rule in masking_rules .items ():
303+ try :
304+ # Handle nested paths (e.g., 'address.street')
305+ parts = path .split ("." )
306+ current = result
307+
308+ for part in parts [:- 1 ]:
309+ if isinstance (current [part ], str ) and current [part ].startswith ("{" ):
310+ try :
311+ current [part ] = ast .literal_eval (current [part ])
312+ except (ValueError , SyntaxError ):
313+ continue
314+ current = current [part ]
315+
316+ final_field = parts [- 1 ]
317+
318+ # Apply masking rule to the target field
319+ if final_field in current :
320+ current [final_field ] = self .provider .erase (str (current [final_field ]), ** rule )
321+
322+ except (KeyError , TypeError , AttributeError ):
323+ # Log warning if field not found or invalid path
324+ warnings .warn (f"Could not apply masking rule for path: { path } " , stacklevel = 2 )
325+ continue
326+
327+ return result
328+
329+ def _mask_nested_field (self , data : dict , field_path : str , mask_function ):
330+ keys = field_path .split ("." )
331+ current = data
332+ for key in keys [:- 1 ]:
333+ current = current .get (key , {})
334+ if not isinstance (current , dict ):
335+ return # Caminho inválido
336+ if keys [- 1 ] in current :
337+ current [keys [- 1 ]] = mask_function (current [keys [- 1 ]])
338+
242339 @staticmethod
243340 def _call_action (
244341 field_value : Any ,
245342 fields : dict [str , Any ],
246343 field_name : str ,
247344 action : Callable ,
248345 provider_options : dict [str , Any ] | None = None ,
346+ custom_mask : bool | None = None ,
347+ mask_pattern : str | None = None ,
348+ regex_pattern : str | None = None ,
349+ mask_format : str | None = None ,
249350 ** encryption_context ,
250351 ) -> None :
251352 """
@@ -263,7 +364,15 @@ def _call_action(
263364 Returns:
264365 - fields[field_name]: Returns the processed field value
265366 """
266- fields [field_name ] = action (field_value , provider_options = provider_options , ** encryption_context )
367+ fields [field_name ] = action (
368+ field_value ,
369+ provider_options = provider_options ,
370+ custom_mask = custom_mask ,
371+ mask_pattern = mask_pattern ,
372+ regex_pattern = regex_pattern ,
373+ mask_format = mask_format ,
374+ ** encryption_context ,
375+ )
267376 return fields [field_name ]
268377
269378 def _normalize_data_to_parse (self , fields : list , data : str | dict ) -> dict :
0 commit comments