44# license information.
55# -------------------------------------------------------------------------
66import logging
7- import hashlib
8- from collections .abc import Mapping
97from typing import overload
108from ._defaultfilters import TimeWindowFilter , TargetingFilter
119from ._featurefilters import FeatureFilter
12- from ._models import FeatureFlag , Variant , EvaluationEvent , VariantAssignmentReason , TargetingContext
13-
14-
15- FEATURE_MANAGEMENT_KEY = "feature_management"
16- FEATURE_FLAG_KEY = "feature_flags"
17-
18- PROVIDED_FEATURE_FILTERS = "feature_filters"
19- FEATURE_FILTER_NAME = "name"
20- REQUIREMENT_TYPE_ALL = "All"
21- REQUIREMENT_TYPE_ANY = "Any"
22-
23- FEATURE_FILTER_PARAMETERS = "parameters"
24-
25-
26- def _get_feature_flag (configuration , feature_flag_name ):
27- """
28- Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
29-
30- :param Mapping configuration: Configuration object.
31- :param str feature_flag_name: Name of the feature flag.
32- :return: FeatureFlag
33- :rtype: FeatureFlag
34- """
35- feature_management = configuration .get (FEATURE_MANAGEMENT_KEY )
36- if not feature_management or not isinstance (feature_management , Mapping ):
37- return None
38- feature_flags = feature_management .get (FEATURE_FLAG_KEY )
39- if not feature_flags or not isinstance (feature_flags , list ):
40- return None
41-
42- for feature_flag in feature_flags :
43- if feature_flag .get ("id" ) == feature_flag_name :
44- return FeatureFlag .convert_from_json (feature_flag )
45-
46- return None
47-
48-
49- def _list_feature_flag_names (configuration ):
50- """
51- List of all feature flag names.
52-
53- :param Mapping configuration: Configuration object.
54- :return: List of feature flag names.
55- """
56- feature_flag_names = []
57- feature_management = configuration .get (FEATURE_MANAGEMENT_KEY )
58- if not feature_management or not isinstance (feature_management , Mapping ):
59- return []
60- feature_flags = feature_management .get (FEATURE_FLAG_KEY )
61- if not feature_flags or not isinstance (feature_flags , list ):
62- return []
63-
64- for feature_flag in feature_flags :
65- feature_flag_names .append (feature_flag .get ("id" ))
66-
67- return feature_flag_names
68-
69-
70- class FeatureManager :
10+ from ._models import EvaluationEvent
11+ from ._featuremanagerbase import (
12+ _get_feature_flag ,
13+ FeatureManagerBase ,
14+ PROVIDED_FEATURE_FILTERS ,
15+ FEATURE_MANAGEMENT_KEY ,
16+ REQUIREMENT_TYPE_ALL ,
17+ FEATURE_FILTER_NAME ,
18+ )
19+
20+
21+ class FeatureManager (FeatureManagerBase ):
7122 """
7223 Feature Manager that determines if a feature flag is enabled for the given context.
7324
@@ -78,161 +29,14 @@ class FeatureManager:
7829 """
7930
8031 def __init__ (self , configuration , ** kwargs ):
81- self ._filters = {}
82- if configuration is None or not isinstance (configuration , Mapping ):
83- raise AttributeError ("Configuration must be a non-empty dictionary" )
84- self ._configuration = configuration
85- self ._cache = {}
86- self ._copy = configuration .get (FEATURE_MANAGEMENT_KEY )
87- self ._on_feature_evaluated = kwargs .pop ("on_feature_evaluated" , None )
32+ super ().__init__ (configuration , ** kwargs )
8833 filters = [TimeWindowFilter (), TargetingFilter ()] + kwargs .pop (PROVIDED_FEATURE_FILTERS , [])
8934
9035 for feature_filter in filters :
9136 if not isinstance (feature_filter , FeatureFilter ):
9237 raise ValueError ("Custom filter must be a subclass of FeatureFilter" )
9338 self ._filters [feature_filter .name ] = feature_filter
9439
95- @staticmethod
96- def _check_default_disabled_variant (evaluation_event ):
97- """
98- A method called when the feature flag is disabled, to determine what the default variant should be. If there is
99- no allocation, then None is set as the value of the variant in the EvaluationEvent.
100-
101- :param EvaluationEvent evaluation_event: Evaluation event object.
102- """
103- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
104- if not evaluation_event .feature .allocation :
105- return
106- FeatureManager ._check_variant_override (
107- evaluation_event .feature .variants ,
108- evaluation_event .feature .allocation .default_when_disabled ,
109- False ,
110- evaluation_event ,
111- )
112-
113- @staticmethod
114- def _check_default_enabled_variant (evaluation_event ):
115- """
116- A method called when the feature flag is enabled, to determine what the default variant should be. If there is
117- no allocation, then None is set as the value of the variant in the EvaluationEvent.
118-
119- :param EvaluationEvent evaluation_event: Evaluation event object.
120- """
121- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
122- if not evaluation_event .feature .allocation :
123- return
124- FeatureManager ._check_variant_override (
125- evaluation_event .feature .variants ,
126- evaluation_event .feature .allocation .default_when_enabled ,
127- True ,
128- evaluation_event ,
129- )
130-
131- @staticmethod
132- def _check_variant_override (variants , default_variant_name , status , evaluation_event ):
133- """
134- A method to check if a variant is overridden to be enabled or disabled by the variant.
135-
136- :param list[Variant] variants: List of variants.
137- :param str default_variant_name: Name of the default variant.
138- :param bool status: Status of the feature flag.
139- :param EvaluationEvent evaluation_event: Evaluation event object.
140- """
141- if not variants or not default_variant_name :
142- evaluation_event .enabled = status
143- return
144- for variant in variants :
145- if variant .name == default_variant_name :
146- if variant .status_override == "Enabled" :
147- evaluation_event .enabled = True
148- return
149- if variant .status_override == "Disabled" :
150- evaluation_event .enabled = False
151- return
152- evaluation_event .enabled = status
153-
154- @staticmethod
155- def _is_targeted (context_id ):
156- """Determine if the user is targeted for the given context"""
157- hashed_context_id = hashlib .sha256 (context_id .encode ()).digest ()
158- context_marker = int .from_bytes (hashed_context_id [:4 ], byteorder = "little" , signed = False )
159-
160- return (context_marker / (2 ** 32 - 1 )) * 100
161-
162- def _assign_variant (self , feature_flag , targeting_context , evaluation_event ):
163- """
164- Assign a variant to the user based on the allocation.
165-
166- :param TargetingContext targeting_context: Targeting context.
167- :param EvaluationEvent evaluation_event: Evaluation event object.
168- """
169- feature = evaluation_event .feature
170- variant_name = None
171- if not feature .variants or not feature .allocation :
172- return
173- if feature .allocation .user and targeting_context .user_id :
174- for user_allocation in feature .allocation .user :
175- if targeting_context .user_id in user_allocation .users :
176- evaluation_event .reason = VariantAssignmentReason .USER
177- variant_name = user_allocation .variant
178- elif feature .allocation .group and len (targeting_context .groups ) > 0 :
179- for group_allocation in feature .allocation .group :
180- for group in targeting_context .groups :
181- if group in group_allocation .groups :
182- evaluation_event .reason = VariantAssignmentReason .GROUP
183- variant_name = group_allocation .variant
184- elif feature .allocation .percentile :
185- context_id = targeting_context .user_id + "\n " + feature .allocation .seed
186- box = self ._is_targeted (context_id )
187- for percentile_allocation in feature .allocation .percentile :
188- if box == 100 and percentile_allocation .percentile_to == 100 :
189- variant_name = percentile_allocation .variant
190- if percentile_allocation .percentile_from <= box < percentile_allocation .percentile_to :
191- evaluation_event .reason = VariantAssignmentReason .PERCENTILE
192- variant_name = percentile_allocation .variant
193- if not variant_name :
194- FeatureManager ._check_default_enabled_variant (evaluation_event )
195- evaluation_event .variant = self ._variant_name_to_variant (
196- feature_flag , feature_flag .allocation .default_when_enabled
197- )
198- return
199- evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
200- FeatureManager ._check_variant_override (feature_flag .variants , variant_name , True , evaluation_event )
201-
202- def _variant_name_to_variant (self , feature_flag , variant_name ):
203- """
204- Get the variant object from the variant name.
205-
206- :param FeatureFlag feature_flag: Feature flag object.
207- :param str variant_name: Name of the variant.
208- :return: Variant object.
209- """
210- if not feature_flag .variants :
211- return None
212- if not variant_name :
213- return None
214- for variant_reference in feature_flag .variants :
215- if variant_reference .name == variant_name :
216- configuration = variant_reference .configuration_value
217- if not configuration :
218- configuration = self ._configuration .get (variant_reference .configuration_reference )
219- return Variant (variant_reference .name , configuration )
220- return None
221-
222- def _build_targeting_context (self , args ):
223- """
224- Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
225- returns an empty context.
226-
227- :param args: Arguments to build the TargetingContext.
228- :return: TargetingContext
229- """
230- if len (args ) == 1 and isinstance (args [0 ], str ):
231- return TargetingContext (user_id = args [0 ], groups = [])
232- if len (args ) == 1 and isinstance (args [0 ], TargetingContext ):
233- return args [0 ]
234- return TargetingContext ()
235-
23640 @overload
23741 def is_enabled (self , feature_flag_id , user_id , ** kwargs ):
23842 """
@@ -315,24 +119,6 @@ def _check_feature_filters(self, evaluation_event, targeting_context, **kwargs):
315119 evaluation_event .enabled = True
316120 break
317121
318- def _assign_allocation (self , evaluation_event , targeting_context ):
319- feature_flag = evaluation_event .feature
320- if feature_flag .variants :
321- if not feature_flag .allocation :
322- if evaluation_event .enabled :
323- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
324- return
325- evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
326- return
327- if not evaluation_event .enabled :
328- FeatureManager ._check_default_disabled_variant (evaluation_event )
329- evaluation_event .variant = self ._variant_name_to_variant (
330- feature_flag , feature_flag .allocation .default_when_disabled
331- )
332- return
333-
334- self ._assign_variant (feature_flag , targeting_context , evaluation_event )
335-
336122 def _check_feature (self , feature_flag_id , targeting_context , ** kwargs ):
337123 """
338124 Determine if the feature flag is enabled for the given context.
@@ -373,9 +159,3 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
373159
374160 self ._assign_allocation (evaluation_event , targeting_context )
375161 return evaluation_event
376-
377- def list_feature_flag_names (self ):
378- """
379- List of all feature flag names.
380- """
381- return _list_feature_flag_names (self ._configuration )
0 commit comments