22This submodule contains the client class that provides most of the SDK functionality.
33"""
44
5- from typing import Optional , Any , Dict , Mapping , Union , Tuple , Callable
5+ from typing import Optional , Any , Dict , Mapping , Union , Tuple , Callable , List
66
77from .impl import AnyNum
88
1515from ldclient .config import Config
1616from ldclient .context import Context
1717from ldclient .feature_store import _FeatureStoreDataSetSorter
18+ from ldclient .hook import Hook , EvaluationSeriesContext , _EvaluationWithHookResult
1819from ldclient .evaluation import EvaluationDetail , FeatureFlagsState
1920from ldclient .impl .big_segments import BigSegmentStoreManager
2021from ldclient .impl .datasource .feature_requester import FeatureRequesterImpl
@@ -187,8 +188,10 @@ def __init__(self, config: Config, start_wait: float=5):
187188 self ._config = config
188189 self ._config ._validate ()
189190
191+ self .__hooks_lock = ReadWriteLock ()
192+ self .__hooks = config .hooks # type: List[Hook]
193+
190194 self ._event_processor = None
191- self ._lock = Lock ()
192195 self ._event_factory_default = EventFactory (False )
193196 self ._event_factory_with_reasons = EventFactory (True )
194197
@@ -395,8 +398,11 @@ def variation(self, key: str, context: Context, default: Any) -> Any:
395398 available from LaunchDarkly
396399 :return: the variation for the given context, or the ``default`` value if the flag cannot be evaluated
397400 """
398- detail , _ = self ._evaluate_internal (key , context , default , self ._event_factory_default )
399- return detail .value
401+ def evaluate ():
402+ detail , _ = self ._evaluate_internal (key , context , default , self ._event_factory_default )
403+ return _EvaluationWithHookResult (evaluation_detail = detail )
404+
405+ return self .__evaluate_with_hooks (key = key , context = context , default_value = default , method = "variation" , block = evaluate ).evaluation_detail .value
400406
401407 def variation_detail (self , key : str , context : Context , default : Any ) -> EvaluationDetail :
402408 """Calculates the value of a feature flag for a given context, and returns an object that
@@ -412,8 +418,11 @@ def variation_detail(self, key: str, context: Context, default: Any) -> Evaluati
412418 :return: an :class:`ldclient.evaluation.EvaluationDetail` object that includes the feature
413419 flag value and evaluation reason
414420 """
415- detail , _ = self ._evaluate_internal (key , context , default , self ._event_factory_with_reasons )
416- return detail
421+ def evaluate ():
422+ detail , _ = self ._evaluate_internal (key , context , default , self ._event_factory_with_reasons )
423+ return _EvaluationWithHookResult (evaluation_detail = detail )
424+
425+ return self .__evaluate_with_hooks (key = key , context = context , default_value = default , method = "variation_detail" , block = evaluate ).evaluation_detail
417426
418427 def migration_variation (self , key : str , context : Context , default_stage : Stage ) -> Tuple [Stage , OpTracker ]:
419428 """
@@ -429,17 +438,21 @@ def migration_variation(self, key: str, context: Context, default_stage: Stage)
429438 log .error (f"default stage { default_stage } is not a valid stage; using 'off' instead" )
430439 default_stage = Stage .OFF
431440
432- detail , flag = self ._evaluate_internal (key , context , default_stage .value , self ._event_factory_default )
441+ def evaluate ():
442+ detail , flag = self ._evaluate_internal (key , context , default_stage .value , self ._event_factory_default )
443+
444+ if isinstance (detail .value , str ):
445+ stage = Stage .from_str (detail .value )
446+ if stage is not None :
447+ tracker = OpTracker (key , flag , context , detail , default_stage )
448+ return _EvaluationWithHookResult (evaluation_detail = detail , results = {'default_stage' : stage , 'tracker' : tracker })
433449
434- if isinstance (detail .value , str ):
435- stage = Stage .from_str (detail .value )
436- if stage is not None :
437- tracker = OpTracker (key , flag , context , detail , default_stage )
438- return stage , tracker
450+ detail = EvaluationDetail (default_stage .value , None , error_reason ('WRONG_TYPE' ))
451+ tracker = OpTracker (key , flag , context , detail , default_stage )
452+ return _EvaluationWithHookResult (evaluation_detail = detail , results = {'default_stage' : default_stage , 'tracker' : tracker })
439453
440- detail = EvaluationDetail (default_stage .value , None , error_reason ('WRONG_TYPE' ))
441- tracker = OpTracker (key , flag , context , detail , default_stage )
442- return default_stage , tracker
454+ hook_result = self .__evaluate_with_hooks (key = key , context = context , default_value = default_stage , method = "migration_variation" , block = evaluate )
455+ return hook_result .results ['default_stage' ], hook_result .results ['tracker' ]
443456
444457 def _evaluate_internal (self , key : str , context : Context , default : Any , event_factory ) -> Tuple [EvaluationDetail , Optional [FeatureFlag ]]:
445458 default = self ._config .get_default (key , default )
@@ -451,8 +464,7 @@ def _evaluate_internal(self, key: str, context: Context, default: Any, event_fac
451464 if self ._store .initialized :
452465 log .warning ("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key )
453466 else :
454- log .warning ("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: "
455- + str (default ) + " for feature key: " + key )
467+ log .warning ("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str (default ) + " for feature key: " + key )
456468 reason = error_reason ('CLIENT_NOT_READY' )
457469 self ._send_event (event_factory .new_unknown_flag_event (key , context , default , reason ))
458470 return EvaluationDetail (default , None , reason ), None
@@ -583,6 +595,70 @@ def secure_mode_hash(self, context: Context) -> str:
583595 return ""
584596 return hmac .new (str (self ._config .sdk_key ).encode (), context .fully_qualified_key .encode (), hashlib .sha256 ).hexdigest ()
585597
598+ def add_hook (self , hook : Hook ):
599+ """
600+ Add a hook to the client. In order to register a hook before the client starts, please use the `hooks` property of
601+ `Config`.
602+
603+ Hooks provide entrypoints which allow for observation of SDK functions.
604+
605+ :param hook:
606+ """
607+ if not isinstance (hook , Hook ):
608+ return
609+
610+ self .__hooks_lock .lock ()
611+ self .__hooks .append (hook )
612+ self .__hooks_lock .unlock ()
613+
614+ def __evaluate_with_hooks (self , key : str , context : Context , default_value : Any , method : str , block : Callable [[], _EvaluationWithHookResult ]) -> _EvaluationWithHookResult :
615+ """
616+ # evaluate_with_hook will run the provided block, wrapping it with evaluation hook support.
617+ #
618+ # :param key:
619+ # :param context:
620+ # :param default:
621+ # :param method:
622+ # :param block:
623+ # :return:
624+ """
625+ hooks = [] # type: List[Hook]
626+ try :
627+ self .__hooks_lock .rlock ()
628+
629+ if len (self .__hooks ) == 0 :
630+ return block ()
631+
632+ hooks = self .__hooks .copy ()
633+ finally :
634+ self .__hooks_lock .runlock ()
635+
636+ series_context = EvaluationSeriesContext (key = key , context = context , default_value = default_value , method = method )
637+ hook_data = self .__execute_before_evaluation (hooks , series_context )
638+ evaluation_result = block ()
639+ self .__execute_after_evaluation (hooks , series_context , hook_data , evaluation_result .evaluation_detail )
640+
641+ return evaluation_result
642+
643+ def __execute_before_evaluation (self , hooks : List [Hook ], series_context : EvaluationSeriesContext ) -> List [Any ]:
644+ return [
645+ self .__try_execute_stage ("beforeEvaluation" , hook .metadata .name , lambda : hook .before_evaluation (series_context , {}))
646+ for hook in hooks
647+ ]
648+
649+ def __execute_after_evaluation (self , hooks : List [Hook ], series_context : EvaluationSeriesContext , hook_data : List [Any ], evaluation_detail : EvaluationDetail ) -> List [Any ]:
650+ return [
651+ self .__try_execute_stage ("afterEvaluation" , hook .metadata .name , lambda : hook .after_evaluation (series_context , data , evaluation_detail ))
652+ for (hook , data ) in reversed (list (zip (hooks , hook_data )))
653+ ]
654+
655+ def __try_execute_stage (self , method : str , hook_name : str , block : Callable [[], dict ]) -> Optional [dict ]:
656+ try :
657+ return block ()
658+ except BaseException as e :
659+ log .error (f"An error occurred in { method } of the hook { hook_name } : #{ e } " )
660+ return None
661+
586662 @property
587663 def big_segment_store_status_provider (self ) -> BigSegmentStoreStatusProvider :
588664 """
0 commit comments