1
+ from __future__ import annotations
2
+
1
3
import logging
2
- from typing import Any , Dict , List , Optional , Union , cast
4
+ from typing import Any , Callable , Dict , List , Optional , TypeVar , Union , cast
5
+
6
+ from typing_extensions import ParamSpec
3
7
4
8
from ... import Logger
5
9
from ...shared .types import JSONType
6
10
from . import schema
7
11
from .base import StoreProvider
8
12
from .comparators import (
13
+ compare_all_in_list ,
14
+ compare_any_in_list ,
9
15
compare_datetime_range ,
10
16
compare_days_of_week ,
11
17
compare_modulo_range ,
18
+ compare_none_in_list ,
12
19
compare_time_range ,
13
- compare_all_in_list ,
14
- compare_any_in_list ,
15
- compare_none_in_list
16
20
)
17
21
from .exceptions import ConfigurationStoreError
18
22
23
+ T = TypeVar ("T" )
24
+ P = ParamSpec ("P" )
25
+
26
+ RULE_ACTION_MAPPING = {
27
+ schema .RuleAction .EQUALS .value : lambda a , b : a == b ,
28
+ schema .RuleAction .NOT_EQUALS .value : lambda a , b : a != b ,
29
+ schema .RuleAction .KEY_GREATER_THAN_VALUE .value : lambda a , b : a > b ,
30
+ schema .RuleAction .KEY_GREATER_THAN_OR_EQUAL_VALUE .value : lambda a , b : a >= b ,
31
+ schema .RuleAction .KEY_LESS_THAN_VALUE .value : lambda a , b : a < b ,
32
+ schema .RuleAction .KEY_LESS_THAN_OR_EQUAL_VALUE .value : lambda a , b : a <= b ,
33
+ schema .RuleAction .STARTSWITH .value : lambda a , b : a .startswith (b ),
34
+ schema .RuleAction .ENDSWITH .value : lambda a , b : a .endswith (b ),
35
+ schema .RuleAction .IN .value : lambda a , b : a in b ,
36
+ schema .RuleAction .NOT_IN .value : lambda a , b : a not in b ,
37
+ schema .RuleAction .KEY_IN_VALUE .value : lambda a , b : a in b ,
38
+ schema .RuleAction .KEY_NOT_IN_VALUE .value : lambda a , b : a not in b ,
39
+ schema .RuleAction .VALUE_IN_KEY .value : lambda a , b : b in a ,
40
+ schema .RuleAction .VALUE_NOT_IN_KEY .value : lambda a , b : b not in a ,
41
+ schema .RuleAction .ALL_IN_VALUE .value : lambda a , b : compare_all_in_list (a , b ),
42
+ schema .RuleAction .ANY_IN_VALUE .value : lambda a , b : compare_any_in_list (a , b ),
43
+ schema .RuleAction .NONE_IN_VALUE .value : lambda a , b : compare_none_in_list (a , b ),
44
+ schema .RuleAction .SCHEDULE_BETWEEN_TIME_RANGE .value : lambda a , b : compare_time_range (a , b ),
45
+ schema .RuleAction .SCHEDULE_BETWEEN_DATETIME_RANGE .value : lambda a , b : compare_datetime_range (a , b ),
46
+ schema .RuleAction .SCHEDULE_BETWEEN_DAYS_OF_WEEK .value : lambda a , b : compare_days_of_week (a , b ),
47
+ schema .RuleAction .MODULO_RANGE .value : lambda a , b : compare_modulo_range (a , b ),
48
+ }
49
+
19
50
20
51
class FeatureFlags :
21
52
def __init__ (self , store : StoreProvider , logger : Optional [Union [logging .Logger , Logger ]] = None ):
@@ -49,37 +80,20 @@ def __init__(self, store: StoreProvider, logger: Optional[Union[logging.Logger,
49
80
"""
50
81
self .store = store
51
82
self .logger = logger or logging .getLogger (__name__ )
83
+ self ._exception_handlers : dict [Exception , Callable ] = {}
52
84
53
85
def _match_by_action (self , action : str , condition_value : Any , context_value : Any ) -> bool :
54
- mapping_by_action = {
55
- schema .RuleAction .EQUALS .value : lambda a , b : a == b ,
56
- schema .RuleAction .NOT_EQUALS .value : lambda a , b : a != b ,
57
- schema .RuleAction .KEY_GREATER_THAN_VALUE .value : lambda a , b : a > b ,
58
- schema .RuleAction .KEY_GREATER_THAN_OR_EQUAL_VALUE .value : lambda a , b : a >= b ,
59
- schema .RuleAction .KEY_LESS_THAN_VALUE .value : lambda a , b : a < b ,
60
- schema .RuleAction .KEY_LESS_THAN_OR_EQUAL_VALUE .value : lambda a , b : a <= b ,
61
- schema .RuleAction .STARTSWITH .value : lambda a , b : a .startswith (b ),
62
- schema .RuleAction .ENDSWITH .value : lambda a , b : a .endswith (b ),
63
- schema .RuleAction .IN .value : lambda a , b : a in b ,
64
- schema .RuleAction .NOT_IN .value : lambda a , b : a not in b ,
65
- schema .RuleAction .KEY_IN_VALUE .value : lambda a , b : a in b ,
66
- schema .RuleAction .KEY_NOT_IN_VALUE .value : lambda a , b : a not in b ,
67
- schema .RuleAction .VALUE_IN_KEY .value : lambda a , b : b in a ,
68
- schema .RuleAction .VALUE_NOT_IN_KEY .value : lambda a , b : b not in a ,
69
- schema .RuleAction .ALL_IN_VALUE .value : lambda a , b : compare_all_in_list (a , b ),
70
- schema .RuleAction .ANY_IN_VALUE .value : lambda a , b : compare_any_in_list (a , b ),
71
- schema .RuleAction .NONE_IN_VALUE .value : lambda a , b : compare_none_in_list (a , b ),
72
- schema .RuleAction .SCHEDULE_BETWEEN_TIME_RANGE .value : lambda a , b : compare_time_range (a , b ),
73
- schema .RuleAction .SCHEDULE_BETWEEN_DATETIME_RANGE .value : lambda a , b : compare_datetime_range (a , b ),
74
- schema .RuleAction .SCHEDULE_BETWEEN_DAYS_OF_WEEK .value : lambda a , b : compare_days_of_week (a , b ),
75
- schema .RuleAction .MODULO_RANGE .value : lambda a , b : compare_modulo_range (a , b ),
76
- }
77
-
78
86
try :
79
- func = mapping_by_action .get (action , lambda a , b : False )
87
+ func = RULE_ACTION_MAPPING .get (action , lambda a , b : False )
80
88
return func (context_value , condition_value )
81
89
except Exception as exc :
82
90
self .logger .debug (f"caught exception while matching action: action={ action } , exception={ str (exc )} " )
91
+
92
+ handler = self ._lookup_exception_handler (exc )
93
+ if handler :
94
+ self .logger .debug ("Exception handler found! Delegating response." )
95
+ return handler (exc )
96
+
83
97
return False
84
98
85
99
def _evaluate_conditions (
@@ -209,6 +223,22 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
209
223
2. Feature exists but has either no rules or no match, return feature default value
210
224
3. Feature doesn't exist in stored schema, encountered an error when fetching -> return default value provided
211
225
226
+ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
227
+ │ Feature flags │──────▶ Get Configuration ├───────▶ Evaluate rules │
228
+ └────────────────────────┘ │ │ │ │
229
+ │┌──────────────────────┐│ │┌──────────────────────┐│
230
+ ││ Fetch schema ││ ││ Match rule ││
231
+ │└───────────┬──────────┘│ │└───────────┬──────────┘│
232
+ │ │ │ │ │ │
233
+ │┌───────────▼──────────┐│ │┌───────────▼──────────┐│
234
+ ││ Cache schema ││ ││ Match condition ││
235
+ │└───────────┬──────────┘│ │└───────────┬──────────┘│
236
+ │ │ │ │ │ │
237
+ │┌───────────▼──────────┐│ │┌───────────▼──────────┐│
238
+ ││ Validate schema ││ ││ Match action ││
239
+ │└──────────────────────┘│ │└──────────────────────┘│
240
+ └────────────────────────┘ └────────────────────────┘
241
+
212
242
Parameters
213
243
----------
214
244
name: str
@@ -222,6 +252,31 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
222
252
or there has been an error when fetching the configuration from the store
223
253
Can be boolean or any JSON values for non-boolean features.
224
254
255
+
256
+ Examples
257
+ --------
258
+
259
+ ```python
260
+ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
261
+ from aws_lambda_powertools.utilities.typing import LambdaContext
262
+
263
+ app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features")
264
+
265
+ feature_flags = FeatureFlags(store=app_config)
266
+
267
+
268
+ def lambda_handler(event: dict, context: LambdaContext):
269
+ # Get customer's tier from incoming request
270
+ ctx = {"tier": event.get("tier", "standard")}
271
+
272
+ # Evaluate whether customer's tier has access to premium features
273
+ # based on `has_premium_features` rules
274
+ has_premium_features: bool = feature_flags.evaluate(name="premium_features", context=ctx, default=False)
275
+ if has_premium_features:
276
+ # enable premium features
277
+ ...
278
+ ```
279
+
225
280
Returns
226
281
------
227
282
JSONType
@@ -335,3 +390,45 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
335
390
features_enabled .append (name )
336
391
337
392
return features_enabled
393
+
394
+ def validation_exception_handler (self , exc_class : Exception | list [Exception ]):
395
+ """Registers function to handle unexpected validation exceptions when evaluating flags.
396
+
397
+ It does not override the function of a default flag value in case of network and IAM permissions.
398
+ For example, you won't be able to catch ConfigurationStoreError exception.
399
+
400
+ Parameters
401
+ ----------
402
+ exc_class : Exception | list[Exception]
403
+ One or more exceptions to catch
404
+
405
+ Examples
406
+ --------
407
+
408
+ ```python
409
+ feature_flags = FeatureFlags(store=app_config)
410
+
411
+ @feature_flags.validation_exception_handler(Exception) # any exception
412
+ def catch_exception(exc):
413
+ raise TypeError("re-raised") from exc
414
+ ```
415
+ """
416
+
417
+ def register_exception_handler (func : Callable [P , T ]) -> Callable [P , T ]:
418
+ if isinstance (exc_class , list ):
419
+ for exp in exc_class :
420
+ self ._exception_handlers [exp ] = func
421
+ else :
422
+ self ._exception_handlers [exc_class ] = func
423
+
424
+ return func
425
+
426
+ return register_exception_handler
427
+
428
+ def _lookup_exception_handler (self , exc : BaseException ) -> Callable | None :
429
+ # Use "Method Resolution Order" to allow for matching against a base class
430
+ # of an exception
431
+ for cls in type (exc ).__mro__ :
432
+ if cls in self ._exception_handlers :
433
+ return self ._exception_handlers [cls ] # type: ignore[index] # index is correct
434
+ return None
0 commit comments