7
7
from abc import ABC , abstractmethod
8
8
from collections import namedtuple
9
9
from datetime import datetime , timedelta
10
- from typing import TYPE_CHECKING , Any , Dict , Optional , Tuple , Type , Union
10
+ from typing import (
11
+ TYPE_CHECKING ,
12
+ Any ,
13
+ Callable ,
14
+ Dict ,
15
+ Optional ,
16
+ Tuple ,
17
+ Type ,
18
+ Union ,
19
+ cast ,
20
+ overload ,
21
+ )
11
22
12
23
import boto3
13
24
from botocore .config import Config
14
25
26
+ from aws_lambda_powertools .utilities .parameters .types import TransformOptions
27
+
15
28
from .exceptions import GetParameterError , TransformParameterError
16
29
17
30
if TYPE_CHECKING :
30
43
SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON , TRANSFORM_METHOD_BINARY ]
31
44
ParameterClients = Union ["AppConfigDataClient" , "SecretsManagerClient" , "SSMClient" ]
32
45
46
+ TRANSFORM_METHOD_MAPPING = {
47
+ TRANSFORM_METHOD_JSON : json .loads ,
48
+ TRANSFORM_METHOD_BINARY : base64 .b64decode ,
49
+ ".json" : json .loads ,
50
+ ".binary" : base64 .b64decode ,
51
+ None : lambda x : x ,
52
+ }
53
+
33
54
34
55
class BaseProvider (ABC ):
35
56
"""
@@ -52,7 +73,7 @@ def get(
52
73
self ,
53
74
name : str ,
54
75
max_age : int = DEFAULT_MAX_AGE_SECS ,
55
- transform : Optional [ str ] = None ,
76
+ transform : TransformOptions = None ,
56
77
force_fetch : bool = False ,
57
78
** sdk_options ,
58
79
) -> Optional [Union [str , dict , bytes ]]:
@@ -107,7 +128,7 @@ def get(
107
128
if transform :
108
129
if isinstance (value , bytes ):
109
130
value = value .decode ("utf-8" )
110
- value = transform_value (value , transform )
131
+ value = transform_value (value , transform , raise_on_transform_error = True )
111
132
112
133
self .store [key ] = ExpirableValue (value , datetime .now () + timedelta (seconds = max_age ))
113
134
@@ -124,7 +145,7 @@ def get_multiple(
124
145
self ,
125
146
path : str ,
126
147
max_age : int = DEFAULT_MAX_AGE_SECS ,
127
- transform : Optional [ str ] = None ,
148
+ transform : TransformOptions = None ,
128
149
raise_on_transform_error : bool = False ,
129
150
force_fetch : bool = False ,
130
151
** sdk_options ,
@@ -170,13 +191,8 @@ def get_multiple(
170
191
raise GetParameterError (str (exc ))
171
192
172
193
if transform :
173
- transformed_values : dict = {}
174
- for (item , value ) in values .items ():
175
- _transform = get_transform_method (item , transform )
176
- if not _transform :
177
- continue
178
- transformed_values [item ] = transform_value (value , _transform , raise_on_transform_error )
179
- values .update (transformed_values )
194
+ values .update (transform_value (values , transform , raise_on_transform_error ))
195
+
180
196
self .store [key ] = ExpirableValue (values , datetime .now () + timedelta (seconds = max_age ))
181
197
182
198
return values
@@ -258,7 +274,7 @@ def _build_boto3_resource_client(
258
274
return session .resource (service_name = service_name , config = config , endpoint_url = endpoint_url )
259
275
260
276
261
- def get_transform_method (key : str , transform : Optional [ str ] = None ) -> Optional [ str ]:
277
+ def get_transform_method (key : str , transform : TransformOptions = None ) -> Callable [..., Any ]:
262
278
"""
263
279
Determine the transform method
264
280
@@ -278,37 +294,50 @@ def get_transform_method(key: str, transform: Optional[str] = None) -> Optional[
278
294
Parameters
279
295
---------
280
296
key: str
281
- Only used when the tranform is "auto".
297
+ Only used when the transform is "auto".
282
298
transform: str, optional
283
299
Original transform method, only "auto" will try to detect the transform method by the key
284
300
285
301
Returns
286
302
------
287
- Optional[str]:
288
- The transform method either when transform is "auto" then None, "json" or "binary" is returned
289
- or the original transform method
303
+ Callable:
304
+ Transform function could be json.loads, base64.b64decode, or a lambda that echo the str value
290
305
"""
291
- if transform != "auto" :
292
- return transform
306
+ transform_method = TRANSFORM_METHOD_MAPPING .get (transform )
307
+
308
+ if transform == "auto" :
309
+ key_suffix = key .rsplit ("." )[- 1 ]
310
+ transform_method = TRANSFORM_METHOD_MAPPING .get (key_suffix , TRANSFORM_METHOD_MAPPING [None ])
293
311
294
- for transform_method in SUPPORTED_TRANSFORM_METHODS :
295
- if key .endswith ("." + transform_method ):
296
- return transform_method
297
- return None
312
+ return cast (Callable , transform_method ) # https://github.com/python/mypy/issues/10740
298
313
299
314
315
+ @overload
300
316
def transform_value (
301
- value : str , transform : str , raise_on_transform_error : Optional [bool ] = True
302
- ) -> Optional [Union [dict , bytes ]]:
317
+ value : Dict [str , Any ], transform : TransformOptions , raise_on_transform_error : bool = False
318
+ ) -> Dict [str , Any ]:
319
+ ...
320
+
321
+
322
+ @overload
323
+ def transform_value (
324
+ value : Union [str , bytes , Dict [str , Any ]], transform : TransformOptions , raise_on_transform_error : bool = False
325
+ ) -> Optional [Union [str , bytes , Dict [str , Any ]]]:
326
+ ...
327
+
328
+
329
+ def transform_value (
330
+ value : Union [str , bytes , Dict [str , Any ]], transform : TransformOptions , raise_on_transform_error : bool = True
331
+ ) -> Optional [Union [str , bytes , Dict [str , Any ]]]:
303
332
"""
304
- Apply a transform to a value
333
+ Transform a value using one of the available options.
305
334
306
335
Parameters
307
336
---------
308
337
value: str
309
338
Parameter value to transform
310
339
transform: str
311
- Type of transform, supported values are "json" and "binary"
340
+ Type of transform, supported values are "json", "binary", and "auto" based on suffix (.json, .binary)
312
341
raise_on_transform_error: bool, optional
313
342
Raises an exception if any transform fails, otherwise this will
314
343
return a None value for each transform that failed
@@ -318,18 +347,35 @@ def transform_value(
318
347
TransformParameterError:
319
348
When the parameter value could not be transformed
320
349
"""
350
+ # Maintenance: For v3, we should consider returning the original value for soft transform failures.
321
351
322
- try :
323
- if transform == TRANSFORM_METHOD_JSON :
324
- return json .loads (value )
325
- elif transform == TRANSFORM_METHOD_BINARY :
326
- return base64 .b64decode (value )
327
- else :
328
- raise ValueError (f"Invalid transform type '{ transform } '" )
352
+ err_msg = "Unable to transform value using '{transform}' transform: {exc}"
353
+
354
+ if isinstance (value , bytes ):
355
+ value = value .decode ("utf-8" )
329
356
357
+ if isinstance (value , dict ):
358
+ # NOTE: We must handle partial failures when receiving multiple values
359
+ # where one of the keys might fail during transform, e.g. `{"a": "valid", "b": "{"}`
360
+ # expected: `{"a": "valid", "b": None}`
361
+
362
+ transformed_values : Dict [str , Any ] = {}
363
+ for dict_key , dict_value in value .items ():
364
+ transform_method = get_transform_method (key = dict_key , transform = transform )
365
+ try :
366
+ transformed_values [dict_key ] = transform_method (dict_value )
367
+ except Exception as exc :
368
+ if raise_on_transform_error :
369
+ raise TransformParameterError (err_msg .format (transform = transform , exc = exc )) from exc
370
+ transformed_values [dict_key ] = None
371
+ return transformed_values
372
+
373
+ try :
374
+ transform_method = get_transform_method (key = value , transform = transform )
375
+ return transform_method (value )
330
376
except Exception as exc :
331
377
if raise_on_transform_error :
332
- raise TransformParameterError (str ( exc ))
378
+ raise TransformParameterError (err_msg . format ( transform = transform , exc = exc )) from exc
333
379
return None
334
380
335
381
0 commit comments