11import json
22import logging
33from threading import Lock
4- from typing import Any , List , Dict
4+ from typing import Any , List , Dict , Set
55
66from amplitude import Amplitude
77
88from .config import LocalEvaluationConfig
9- from .. flagresult import FlagResult
9+ from .topological_sort import topological_sort
1010from ..assignment import Assignment , AssignmentFilter , AssignmentService
11- from ..assignment .assignment_service import FLAG_TYPE_MUTUAL_EXCLUSION_GROUP , FLAG_TYPE_HOLDOUT_GROUP
1211from ..user import User
1312from ..connection_pool import HTTPConnectionPool
1413from .poller import Poller
1514from .evaluation .evaluation import evaluate
15+ from ..util import deprecated
16+ from ..util .user import user_to_evaluation_context
17+ from ..util .variant import evaluation_variants_json_to_variants
1618from ..variant import Variant
1719from ..version import __version__
1820
@@ -57,37 +59,62 @@ def start(self):
5759 self .__do_flags ()
5860 self .poller .start ()
5961
60- def evaluate (self , user : User , flag_keys : List [str ] = None ) -> Dict [str , Variant ]:
62+ def evaluate_v2 (self , user : User , flag_keys : Set [str ] = None ) -> Dict [str , Variant ]:
6163 """
62- Locally evaluates flag variants for a user.
63- Parameters:
64+ Locally evaluates flag variants for a user.
65+
66+ This function will only evaluate flags for the keys specified in the flag_keys argument. If flag_keys is
67+ missing or None, all flags are evaluated. This function differs from evaluate as it will return a default
68+ variant object if the flag was evaluated but the user was not assigned (i.e. off).
69+
70+ Parameters:
6471 user (User): The user to evaluate
65- flag_keys (List[str]): The flags to evaluate with the user. If empty, all flags from the flag cache are evaluated.
72+ flag_keys (List[str]): The flags to evaluate with the user. If empty, all flags are evaluated.
6673
6774 Returns:
6875 The evaluated variants.
6976 """
70- variants = {}
7177 if self .flags is None or len (self .flags ) == 0 :
72- return variants
73- user_json = str (user )
74- self .logger .debug (f"[Experiment] Evaluate: User: { user_json } - Flags: { self .flags } " )
75- result_json = evaluate (self .flags , user_json )
78+ return {}
79+ self .logger .debug (f"[Experiment] Evaluate: user={ user } - Flags: { self .flags } " )
80+ context = user_to_evaluation_context (user )
81+ sorted_flags = topological_sort (self .flags , flag_keys )
82+ flags_json = json .dumps (sorted_flags )
83+ context_json = json .dumps (context )
84+ result_json = evaluate (flags_json , context_json )
7685 self .logger .debug (f"[Experiment] Evaluate Result: { result_json } " )
7786 evaluation_result = json .loads (result_json )
78- filter_result = flag_keys is not None
79- assignment_result = {}
80- for key , value in evaluation_result .items ():
81- included = not filter_result or key in flag_keys
82- if not value .get ('isDefaultVariant' ) and included :
83- variants [key ] = Variant (value ['variant' ].get ('key' ), value ['variant' ].get ('payload' ))
84- if included or evaluation_result [key ]['type' ] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP or \
85- evaluation_result [key ]['type' ] == FLAG_TYPE_HOLDOUT_GROUP :
86- assignment_result [key ] = FlagResult (value )
87- if self .assignment_service :
88- self .assignment_service .track (Assignment (user , assignment_result ))
87+ error = evaluation_result .get ('error' )
88+ if error is not None :
89+ self .logger .error (f"[Experiment] Evaluation failed: { error } " )
90+ return {}
91+ result = evaluation_result .get ('result' )
92+ if result is None :
93+ return {}
94+ variants = evaluation_variants_json_to_variants (result )
95+ if self .assignment_service is not None :
96+ self .assignment_service .track (Assignment (user , variants ))
8997 return variants
9098
99+ @deprecated ("Use evaluate_v2" )
100+ def evaluate (self , user : User , flag_keys : List [str ] = None ) -> Dict [str , Variant ]:
101+ """
102+ Locally evaluates flag variants for a user.
103+
104+ This function will only evaluate flags for the keys specified in the flag_keys argument. If flag_keys is
105+ missing, all flags are evaluated.
106+
107+ Parameters:
108+ user (User): The user to evaluate
109+ flag_keys (List[str]): The flags to evaluate with the user. If empty, all flags are evaluated.
110+
111+ Returns:
112+ The evaluated variants.
113+ """
114+ flag_keys = set (flag_keys ) if flag_keys is not None else None
115+ variants = self .evaluate_v2 (user , flag_keys )
116+ return self .__filter_default_variants (variants )
117+
91118 def __do_flags (self ):
92119 conn = self ._connection_pool .acquire ()
93120 headers = {
@@ -98,14 +125,16 @@ def __do_flags(self):
98125 body = None
99126 self .logger .debug ('[Experiment] Get flag configs' )
100127 try :
101- response = conn .request ('GET' , '/sdk/v1 /flags' , body , headers )
128+ response = conn .request ('GET' , '/sdk/v2 /flags?v=0 ' , body , headers )
102129 response_body = response .read ().decode ("utf8" )
103130 if response .status != 200 :
104131 raise Exception (
105132 f"[Experiment] Get flagConfigs - received error response: ${ response .status } : ${ response_body } " )
106- self .logger .debug (f"[Experiment] Got flag configs: { response_body } " )
133+ flags = json .loads (response_body )
134+ flags_dict = {flag ['key' ]: flag for flag in flags }
135+ self .logger .debug (f"[Experiment] Got flag configs: { flags } " )
107136 self .lock .acquire ()
108- self .flags = response_body
137+ self .flags = flags_dict
109138 self .lock .release ()
110139 finally :
111140 self ._connection_pool .release (conn )
@@ -128,3 +157,15 @@ def __enter__(self) -> 'LocalEvaluationClient':
128157
129158 def __exit__ (self , * exit_info : Any ) -> None :
130159 self .stop ()
160+
161+ @staticmethod
162+ def __filter_default_variants (variants : Dict [str , Variant ]) -> Dict [str , Variant ]:
163+ def is_default_variant (variant : Variant ) -> bool :
164+ default = False if variant .metadata .get ('default' ) is None else variant .metadata .get ('default' )
165+ deployed = True if variant .metadata .get ('deployed' ) is None else variant .metadata .get ('deployed' )
166+ return default or not deployed
167+
168+ return {key : variant for key , variant in variants .items () if not is_default_variant (variant )}
169+
170+
171+
0 commit comments