44# license information.
55# -------------------------------------------------------------------------
66import logging
7+ import hashlib
78from collections .abc import Mapping
89from typing import overload
910from ._defaultfilters import TimeWindowFilter , TargetingFilter
1011from ._featurefilters import FeatureFilter
11- from ._models import FeatureFlag , EvaluationEvent , TargetingContext
12+ from ._models import FeatureFlag , Variant , EvaluationEvent , TargetingContext
1213
1314
1415FEATURE_MANAGEMENT_KEY = "feature_management"
@@ -88,6 +89,111 @@ def __init__(self, configuration, **kwargs):
8889 raise ValueError ("Custom filter must be a subclass of FeatureFilter" )
8990 self ._filters [feature_filter .name ] = feature_filter
9091
92+ @staticmethod
93+ def _check_default_disabled_variant (feature_flag ):
94+ """
95+ A method called when the feature flag is disabled, to determine what the default variant should be. If there is
96+ no allocation, then None is set as the value of the variant in the EvaluationEvent.
97+
98+ :param FeatureFlag feature_flag: Feature flag object.
99+ :return: EvaluationEvent
100+ """
101+ if not feature_flag .allocation :
102+ return EvaluationEvent (enabled = False )
103+ return FeatureManager ._check_variant_override (
104+ feature_flag .variants , feature_flag .allocation .default_when_disabled , False
105+ )
106+
107+ @staticmethod
108+ def _check_default_enabled_variant (feature_flag ):
109+ """
110+ A method called when the feature flag is enabled, to determine what the default variant should be. If there is
111+ no allocation, then None is set as the value of the variant in the EvaluationEvent.
112+
113+ :param FeatureFlag feature_flag: Feature flag object.
114+ :return: EvaluationEvent
115+ """
116+ if not feature_flag .allocation :
117+ return EvaluationEvent (enabled = True )
118+ return FeatureManager ._check_variant_override (
119+ feature_flag .variants , feature_flag .allocation .default_when_enabled , True
120+ )
121+
122+ @staticmethod
123+ def _check_variant_override (variants , default_variant_name , status ):
124+ """
125+ A method to check if a variant is overridden to be enabled or disabled by the variant.
126+
127+ :param list[Variant] variants: List of variants.
128+ :param str default_variant_name: Name of the default variant.
129+ :param bool status: Status of the feature flag.
130+ :return: EvaluationEvent
131+ """
132+ if not variants or not default_variant_name :
133+ return EvaluationEvent (enabled = status )
134+ for variant in variants :
135+ if variant .name == default_variant_name :
136+ if variant .status_override == "Enabled" :
137+ return EvaluationEvent (enabled = True )
138+ if variant .status_override == "Disabled" :
139+ return EvaluationEvent (enabled = False )
140+ return EvaluationEvent (enabled = status )
141+
142+ @staticmethod
143+ def _is_targeted (context_id ):
144+ """Determine if the user is targeted for the given context"""
145+ hashed_context_id = hashlib .sha256 (context_id .encode ()).digest ()
146+ context_marker = int .from_bytes (hashed_context_id [:4 ], byteorder = "little" , signed = False )
147+
148+ return (context_marker / (2 ** 32 - 1 )) * 100
149+
150+ def _assign_variant (self , feature_flag , targeting_context ):
151+ """
152+ Assign a variant to the user based on the allocation.
153+
154+ :param FeatureFlag feature_flag: Feature flag object.
155+ :param TargetingContext targeting_context: Targeting context.
156+ :return: Variant name.
157+ """
158+ if not feature_flag .variants or not feature_flag .allocation :
159+ return None
160+ if feature_flag .allocation .user and targeting_context .user_id :
161+ for user_allocation in feature_flag .allocation .user :
162+ if targeting_context .user_id in user_allocation .users :
163+ return user_allocation .variant
164+ if feature_flag .allocation .group and len (targeting_context .groups ) > 0 :
165+ for group_allocation in feature_flag .allocation .group :
166+ for group in targeting_context .groups :
167+ if group in group_allocation .groups :
168+ return group_allocation .variant
169+ if feature_flag .allocation .percentile :
170+ context_id = targeting_context .user_id + "\n " + feature_flag .allocation .seed
171+ box = self ._is_targeted (context_id )
172+ for percentile_allocation in feature_flag .allocation .percentile :
173+ if box == 100 and percentile_allocation .percentile_to == 100 :
174+ return percentile_allocation .variant
175+ if percentile_allocation .percentile_from <= box < percentile_allocation .percentile_to :
176+ return percentile_allocation .variant
177+ return None
178+
179+ def _variant_name_to_variant (self , feature_flag , variant_name ):
180+ """
181+ Get the variant object from the variant name.
182+
183+ :param FeatureFlag feature_flag: Feature flag object.
184+ :param str variant_name: Name of the variant.
185+ :return: Variant object.
186+ """
187+ if not feature_flag .variants :
188+ return None
189+ for variant_reference in feature_flag .variants :
190+ if variant_reference .name == variant_name :
191+ configuration = variant_reference .configuration_value
192+ if not configuration :
193+ configuration = self ._configuration .get (variant_reference .configuration_reference )
194+ return Variant (variant_reference .name , configuration )
195+ return None
196+
91197 def _build_targeting_context (self , args ):
92198 """
93199 Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
@@ -126,6 +232,31 @@ def is_enabled(self, feature_flag_id, *args, **kwargs):
126232 result = self ._check_feature (feature_flag_id , targeting_context , ** kwargs )
127233 return result .enabled
128234
235+ @overload
236+ def get_variant (self , feature_flag_id , user_id , ** kwargs ):
237+ """
238+ Determine the variant for the given context.
239+
240+ :param str feature_flag_id: Name of the feature flag.
241+ :param str user_id: User identifier.
242+ :return: return: Variant instance.
243+ :rtype: Variant
244+ """
245+
246+ def get_variant (self , feature_flag_id , * args , ** kwargs ):
247+ """
248+ Determine the variant for the given context.
249+
250+ :param str feature_flag_id: Name of the feature flag
251+ :keyword TargetingContext targeting_context: Targeting context.
252+ :return: Variant instance.
253+ :rtype: Variant
254+ """
255+ targeting_context = self ._build_targeting_context (args )
256+
257+ result = self ._check_feature (feature_flag_id , targeting_context , ** kwargs )
258+ return result .variant
259+
129260 def _check_feature_filters (self , feature_flag , targeting_context , ** kwargs ):
130261 feature_conditions = feature_flag .conditions
131262 feature_filters = feature_conditions .client_filters
@@ -154,6 +285,30 @@ def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
154285 break
155286 return evaluation_event
156287
288+ def _assign_allocation (self , feature_flag , evaluation_event , targeting_context ):
289+ if feature_flag .allocation and feature_flag .variants :
290+ variant_name = self ._assign_variant (feature_flag , targeting_context )
291+ if variant_name :
292+ evaluation_event .enabled = FeatureManager ._check_variant_override (
293+ feature_flag .variants , variant_name , evaluation_event .enabled
294+ ).enabled
295+ evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
296+ evaluation_event .feature = feature_flag
297+ return evaluation_event
298+
299+ variant_name = None
300+ if evaluation_event .enabled :
301+ evaluation_event = FeatureManager ._check_default_enabled_variant (feature_flag )
302+ if feature_flag .allocation :
303+ variant_name = feature_flag .allocation .default_when_enabled
304+ else :
305+ evaluation_event = FeatureManager ._check_default_disabled_variant (feature_flag )
306+ if feature_flag .allocation :
307+ variant_name = feature_flag .allocation .default_when_disabled
308+ evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
309+ evaluation_event .feature = feature_flag
310+ return evaluation_event
311+
157312 def _check_feature (self , feature_flag_id , targeting_context , ** kwargs ):
158313 """
159314 Determine if the feature flag is enabled for the given context.
@@ -179,9 +334,16 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
179334
180335 if not feature_flag .enabled :
181336 # Feature flags that are disabled are always disabled
182- return EvaluationEvent (enabled = False )
337+ evaluation_event = FeatureManager ._check_default_disabled_variant (feature_flag )
338+ if feature_flag .allocation :
339+ variant_name = feature_flag .allocation .default_when_disabled
340+ evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
341+ evaluation_event .feature = feature_flag
342+ return evaluation_event
343+
344+ evaluation_event = self ._check_feature_filters (feature_flag , targeting_context , ** kwargs )
183345
184- return self ._check_feature_filters (feature_flag , targeting_context , ** kwargs )
346+ return self ._assign_allocation (feature_flag , evaluation_event , targeting_context )
185347
186348 def list_feature_flag_names (self ):
187349 """
0 commit comments