44# license information.
55# -------------------------------------------------------------------------
66import hashlib
7+ import logging
78from abc import ABC
89from typing import List , Optional , Dict , Tuple , Any , Mapping
910from ._models import FeatureFlag , Variant , VariantAssignmentReason , TargetingContext , EvaluationEvent , VariantReference
@@ -78,7 +79,7 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Any):
7879 self ._on_feature_evaluated = kwargs .pop ("on_feature_evaluated" , None )
7980
8081 @staticmethod
81- def _check_default_disabled_variant (evaluation_event : EvaluationEvent ) -> None :
82+ def _assign_default_disabled_variant (evaluation_event : EvaluationEvent ) -> None :
8283 """
8384 A method called when the feature flag is disabled, to determine what the default variant should be. If there is
8485 no allocation, then None is set as the value of the variant in the EvaluationEvent.
@@ -88,15 +89,15 @@ def _check_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
8889 evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
8990 if not evaluation_event .feature or not evaluation_event .feature .allocation :
9091 return
91- FeatureManagerBase ._check_variant_override (
92+ FeatureManagerBase ._assign_variant_override (
9293 evaluation_event .feature .variants ,
9394 evaluation_event .feature .allocation .default_when_disabled ,
9495 False ,
9596 evaluation_event ,
9697 )
9798
9899 @staticmethod
99- def _check_default_enabled_variant (evaluation_event : EvaluationEvent ) -> None :
100+ def _assign_default_enabled_variant (evaluation_event : EvaluationEvent ) -> None :
100101 """
101102 A method called when the feature flag is enabled, to determine what the default variant should be. If there is
102103 no allocation, then None is set as the value of the variant in the EvaluationEvent.
@@ -106,15 +107,15 @@ def _check_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
106107 evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
107108 if not evaluation_event .feature or not evaluation_event .feature .allocation :
108109 return
109- FeatureManagerBase ._check_variant_override (
110+ FeatureManagerBase ._assign_variant_override (
110111 evaluation_event .feature .variants ,
111112 evaluation_event .feature .allocation .default_when_enabled ,
112113 True ,
113114 evaluation_event ,
114115 )
115116
116117 @staticmethod
117- def _check_variant_override (
118+ def _assign_variant_override (
118119 variants : Optional [List [VariantReference ]],
119120 default_variant_name : Optional [str ],
120121 status : bool ,
@@ -149,56 +150,57 @@ def _is_targeted(context_id: str) -> float:
149150
150151 return (context_marker / (2 ** 32 - 1 )) * 100
151152
152- def _assign_variant (
153- self , feature_flag : FeatureFlag , targeting_context : TargetingContext , evaluation_event : EvaluationEvent
154- ) -> None :
153+ def _assign_variant (self , targeting_context : TargetingContext , evaluation_event : EvaluationEvent ) -> None :
155154 """
156155 Assign a variant to the user based on the allocation.
157156
158- :param FeatureFlag feature_flag: Feature flag object.
159157 :param TargetingContext targeting_context: Targeting context.
160158 :param EvaluationEvent evaluation_event: Evaluation event object.
161159 """
162- feature = evaluation_event .feature
160+ feature_flag = evaluation_event .feature
163161 variant_name = None
164- if not feature or not feature .variants or not feature .allocation :
162+ if not feature_flag or not feature_flag .variants or not feature_flag .allocation :
165163 return
166- if feature .allocation .user and targeting_context .user_id :
167- for user_allocation in feature .allocation .user :
164+
165+ allocation = feature_flag .allocation
166+ groups = targeting_context .groups
167+
168+ if allocation .user and targeting_context .user_id :
169+ for user_allocation in allocation .user :
168170 if targeting_context .user_id in user_allocation .users :
169171 evaluation_event .reason = VariantAssignmentReason .USER
170172 variant_name = user_allocation .variant
171- if not variant_name and feature .allocation .group and len (targeting_context .groups ) > 0 :
172- for group_allocation in feature .allocation .group :
173- for group in targeting_context .groups :
174- if group in group_allocation .groups :
175- evaluation_event .reason = VariantAssignmentReason .GROUP
176- variant_name = group_allocation .variant
177- if not variant_name and feature .allocation .percentile :
178- seed = feature .allocation .seed
179- if not seed :
180- seed = "allocation\n " + feature .name
181- context_id = targeting_context .user_id + "\n " + seed
173+
174+ if not variant_name and allocation .group and groups :
175+ for group_allocation in allocation .group :
176+ if any (group in group_allocation .groups for group in groups ):
177+ evaluation_event .reason = VariantAssignmentReason .GROUP
178+ variant_name = group_allocation .variant
179+
180+ if not variant_name and allocation .percentile :
181+ seed = allocation .seed or f"allocation\n { feature_flag .name } "
182+ context_id = f"{ targeting_context .user_id } \n { seed } "
182183 box : float = self ._is_targeted (context_id )
183- for percentile_allocation in feature .allocation .percentile :
184- if box == 100 and percentile_allocation .percentile_to == 100 :
185- variant_name = percentile_allocation .variant
186- if not percentile_allocation .percentile_to :
187- continue
188- if percentile_allocation .percentile_from <= box < percentile_allocation .percentile_to :
184+ for percentile_allocation in allocation .percentile :
185+ percentile_to : int = percentile_allocation .percentile_to
186+ if (box == 100 and percentile_to == 100 ) or (
187+ percentile_allocation .percentile_from <= box < percentile_to
188+ ):
189189 evaluation_event .reason = VariantAssignmentReason .PERCENTILE
190190 variant_name = percentile_allocation .variant
191+ break
192+
191193 if not variant_name :
192- FeatureManagerBase ._check_default_enabled_variant (evaluation_event )
194+ FeatureManagerBase ._assign_default_enabled_variant (evaluation_event )
193195 if feature_flag .allocation :
194196 evaluation_event .variant = self ._variant_name_to_variant (
195197 feature_flag , feature_flag .allocation .default_when_enabled
196198 )
197199 return
200+
198201 evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
199- if not feature_flag .variants :
200- return
201- FeatureManagerBase ._check_variant_override (feature_flag .variants , variant_name , True , evaluation_event )
202+ if feature_flag .variants :
203+ FeatureManagerBase ._assign_variant_override (feature_flag .variants , variant_name , True , evaluation_event )
202204
203205 def _variant_name_to_variant (self , feature_flag : FeatureFlag , variant_name : Optional [str ]) -> Optional [Variant ]:
204206 """
@@ -208,10 +210,9 @@ def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Opti
208210 :param str variant_name: Name of the variant.
209211 :return: Variant object.
210212 """
211- if not feature_flag .variants :
212- return None
213- if not variant_name :
213+ if not feature_flag .variants or not variant_name :
214214 return None
215+
215216 for variant_reference in feature_flag .variants :
216217 if variant_reference .name == variant_name :
217218 return Variant (variant_reference .name , variant_reference .configuration_value )
@@ -225,31 +226,66 @@ def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
225226 :param args: Arguments to build the TargetingContext.
226227 :return: TargetingContext
227228 """
228- if len (args ) == 1 and isinstance (args [0 ], str ):
229- return TargetingContext (user_id = args [0 ], groups = [])
230- if len (args ) == 1 and isinstance (args [0 ], TargetingContext ):
231- return args [0 ]
229+ if len (args ) == 1 :
230+ arg = args [0 ]
231+ if isinstance (arg , str ):
232+ return TargetingContext (user_id = arg , groups = [])
233+ if isinstance (arg , TargetingContext ):
234+ return arg
232235 return TargetingContext ()
233236
234237 def _assign_allocation (self , evaluation_event : EvaluationEvent , targeting_context : TargetingContext ) -> None :
235238 feature_flag = evaluation_event .feature
236239 if not feature_flag :
237240 return
238- if feature_flag .variants :
239- if not feature_flag .allocation :
240- if evaluation_event .enabled :
241- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
242- return
243- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
244- return
245- if not evaluation_event .enabled :
246- FeatureManagerBase ._check_default_disabled_variant (evaluation_event )
247- evaluation_event .variant = self ._variant_name_to_variant (
248- feature_flag , feature_flag .allocation .default_when_disabled
249- )
250- return
251241
252- self ._assign_variant (feature_flag , targeting_context , evaluation_event )
242+ if not feature_flag .variants or not feature_flag .allocation :
243+ return
244+
245+ if not evaluation_event .enabled :
246+ FeatureManagerBase ._assign_default_disabled_variant (evaluation_event )
247+ evaluation_event .variant = self ._variant_name_to_variant (
248+ feature_flag , feature_flag .allocation .default_when_disabled
249+ )
250+ return
251+
252+ self ._assign_variant (targeting_context , evaluation_event )
253+
254+ def _check_feature_base (self , feature_flag_id : str ) -> Tuple [EvaluationEvent , bool ]:
255+ """
256+ Determine if the feature flag is enabled for the given context.
257+
258+ :param str feature_flag_id: Name of the feature flag.
259+ :return: The evaluation event and if the feature filters need to be checked.
260+ :rtype: evaluation_event, bool
261+ """
262+ if self ._copy is not self ._configuration .get (FEATURE_MANAGEMENT_KEY ):
263+ self ._cache = {}
264+ self ._copy = self ._configuration .get (FEATURE_MANAGEMENT_KEY )
265+
266+ if not self ._cache .get (feature_flag_id ):
267+ feature_flag = _get_feature_flag (self ._configuration , feature_flag_id )
268+ self ._cache [feature_flag_id ] = feature_flag
269+ else :
270+ feature_flag = self ._cache .get (feature_flag_id )
271+
272+ evaluation_event = EvaluationEvent (feature_flag )
273+ if not feature_flag :
274+ logging .warning ("Feature flag %s not found" , feature_flag_id )
275+ # Unknown feature flags are disabled by default
276+ return evaluation_event , True
277+
278+ if not feature_flag .enabled :
279+ # Feature flags that are disabled are always disabled
280+ self ._assign_default_disabled_variant (evaluation_event )
281+ if feature_flag .allocation :
282+ variant_name = feature_flag .allocation .default_when_disabled
283+ evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
284+
285+ # If a feature flag is disabled and override can't enable it
286+ evaluation_event .enabled = False
287+ return evaluation_event , True
288+ return evaluation_event , False
253289
254290 def list_feature_flag_names (self ) -> List [str ]:
255291 """
0 commit comments