1+ from __future__ import annotations
2+
13import 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
37
48from ... import Logger
59from ...shared .types import JSONType
610from . import schema
711from .base import StoreProvider
812from .comparators import (
13+ compare_all_in_list ,
14+ compare_any_in_list ,
915 compare_datetime_range ,
1016 compare_days_of_week ,
1117 compare_modulo_range ,
18+ compare_none_in_list ,
1219 compare_time_range ,
13- compare_all_in_list ,
14- compare_any_in_list ,
15- compare_none_in_list
1620)
1721from .exceptions import ConfigurationStoreError
1822
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+
1950
2051class FeatureFlags :
2152 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,
4980 """
5081 self .store = store
5182 self .logger = logger or logging .getLogger (__name__ )
83+ self ._exception_handlers : dict [Exception , Callable ] = {}
5284
5385 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-
7886 try :
79- func = mapping_by_action .get (action , lambda a , b : False )
87+ func = RULE_ACTION_MAPPING .get (action , lambda a , b : False )
8088 return func (context_value , condition_value )
8189 except Exception as exc :
8290 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+
8397 return False
8498
8599 def _evaluate_conditions (
@@ -209,6 +223,22 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
209223 2. Feature exists but has either no rules or no match, return feature default value
210224 3. Feature doesn't exist in stored schema, encountered an error when fetching -> return default value provided
211225
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+
212242 Parameters
213243 ----------
214244 name: str
@@ -222,6 +252,31 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
222252 or there has been an error when fetching the configuration from the store
223253 Can be boolean or any JSON values for non-boolean features.
224254
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+
225280 Returns
226281 ------
227282 JSONType
@@ -335,3 +390,45 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
335390 features_enabled .append (name )
336391
337392 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