2
2
This submodule contains the client class that provides most of the SDK functionality.
3
3
"""
4
4
5
- from typing import Optional , Any , Dict , Mapping , Union , Tuple , Callable
5
+ from typing import Optional , Any , Dict , Mapping , Union , Tuple , Callable , List
6
6
7
7
from .impl import AnyNum
8
8
15
15
from ldclient .config import Config
16
16
from ldclient .context import Context
17
17
from ldclient .feature_store import _FeatureStoreDataSetSorter
18
+ from ldclient .hook import Hook , EvaluationSeriesContext , _EvaluationWithHookResult
18
19
from ldclient .evaluation import EvaluationDetail , FeatureFlagsState
19
20
from ldclient .impl .big_segments import BigSegmentStoreManager
20
21
from ldclient .impl .datasource .feature_requester import FeatureRequesterImpl
@@ -187,8 +188,10 @@ def __init__(self, config: Config, start_wait: float=5):
187
188
self ._config = config
188
189
self ._config ._validate ()
189
190
191
+ self .__hooks_lock = ReadWriteLock ()
192
+ self .__hooks = config .hooks # type: List[Hook]
193
+
190
194
self ._event_processor = None
191
- self ._lock = Lock ()
192
195
self ._event_factory_default = EventFactory (False )
193
196
self ._event_factory_with_reasons = EventFactory (True )
194
197
@@ -395,8 +398,11 @@ def variation(self, key: str, context: Context, default: Any) -> Any:
395
398
available from LaunchDarkly
396
399
:return: the variation for the given context, or the ``default`` value if the flag cannot be evaluated
397
400
"""
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
400
406
401
407
def variation_detail (self , key : str , context : Context , default : Any ) -> EvaluationDetail :
402
408
"""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
412
418
:return: an :class:`ldclient.evaluation.EvaluationDetail` object that includes the feature
413
419
flag value and evaluation reason
414
420
"""
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
417
426
418
427
def migration_variation (self , key : str , context : Context , default_stage : Stage ) -> Tuple [Stage , OpTracker ]:
419
428
"""
@@ -429,17 +438,21 @@ def migration_variation(self, key: str, context: Context, default_stage: Stage)
429
438
log .error (f"default stage { default_stage } is not a valid stage; using 'off' instead" )
430
439
default_stage = Stage .OFF
431
440
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 })
433
449
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 })
439
453
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' ]
443
456
444
457
def _evaluate_internal (self , key : str , context : Context , default : Any , event_factory ) -> Tuple [EvaluationDetail , Optional [FeatureFlag ]]:
445
458
default = self ._config .get_default (key , default )
@@ -451,8 +464,7 @@ def _evaluate_internal(self, key: str, context: Context, default: Any, event_fac
451
464
if self ._store .initialized :
452
465
log .warning ("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key )
453
466
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 )
456
468
reason = error_reason ('CLIENT_NOT_READY' )
457
469
self ._send_event (event_factory .new_unknown_flag_event (key , context , default , reason ))
458
470
return EvaluationDetail (default , None , reason ), None
@@ -583,6 +595,70 @@ def secure_mode_hash(self, context: Context) -> str:
583
595
return ""
584
596
return hmac .new (str (self ._config .sdk_key ).encode (), context .fully_qualified_key .encode (), hashlib .sha256 ).hexdigest ()
585
597
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
+
586
662
@property
587
663
def big_segment_store_status_provider (self ) -> BigSegmentStoreStatusProvider :
588
664
"""
0 commit comments