Skip to content

Commit eb159d2

Browse files
authored
Split of shared code to base (#34)
1 parent 0fd11f1 commit eb159d2

File tree

3 files changed

+266
-415
lines changed

3 files changed

+266
-415
lines changed

featuremanagement/_featuremanager.py

Lines changed: 13 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,21 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
import logging
7-
import hashlib
8-
from collections.abc import Mapping
97
from typing import overload
108
from ._defaultfilters import TimeWindowFilter, TargetingFilter
119
from ._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

Comments
 (0)