55# -------------------------------------------------------------------------
66from collections .abc import Mapping
77import logging
8+ import hashlib
89from ._defaultfilters import TimeWindowFilter , TargetingFilter
910from ._featurefilters import FeatureFilter
1011from .._featuremanager import (
1516 _get_feature_flag ,
1617 _list_feature_flag_names ,
1718)
19+ from .._models ._variant import Variant
1820
1921
2022class FeatureManager :
@@ -42,6 +44,80 @@ def __init__(self, configuration, **kwargs):
4244 raise ValueError ("Custom filter must be a subclass of FeatureFilter" )
4345 self ._filters [feature_filter .name ] = feature_filter
4446
47+ @staticmethod
48+ def _check_default_disabled_variant (feature_flag ):
49+ if not feature_flag .allocation :
50+ return False
51+ return FeatureManager ._check_variant_override (
52+ feature_flag .variants , feature_flag .allocation .default_when_disabled , False
53+ )
54+
55+ @staticmethod
56+ def _check_default_enabled_variant (feature_flag ):
57+ if not feature_flag .allocation :
58+ return True
59+ return FeatureManager ._check_variant_override (
60+ feature_flag .variants , feature_flag .allocation .default_when_enabled , True
61+ )
62+
63+ @staticmethod
64+ def _check_variant_override (variants , default_variant_name , status ):
65+ if not variants or not default_variant_name :
66+ return status
67+ for variant in variants :
68+ if variant .name == default_variant_name :
69+ if variant .status_override == "Enabled" :
70+ return True
71+ if variant .status_override == "Disabled" :
72+ return False
73+ return status
74+
75+ @staticmethod
76+ def _is_targeted (context_id ):
77+ """Determine if the user is targeted for the given context"""
78+ hashed_context_id = hashlib .sha256 (context_id .encode ()).digest ()
79+ context_marker = int .from_bytes (hashed_context_id [:4 ], byteorder = "little" , signed = False )
80+
81+ return (context_marker / (2 ** 32 - 1 )) * 100
82+
83+ def _assign_variant (self , feature_flag , ** kwargs ):
84+ if not feature_flag .variants or not feature_flag .allocation :
85+ return None
86+ if feature_flag .allocation .user :
87+ user = kwargs .get ("user" )
88+ if user :
89+ for user_allocation in feature_flag .allocation .user :
90+ if user in user_allocation .users :
91+ return user_allocation .variant
92+ if feature_flag .allocation .group :
93+ groups = kwargs .get ("groups" )
94+ if groups :
95+ for group_allocation in feature_flag .allocation .group :
96+ for group in groups :
97+ if group in group_allocation .groups :
98+ return group_allocation .variant
99+ if feature_flag .allocation .percentile :
100+ user = kwargs .get ("user" , "" )
101+ context_id = user + "\n " + feature_flag .allocation .seed
102+ box = self ._is_targeted (context_id )
103+ for percentile_allocation in feature_flag .allocation .percentile :
104+ if box == 100 and percentile_allocation .percentile_to == 100 :
105+ return percentile_allocation .variant
106+ if percentile_allocation .percentile_from <= box < percentile_allocation .percentile_to :
107+ return percentile_allocation .variant
108+ return None
109+
110+ def _variant_name_to_variant (self , feature_flag , variant_name ):
111+ if not feature_flag .variants :
112+ return None
113+ for variant_reference in feature_flag .variants :
114+ if variant_reference .name == variant_name :
115+ configuration = variant_reference .configuration_value
116+ if not configuration :
117+ configuration = self ._configuration .get (variant_reference .configuration_reference )
118+ return Variant (variant_reference .name , configuration )
119+ return None
120+
45121 async def is_enabled (self , feature_flag_id , ** kwargs ):
46122 """
47123 Determine if the feature flag is enabled for the given context
@@ -51,6 +127,29 @@ async def is_enabled(self, feature_flag_id, **kwargs):
51127 :return: True if the feature flag is enabled for the given context
52128 :rtype: bool
53129 """
130+ return (await self ._check_feature (feature_flag_id , ** kwargs ))["enabled" ]
131+
132+ async def get_variant (self , feature_flag_id , ** kwargs ):
133+ """
134+ Determine the variant for the given context
135+
136+ :param str feature_flag_id: Name of the feature flag
137+ :paramtype feature_flag_id: str
138+ :return: Name of the variant
139+ :rtype: str
140+ """
141+ return (await self ._check_feature (feature_flag_id , ** kwargs ))["variant" ]
142+
143+ async def _check_feature (self , feature_flag_id , ** kwargs ):
144+ """
145+ Determine if the feature flag is enabled for the given context
146+
147+ :param str feature_flag_id: Name of the feature flag
148+ :paramtype feature_flag_id: str
149+ :return: True if the feature flag is enabled for the given context
150+ :rtype: bool
151+ """
152+ result = {"enabled" : None , "variant" : None }
54153 if self ._copy is not self ._configuration .get (FEATURE_MANAGEMENT_KEY ):
55154 self ._cache = {}
56155 self ._copy = self ._configuration .get (FEATURE_MANAGEMENT_KEY )
@@ -64,32 +163,60 @@ async def is_enabled(self, feature_flag_id, **kwargs):
64163 if not feature_flag :
65164 logging .warning ("Feature flag %s not found" , feature_flag_id )
66165 # Unknown feature flags are disabled by default
67- return False
166+ return result
68167
69168 if not feature_flag .enabled :
70169 # Feature flags that are disabled are always disabled
71- return False
170+ result ["enabled" ] = FeatureManager ._check_default_disabled_variant (feature_flag )
171+ if feature_flag .allocation :
172+ variant_name = feature_flag .allocation .default_when_disabled
173+ result ["variant" ] = self ._variant_name_to_variant (feature_flag , variant_name )
174+ return result
72175
73176 feature_conditions = feature_flag .conditions
74177 feature_filters = feature_conditions .client_filters
75178
76179 if len (feature_filters ) == 0 :
77180 # Feature flags without any filters return evaluate
78- return True
181+ result ["enabled" ] = True
182+ else :
183+ # The assumed value is no filters is based on the requirement type.
184+ # Requirement type Any assumes false until proven true, All assumes true until proven false
185+ result ["enabled" ] = feature_conditions .requirement_type == REQUIREMENT_TYPE_ALL
79186
80187 for feature_filter in feature_filters :
81188 filter_name = feature_filter [FEATURE_FILTER_NAME ]
82- if filter_name in self ._filters :
83- if feature_conditions .requirement_type == REQUIREMENT_TYPE_ALL :
84- if not await self ._filters [filter_name ].evaluate (feature_filter , ** kwargs ):
85- return False
86- else :
87- if await self ._filters [filter_name ].evaluate (feature_filter , ** kwargs ):
88- return True
89- else :
189+ if filter_name not in self ._filters :
90190 raise ValueError (f"Feature flag { feature_flag_id } has unknown filter { filter_name } " )
91- # If this is reached, and true, default return value is true, else false
92- return feature_conditions .requirement_type == REQUIREMENT_TYPE_ALL
191+ if feature_conditions .requirement_type == REQUIREMENT_TYPE_ALL :
192+ if not await self ._filters [filter_name ].evaluate (feature_filter , ** kwargs ):
193+ result ["enabled" ] = False
194+ break
195+ else :
196+ if await self ._filters [filter_name ].evaluate (feature_filter , ** kwargs ):
197+ result ["enabled" ] = True
198+ break
199+
200+ if feature_flag .allocation and feature_flag .variants :
201+ variant_name = self ._assign_variant (feature_flag , ** kwargs )
202+ if variant_name :
203+ result ["enabled" ] = FeatureManager ._check_variant_override (
204+ feature_flag .variants , variant_name , result ["enabled" ]
205+ )
206+ result ["variant" ] = self ._variant_name_to_variant (feature_flag , variant_name )
207+ return result
208+
209+ variant_name = None
210+ if result ["enabled" ]:
211+ result ["enabled" ] = FeatureManager ._check_default_enabled_variant (feature_flag )
212+ if feature_flag .allocation :
213+ variant_name = feature_flag .allocation .default_when_enabled
214+ else :
215+ result ["enabled" ] = FeatureManager ._check_default_disabled_variant (feature_flag )
216+ if feature_flag .allocation :
217+ variant_name = feature_flag .allocation .default_when_disabled
218+ result ["variant" ] = self ._variant_name_to_variant (feature_flag , variant_name )
219+ return result
93220
94221 def list_feature_flag_names (self ):
95222 """
0 commit comments