Skip to content

Commit e6f53c9

Browse files
authored
Removing non-b1 features (#25)
* Removing non-beta.1 features * Update _feature_flag.py * fixing tests * Update _featuremanager.py * Update Merge of Targeting Update * Update _featuremanager.py * Update _featuremanager.py
1 parent 34bec23 commit e6f53c9

14 files changed

+8
-1405
lines changed

README.md

Lines changed: 0 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -270,164 +270,6 @@ class MyCustomFilter(FeatureFilter):
270270
...
271271
```
272272

273-
### Feature Variants
274-
275-
When new features are added to an application, there may come a time when a feature has multiple different proposed design options. A common solution for deciding on a design is some form of A/B testing, which involves providing a different version of the feature to different segments of the user base and choosing a version based on user interaction. In this library, this functionality is enabled by representing different configurations of a feature with variants.
276-
277-
Variants enable a feature flag to become more than a simple on/off flag. A variant represents a value of a feature flag that can be a string, a number, a boolean, or even a configuration object. A feature flag that declares variants should define under what circumstances each variant should be used, which is covered in greater detail in the [Allocating Variants](#allocating-variants) section.
278-
279-
```python
280-
class Variant():
281-
282-
@property
283-
def name(self):
284-
285-
@property
286-
def configuration(self):
287-
```
288-
289-
#### Getting Variants
290-
291-
For each feature, a variant can be retrieved using `FeatureManager`'s `get_variant` method. The method returns a `Variant` object that contains the name and configuration of the variant. Once a variant is retrieved, the configuration of a variant can be used directly as JSON from the variant's `configuration` property.
292-
293-
```python
294-
feature_manager = FeatureManager(feature_flags)
295-
296-
variant = feature_manager.get_variant("FeatureU")
297-
298-
my_configuration = variant.configuration
299-
300-
variant = feature_manager.get_variant("FeatureV")
301-
302-
sub_configuration = variant.configuration["json_key"]
303-
```
304-
305-
#### Defining Variants
306-
307-
Each variant has two properties: a name and a configuration. The name is used to refer to a specific variant, and the configuration is the value of that variant. The configuration can be set using either the `configuration_reference` or `configuration_value` properties. `configuration_reference` is a string that references a configuration, this configuration is a key inside of the configuration object passed into `FeatureManager`. `configuration_value` is an inline configuration that can be a string, number, boolean, or json object. If both are specified, `configuration_value` is used. If neither are specified, the returned variant's `configuration` property will be `None`.
308-
309-
A list of all possible variants is defined for each feature under the Variants property.
310-
311-
```json
312-
{
313-
"feature_management": {
314-
"feature_flags": [
315-
{
316-
"id": "FeatureU",
317-
"variants": [
318-
{
319-
"name": "VariantA",
320-
"configuration_reference": "config1"
321-
},
322-
{
323-
"name": "VariantB",
324-
"configuration_value": {
325-
"name": "value"
326-
}
327-
}
328-
]
329-
}
330-
]
331-
}
332-
}
333-
```
334-
335-
#### Allocating Variants
336-
337-
The process of allocating a feature's variants is determined by the `allocation` property of the feature.
338-
339-
```json
340-
"allocation": {
341-
"default_when_enabled": "Small",
342-
"default_when_disabled": "Small",
343-
"user": [
344-
{
345-
"variant": "Big",
346-
"users": [
347-
"Marsha"
348-
]
349-
}
350-
],
351-
"group": [
352-
{
353-
"variant": "Big",
354-
"groups": [
355-
"Ring1"
356-
]
357-
}
358-
],
359-
"percentile": [
360-
{
361-
"variant": "Big",
362-
"from": 0,
363-
"to": 10
364-
}
365-
],
366-
"seed": "13973240"
367-
},
368-
"variants": [
369-
{
370-
"name": "Big",
371-
"configuration_reference": "ShoppingCart:Big"
372-
},
373-
{
374-
"name": "Small",
375-
"configuration_value": "300px"
376-
}
377-
]
378-
```
379-
380-
The `allocation` setting of a feature flag has the following properties:
381-
382-
| Property | Description |
383-
| ---------------- | ---------------- |
384-
| `default_when_disabled` | Specifies which variant should be used when a variant is requested while the feature is considered disabled. |
385-
| `default_when_enabled` | Specifies which variant should be used when a variant is requested while the feature is considered enabled and no other variant was assigned to the user. |
386-
| `user` | Specifies a variant and a list of users to whom that variant should be assigned. |
387-
| `group` | Specifies a variant and a list of groups the current user has to be in for that variant to be assigned. |
388-
| `percentile` | Specifies a variant and a percentage range the user's calculated percentage has to fit into for that variant to be assigned. |
389-
| `seed` | The value which percentage calculations for `percentile` are based on. The percentage calculation for a specific user will be the same across all features if the same `seed` value is used. If no `seed` is specified, then a default seed is created based on the feature name. |
390-
391-
In the above example, if the feature is not enabled, the feature manager will assign the variant marked as `default_when_disabled` to the current user, which is `Small` in this case.
392-
393-
If the feature is enabled, the feature manager will check the `user`, `group`, and `percentile` allocations in that order to assign a variant. For this particular example, if the user being evaluated is named `Marsha`, in the group named `Ring1`, or the user happens to fall between the 0 and 10th percentile, then the specified variant is assigned to the user. In this case, all of these would return the `Big` variant. If none of these allocations match, the user is assigned the `default_when_enabled` variant, which is `Small`.
394-
395-
Allocation logic is similar to the [Microsoft.Targeting](#microsoft-targeting) feature filter, but there are some parameters that are present in targeting that aren't in allocation, and vice versa. The outcomes of targeting and allocation are not related.
396-
397-
### Overriding Enabled State with a Variant
398-
399-
You can use variants to override the enabled state of a feature flag. This gives variants an opportunity to extend the evaluation of a feature flag. If a caller is checking whether a flag that has variants is enabled, the feature manager will check if the variant assigned to the current user is set up to override the result. This is done using the optional variant property `status_override`. By default, this property is set to `None`, which means the variant doesn't affect whether the flag is considered enabled or disabled. Setting `status_override` to `Enabled` allows the variant, when chosen, to override a flag to be enabled. Setting `status_override` to `Disabled` provides the opposite functionality, therefore disabling the flag when the variant is chosen.
400-
401-
If you are using a feature flag with binary variants, the `status_override` property can be very helpful. It allows you to continue using `is_enabled` in your application, all while benefiting from the new features that come with variants, such as percentile allocation and seed.
402-
403-
```json
404-
"allocation": {
405-
"percentile": [{
406-
"variant": "On",
407-
"from": 10,
408-
"to": 20
409-
}],
410-
"default_when_enabled": "Off",
411-
"seed": "Enhanced-Feature-Group"
412-
},
413-
"variants": [
414-
{
415-
"name": "On"
416-
},
417-
{
418-
"name": "Off",
419-
"status_override": "Disabled"
420-
}
421-
],
422-
"enabled_for": [
423-
{
424-
"name": "AlwaysOn"
425-
}
426-
]
427-
```
428-
429-
In the above example, the feature is enabled by the `AlwaysOn` filter. If the current user is in the calculated percentile range of 10 to 20, then the `On` variant is returned. Otherwise, the `Off` variant is returned and because `status_override` is equal to `Disabled`, the feature will now be considered disabled.
430-
431273
## Contributing
432274

433275
This project welcomes contributions and suggestions. Most contributions require you to agree to a

featuremanagement/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ._featuremanager import FeatureManager
77
from ._featurefilters import FeatureFilter
88
from ._defaultfilters import TimeWindowFilter, TargetingFilter
9-
from ._models import FeatureFlag, Variant, TargetingContext
9+
from ._models import TargetingContext
1010

1111
from ._version import VERSION
1212

@@ -16,7 +16,5 @@
1616
"TimeWindowFilter",
1717
"TargetingFilter",
1818
"FeatureFilter",
19-
"FeatureFlag",
20-
"Variant",
2119
"TargetingContext",
2220
]

featuremanagement/_featuremanager.py

Lines changed: 3 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
import logging
7-
import hashlib
87
from collections.abc import Mapping
98
from typing import overload
109
from ._defaultfilters import TimeWindowFilter, TargetingFilter
1110
from ._featurefilters import FeatureFilter
12-
from ._models import FeatureFlag, Variant, EvaluationEvent, TargetingContext
11+
from ._models import FeatureFlag, EvaluationEvent, TargetingContext
1312

1413

1514
FEATURE_MANAGEMENT_KEY = "feature_management"
@@ -89,111 +88,6 @@ def __init__(self, configuration, **kwargs):
8988
raise ValueError("Custom filter must be a subclass of FeatureFilter")
9089
self._filters[feature_filter.name] = feature_filter
9190

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-
19791
def _build_targeting_context(self, args):
19892
"""
19993
Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
@@ -232,31 +126,6 @@ def is_enabled(self, feature_flag_id, *args, **kwargs):
232126
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
233127
return result.enabled
234128

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-
260129
def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
261130
feature_conditions = feature_flag.conditions
262131
feature_filters = feature_conditions.client_filters
@@ -285,30 +154,6 @@ def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
285154
break
286155
return evaluation_event
287156

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-
312157
def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
313158
"""
314159
Determine if the feature flag is enabled for the given context.
@@ -334,16 +179,9 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
334179

335180
if not feature_flag.enabled:
336181
# Feature flags that are disabled are always disabled
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)
182+
return EvaluationEvent(enabled=False)
345183

346-
return self._assign_allocation(feature_flag, evaluation_event, targeting_context)
184+
return self._check_feature_filters(feature_flag, targeting_context, **kwargs)
347185

348186
def list_feature_flag_names(self):
349187
"""

0 commit comments

Comments
 (0)