FeatureManagement 2.0.0b1__tar.gz → 2.0.0b3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/CHANGELOG.md +10 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3/FeatureManagement.egg-info}/PKG-INFO +1 -1
- {featuremanagement-2.0.0b1/FeatureManagement.egg-info → featuremanagement-2.0.0b3}/PKG-INFO +1 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanager.py +5 -30
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanagerbase.py +91 -52
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_allocation.py +7 -7
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_evaluation_event.py +1 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_flag.py +2 -4
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_version.py +1 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featuremanager.py +20 -36
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/_send_telemetry.py +48 -14
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/pyproject.toml +1 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager.py +32 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager_async.py +54 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_variants.py +78 -1
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_variants_async.py +67 -0
- featuremanagement-2.0.0b3/tests/test_send_telemetry_appinsights.py +213 -0
- featuremanagement-2.0.0b1/tests/test_send_telemetry_appinsights.py +0 -78
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/CODE_OF_CONDUCT.md +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/SOURCES.txt +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/dependency_links.txt +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/not-zip-safe +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/requires.txt +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/top_level.txt +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/LICENSE +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/MANIFEST.in +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/README.md +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/SECURITY.md +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/SUPPORT.md +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/__init__.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_defaultfilters.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/__init__.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_constants.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_conditions.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_targeting_context.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_telemetry.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_reference.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/__init__.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_defaultfilters.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/__init__.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/py.typed +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_flag_sample.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_variant_sample.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_variant_sample_with_telemetry.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/formatted_feature_flags.json +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/random_filter.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/setup.cfg +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/setup.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags_async.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager_refresh.py +0 -0
- {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/validation_tests/test_json_validations.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
## 2.0.0b3 (11/14/2024)
|
|
4
|
+
|
|
5
|
+
* Fixes a bug where no allocation reason is set if a user is allocated to exactly 100.
|
|
6
|
+
* Fixes a bug where VariantAssignmentPercentage wasn't correct for default when enabled.
|
|
7
|
+
|
|
8
|
+
## 2.0.0b2 (10/11/2024)
|
|
9
|
+
|
|
10
|
+
* Adds VariantAssignmentPercentage, DefaultWhenEnabled, and AllocationId to telemetry.
|
|
11
|
+
* Allocation seed value is now None by default, and only defaults to `allocation\n<feature.id>` when assigning variants.
|
|
12
|
+
|
|
3
13
|
## 2.0.0b1 (09/10/2024)
|
|
4
14
|
|
|
5
15
|
* Adds support for Feature Variants.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b3
|
|
4
4
|
Summary: A library for enabling/disabling features at runtime.
|
|
5
5
|
Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/appconfiguration/feature-management
|
|
6
6
|
Author: Microsoft Corporation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b3
|
|
4
4
|
Summary: A library for enabling/disabling features at runtime.
|
|
5
5
|
Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/appconfiguration/feature-management
|
|
6
6
|
Author: Microsoft Corporation
|
{featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanager.py
RENAMED
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
-
import logging
|
|
7
6
|
from typing import cast, overload, Any, Optional, Dict, Mapping, List
|
|
8
7
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
9
8
|
from ._featurefilters import FeatureFilter
|
|
10
9
|
from ._models import EvaluationEvent, Variant, TargetingContext
|
|
11
10
|
from ._featuremanagerbase import (
|
|
12
|
-
_get_feature_flag,
|
|
13
11
|
FeatureManagerBase,
|
|
14
12
|
PROVIDED_FEATURE_FILTERS,
|
|
15
|
-
FEATURE_MANAGEMENT_KEY,
|
|
16
13
|
REQUIREMENT_TYPE_ALL,
|
|
17
14
|
FEATURE_FILTER_NAME,
|
|
18
15
|
)
|
|
@@ -143,35 +140,13 @@ class FeatureManager(FeatureManagerBase):
|
|
|
143
140
|
Determine if the feature flag is enabled for the given context.
|
|
144
141
|
|
|
145
142
|
:param str feature_flag_id: Name of the feature flag.
|
|
146
|
-
:
|
|
147
|
-
:
|
|
143
|
+
:param TargetingContext targeting_context: Targeting context.
|
|
144
|
+
:return: EvaluationEvent for the given context.
|
|
145
|
+
:rtype: EvaluationEvent
|
|
148
146
|
"""
|
|
149
|
-
|
|
150
|
-
self._cache = {}
|
|
151
|
-
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
152
|
-
|
|
153
|
-
if not self._cache.get(feature_flag_id):
|
|
154
|
-
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
155
|
-
self._cache[feature_flag_id] = feature_flag
|
|
156
|
-
else:
|
|
157
|
-
feature_flag = self._cache.get(feature_flag_id)
|
|
158
|
-
|
|
159
|
-
evaluation_event = EvaluationEvent(feature_flag)
|
|
160
|
-
if not feature_flag:
|
|
161
|
-
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
162
|
-
# Unknown feature flags are disabled by default
|
|
163
|
-
return evaluation_event
|
|
164
|
-
|
|
165
|
-
if not feature_flag.enabled:
|
|
166
|
-
# Feature flags that are disabled are always disabled
|
|
167
|
-
FeatureManager._check_default_disabled_variant(evaluation_event)
|
|
168
|
-
if feature_flag.allocation:
|
|
169
|
-
variant_name = feature_flag.allocation.default_when_disabled
|
|
170
|
-
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
171
|
-
evaluation_event.feature = feature_flag
|
|
147
|
+
evaluation_event, done = super()._check_feature_base(feature_flag_id)
|
|
172
148
|
|
|
173
|
-
|
|
174
|
-
evaluation_event.enabled = False
|
|
149
|
+
if done:
|
|
175
150
|
return evaluation_event
|
|
176
151
|
|
|
177
152
|
self._check_feature_filters(evaluation_event, targeting_context, **kwargs)
|
{featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanagerbase.py
RENAMED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import hashlib
|
|
7
|
+
import logging
|
|
7
8
|
from abc import ABC
|
|
8
9
|
from typing import List, Optional, Dict, Tuple, Any, Mapping
|
|
9
10
|
from ._models import FeatureFlag, Variant, VariantAssignmentReason, TargetingContext, EvaluationEvent, VariantReference
|
|
@@ -78,7 +79,7 @@ class FeatureManagerBase(ABC):
|
|
|
78
79
|
self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None)
|
|
79
80
|
|
|
80
81
|
@staticmethod
|
|
81
|
-
def
|
|
82
|
+
def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
|
|
82
83
|
"""
|
|
83
84
|
A method called when the feature flag is disabled, to determine what the default variant should be. If there is
|
|
84
85
|
no allocation, then None is set as the value of the variant in the EvaluationEvent.
|
|
@@ -88,7 +89,7 @@ class FeatureManagerBase(ABC):
|
|
|
88
89
|
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
|
|
89
90
|
if not evaluation_event.feature or not evaluation_event.feature.allocation:
|
|
90
91
|
return
|
|
91
|
-
FeatureManagerBase.
|
|
92
|
+
FeatureManagerBase._assign_variant_override(
|
|
92
93
|
evaluation_event.feature.variants,
|
|
93
94
|
evaluation_event.feature.allocation.default_when_disabled,
|
|
94
95
|
False,
|
|
@@ -96,7 +97,7 @@ class FeatureManagerBase(ABC):
|
|
|
96
97
|
)
|
|
97
98
|
|
|
98
99
|
@staticmethod
|
|
99
|
-
def
|
|
100
|
+
def _assign_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
|
|
100
101
|
"""
|
|
101
102
|
A method called when the feature flag is enabled, to determine what the default variant should be. If there is
|
|
102
103
|
no allocation, then None is set as the value of the variant in the EvaluationEvent.
|
|
@@ -106,7 +107,7 @@ class FeatureManagerBase(ABC):
|
|
|
106
107
|
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
|
|
107
108
|
if not evaluation_event.feature or not evaluation_event.feature.allocation:
|
|
108
109
|
return
|
|
109
|
-
FeatureManagerBase.
|
|
110
|
+
FeatureManagerBase._assign_variant_override(
|
|
110
111
|
evaluation_event.feature.variants,
|
|
111
112
|
evaluation_event.feature.allocation.default_when_enabled,
|
|
112
113
|
True,
|
|
@@ -114,7 +115,7 @@ class FeatureManagerBase(ABC):
|
|
|
114
115
|
)
|
|
115
116
|
|
|
116
117
|
@staticmethod
|
|
117
|
-
def
|
|
118
|
+
def _assign_variant_override(
|
|
118
119
|
variants: Optional[List[VariantReference]],
|
|
119
120
|
default_variant_name: Optional[str],
|
|
120
121
|
status: bool,
|
|
@@ -149,53 +150,57 @@ class FeatureManagerBase(ABC):
|
|
|
149
150
|
|
|
150
151
|
return (context_marker / (2**32 - 1)) * 100
|
|
151
152
|
|
|
152
|
-
def _assign_variant(
|
|
153
|
-
self, feature_flag: FeatureFlag, targeting_context: TargetingContext, evaluation_event: EvaluationEvent
|
|
154
|
-
) -> None:
|
|
153
|
+
def _assign_variant(self, targeting_context: TargetingContext, evaluation_event: EvaluationEvent) -> None:
|
|
155
154
|
"""
|
|
156
155
|
Assign a variant to the user based on the allocation.
|
|
157
156
|
|
|
158
|
-
:param FeatureFlag feature_flag: Feature flag object.
|
|
159
157
|
:param TargetingContext targeting_context: Targeting context.
|
|
160
158
|
:param EvaluationEvent evaluation_event: Evaluation event object.
|
|
161
159
|
"""
|
|
162
|
-
|
|
160
|
+
feature_flag = evaluation_event.feature
|
|
163
161
|
variant_name = None
|
|
164
|
-
if not
|
|
162
|
+
if not feature_flag or not feature_flag.variants or not feature_flag.allocation:
|
|
165
163
|
return
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
|
|
165
|
+
allocation = feature_flag.allocation
|
|
166
|
+
groups = targeting_context.groups
|
|
167
|
+
|
|
168
|
+
if allocation.user and targeting_context.user_id:
|
|
169
|
+
for user_allocation in allocation.user:
|
|
168
170
|
if targeting_context.user_id in user_allocation.users:
|
|
169
171
|
evaluation_event.reason = VariantAssignmentReason.USER
|
|
170
172
|
variant_name = user_allocation.variant
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
|
|
174
|
+
if not variant_name and allocation.group and groups:
|
|
175
|
+
for group_allocation in allocation.group:
|
|
176
|
+
if any(group in group_allocation.groups for group in groups):
|
|
177
|
+
evaluation_event.reason = VariantAssignmentReason.GROUP
|
|
178
|
+
variant_name = group_allocation.variant
|
|
179
|
+
|
|
180
|
+
if not variant_name and allocation.percentile:
|
|
181
|
+
seed = allocation.seed or f"allocation\n{feature_flag.name}"
|
|
182
|
+
context_id = f"{targeting_context.user_id}\n{seed}"
|
|
179
183
|
box: float = self._is_targeted(context_id)
|
|
180
|
-
for percentile_allocation in
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if percentile_allocation.percentile_from <= box < percentile_allocation.percentile_to:
|
|
184
|
+
for percentile_allocation in allocation.percentile:
|
|
185
|
+
percentile_to: int = percentile_allocation.percentile_to
|
|
186
|
+
if (box == 100 and percentile_to == 100) or (
|
|
187
|
+
percentile_allocation.percentile_from <= box < percentile_to
|
|
188
|
+
):
|
|
186
189
|
evaluation_event.reason = VariantAssignmentReason.PERCENTILE
|
|
187
190
|
variant_name = percentile_allocation.variant
|
|
191
|
+
break
|
|
192
|
+
|
|
188
193
|
if not variant_name:
|
|
189
|
-
FeatureManagerBase.
|
|
194
|
+
FeatureManagerBase._assign_default_enabled_variant(evaluation_event)
|
|
190
195
|
if feature_flag.allocation:
|
|
191
196
|
evaluation_event.variant = self._variant_name_to_variant(
|
|
192
197
|
feature_flag, feature_flag.allocation.default_when_enabled
|
|
193
198
|
)
|
|
194
199
|
return
|
|
200
|
+
|
|
195
201
|
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
FeatureManagerBase._check_variant_override(feature_flag.variants, variant_name, True, evaluation_event)
|
|
202
|
+
if feature_flag.variants:
|
|
203
|
+
FeatureManagerBase._assign_variant_override(feature_flag.variants, variant_name, True, evaluation_event)
|
|
199
204
|
|
|
200
205
|
def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Optional[str]) -> Optional[Variant]:
|
|
201
206
|
"""
|
|
@@ -205,10 +210,9 @@ class FeatureManagerBase(ABC):
|
|
|
205
210
|
:param str variant_name: Name of the variant.
|
|
206
211
|
:return: Variant object.
|
|
207
212
|
"""
|
|
208
|
-
if not feature_flag.variants:
|
|
209
|
-
return None
|
|
210
|
-
if not variant_name:
|
|
213
|
+
if not feature_flag.variants or not variant_name:
|
|
211
214
|
return None
|
|
215
|
+
|
|
212
216
|
for variant_reference in feature_flag.variants:
|
|
213
217
|
if variant_reference.name == variant_name:
|
|
214
218
|
return Variant(variant_reference.name, variant_reference.configuration_value)
|
|
@@ -222,31 +226,66 @@ class FeatureManagerBase(ABC):
|
|
|
222
226
|
:param args: Arguments to build the TargetingContext.
|
|
223
227
|
:return: TargetingContext
|
|
224
228
|
"""
|
|
225
|
-
if len(args) == 1
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
if len(args) == 1:
|
|
230
|
+
arg = args[0]
|
|
231
|
+
if isinstance(arg, str):
|
|
232
|
+
return TargetingContext(user_id=arg, groups=[])
|
|
233
|
+
if isinstance(arg, TargetingContext):
|
|
234
|
+
return arg
|
|
229
235
|
return TargetingContext()
|
|
230
236
|
|
|
231
237
|
def _assign_allocation(self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext) -> None:
|
|
232
238
|
feature_flag = evaluation_event.feature
|
|
233
239
|
if not feature_flag:
|
|
234
240
|
return
|
|
235
|
-
if feature_flag.variants:
|
|
236
|
-
if not feature_flag.allocation:
|
|
237
|
-
if evaluation_event.enabled:
|
|
238
|
-
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
|
|
239
|
-
return
|
|
240
|
-
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
|
|
241
|
-
return
|
|
242
|
-
if not evaluation_event.enabled:
|
|
243
|
-
FeatureManagerBase._check_default_disabled_variant(evaluation_event)
|
|
244
|
-
evaluation_event.variant = self._variant_name_to_variant(
|
|
245
|
-
feature_flag, feature_flag.allocation.default_when_disabled
|
|
246
|
-
)
|
|
247
|
-
return
|
|
248
241
|
|
|
249
|
-
|
|
242
|
+
if not feature_flag.variants or not feature_flag.allocation:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if not evaluation_event.enabled:
|
|
246
|
+
FeatureManagerBase._assign_default_disabled_variant(evaluation_event)
|
|
247
|
+
evaluation_event.variant = self._variant_name_to_variant(
|
|
248
|
+
feature_flag, feature_flag.allocation.default_when_disabled
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
self._assign_variant(targeting_context, evaluation_event)
|
|
253
|
+
|
|
254
|
+
def _check_feature_base(self, feature_flag_id: str) -> Tuple[EvaluationEvent, bool]:
|
|
255
|
+
"""
|
|
256
|
+
Determine if the feature flag is enabled for the given context.
|
|
257
|
+
|
|
258
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
259
|
+
:return: The evaluation event and if the feature filters need to be checked.
|
|
260
|
+
:rtype: evaluation_event, bool
|
|
261
|
+
"""
|
|
262
|
+
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
|
|
263
|
+
self._cache = {}
|
|
264
|
+
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
265
|
+
|
|
266
|
+
if not self._cache.get(feature_flag_id):
|
|
267
|
+
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
268
|
+
self._cache[feature_flag_id] = feature_flag
|
|
269
|
+
else:
|
|
270
|
+
feature_flag = self._cache.get(feature_flag_id)
|
|
271
|
+
|
|
272
|
+
evaluation_event = EvaluationEvent(feature_flag)
|
|
273
|
+
if not feature_flag:
|
|
274
|
+
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
275
|
+
# Unknown feature flags are disabled by default
|
|
276
|
+
return evaluation_event, True
|
|
277
|
+
|
|
278
|
+
if not feature_flag.enabled:
|
|
279
|
+
# Feature flags that are disabled are always disabled
|
|
280
|
+
self._assign_default_disabled_variant(evaluation_event)
|
|
281
|
+
if feature_flag.allocation:
|
|
282
|
+
variant_name = feature_flag.allocation.default_when_disabled
|
|
283
|
+
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
284
|
+
|
|
285
|
+
# If a feature flag is disabled and override can't enable it
|
|
286
|
+
evaluation_event.enabled = False
|
|
287
|
+
return evaluation_event, True
|
|
288
|
+
return evaluation_event, False
|
|
250
289
|
|
|
251
290
|
def list_feature_flag_names(self) -> List[str]:
|
|
252
291
|
"""
|
{featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_allocation.py
RENAMED
|
@@ -36,7 +36,7 @@ class PercentileAllocation:
|
|
|
36
36
|
def __init__(self) -> None:
|
|
37
37
|
self._variant: Optional[str] = None
|
|
38
38
|
self._percentile_from: int = 0
|
|
39
|
-
self._percentile_to:
|
|
39
|
+
self._percentile_to: int = 0
|
|
40
40
|
|
|
41
41
|
@classmethod
|
|
42
42
|
def convert_from_json(cls, json: Mapping[str, Union[str, int]]) -> "PercentileAllocation":
|
|
@@ -88,7 +88,7 @@ class PercentileAllocation:
|
|
|
88
88
|
return self._percentile_from
|
|
89
89
|
|
|
90
90
|
@property
|
|
91
|
-
def percentile_to(self) ->
|
|
91
|
+
def percentile_to(self) -> int:
|
|
92
92
|
"""
|
|
93
93
|
Get the ending percentile for the allocation.
|
|
94
94
|
|
|
@@ -103,16 +103,16 @@ class Allocation:
|
|
|
103
103
|
Represents an allocation configuration for a feature flag.
|
|
104
104
|
"""
|
|
105
105
|
|
|
106
|
-
def __init__(self
|
|
106
|
+
def __init__(self) -> None:
|
|
107
107
|
self._default_when_enabled = None
|
|
108
108
|
self._default_when_disabled = None
|
|
109
109
|
self._user: List[UserAllocation] = []
|
|
110
110
|
self._group: List[GroupAllocation] = []
|
|
111
111
|
self._percentile: List[PercentileAllocation] = []
|
|
112
|
-
self._seed =
|
|
112
|
+
self._seed = None
|
|
113
113
|
|
|
114
114
|
@classmethod
|
|
115
|
-
def convert_from_json(cls, json: Dict[str, Any]
|
|
115
|
+
def convert_from_json(cls, json: Dict[str, Any]) -> Optional["Allocation"]:
|
|
116
116
|
"""
|
|
117
117
|
Convert a JSON object to Allocation.
|
|
118
118
|
|
|
@@ -123,7 +123,7 @@ class Allocation:
|
|
|
123
123
|
"""
|
|
124
124
|
if not json:
|
|
125
125
|
return None
|
|
126
|
-
allocation = cls(
|
|
126
|
+
allocation = cls()
|
|
127
127
|
allocation._default_when_enabled = json.get(DEFAULT_WHEN_ENABLED)
|
|
128
128
|
allocation._default_when_disabled = json.get(DEFAULT_WHEN_DISABLED)
|
|
129
129
|
allocation._user = []
|
|
@@ -197,7 +197,7 @@ class Allocation:
|
|
|
197
197
|
return self._percentile
|
|
198
198
|
|
|
199
199
|
@property
|
|
200
|
-
def seed(self) -> str:
|
|
200
|
+
def seed(self) -> Optional[str]:
|
|
201
201
|
"""
|
|
202
202
|
Get the seed for the allocation.
|
|
203
203
|
|
{featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_flag.py
RENAMED
|
@@ -51,9 +51,7 @@ class FeatureFlag:
|
|
|
51
51
|
)
|
|
52
52
|
else:
|
|
53
53
|
feature_flag._conditions = FeatureConditions()
|
|
54
|
-
feature_flag._allocation = Allocation.convert_from_json(
|
|
55
|
-
json_value.get(FEATURE_FLAG_ALLOCATION, None), feature_flag._id
|
|
56
|
-
)
|
|
54
|
+
feature_flag._allocation = Allocation.convert_from_json(json_value.get(FEATURE_FLAG_ALLOCATION, None))
|
|
57
55
|
if FEATURE_FLAG_VARIANTS in json_value:
|
|
58
56
|
variants: List[Mapping[str, Any]] = json_value.get(FEATURE_FLAG_VARIANTS, [])
|
|
59
57
|
feature_flag._variants = []
|
|
@@ -66,7 +64,7 @@ class FeatureFlag:
|
|
|
66
64
|
return feature_flag
|
|
67
65
|
|
|
68
66
|
@property
|
|
69
|
-
def name(self) ->
|
|
67
|
+
def name(self) -> str:
|
|
70
68
|
"""
|
|
71
69
|
Get the name of the feature flag.
|
|
72
70
|
|
{featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featuremanager.py
RENAMED
|
@@ -4,16 +4,13 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import inspect
|
|
7
|
-
import
|
|
8
|
-
from typing import cast, overload, Mapping, Dict, Any, Optional, List
|
|
7
|
+
from typing import cast, overload, Any, Optional, Dict, Mapping, List
|
|
9
8
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
10
9
|
from ._featurefilters import FeatureFilter
|
|
11
|
-
from .._models import EvaluationEvent,
|
|
10
|
+
from .._models import EvaluationEvent, Variant, TargetingContext
|
|
12
11
|
from .._featuremanagerbase import (
|
|
13
|
-
_get_feature_flag,
|
|
14
12
|
FeatureManagerBase,
|
|
15
13
|
PROVIDED_FEATURE_FILTERS,
|
|
16
|
-
FEATURE_MANAGEMENT_KEY,
|
|
17
14
|
REQUIREMENT_TYPE_ALL,
|
|
18
15
|
FEATURE_FILTER_NAME,
|
|
19
16
|
)
|
|
@@ -63,11 +60,16 @@ class FeatureManager(FeatureManagerBase):
|
|
|
63
60
|
targeting_context = self._build_targeting_context(args)
|
|
64
61
|
|
|
65
62
|
result = await self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
66
|
-
if
|
|
63
|
+
if (
|
|
64
|
+
self._on_feature_evaluated
|
|
65
|
+
and result.feature
|
|
66
|
+
and result.feature.telemetry.enabled
|
|
67
|
+
and callable(self._on_feature_evaluated)
|
|
68
|
+
):
|
|
67
69
|
result.user = targeting_context.user_id
|
|
68
70
|
if inspect.iscoroutinefunction(self._on_feature_evaluated):
|
|
69
71
|
await self._on_feature_evaluated(result)
|
|
70
|
-
|
|
72
|
+
else:
|
|
71
73
|
self._on_feature_evaluated(result)
|
|
72
74
|
return result.enabled
|
|
73
75
|
|
|
@@ -94,11 +96,16 @@ class FeatureManager(FeatureManagerBase):
|
|
|
94
96
|
targeting_context = self._build_targeting_context(args)
|
|
95
97
|
|
|
96
98
|
result = await self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
97
|
-
if
|
|
99
|
+
if (
|
|
100
|
+
self._on_feature_evaluated
|
|
101
|
+
and result.feature
|
|
102
|
+
and result.feature.telemetry.enabled
|
|
103
|
+
and callable(self._on_feature_evaluated)
|
|
104
|
+
):
|
|
98
105
|
result.user = targeting_context.user_id
|
|
99
106
|
if inspect.iscoroutinefunction(self._on_feature_evaluated):
|
|
100
107
|
await self._on_feature_evaluated(result)
|
|
101
|
-
|
|
108
|
+
else:
|
|
102
109
|
self._on_feature_evaluated(result)
|
|
103
110
|
return result.variant
|
|
104
111
|
|
|
@@ -141,35 +148,12 @@ class FeatureManager(FeatureManagerBase):
|
|
|
141
148
|
|
|
142
149
|
:param str feature_flag_id: Name of the feature flag.
|
|
143
150
|
:param TargetingContext targeting_context: Targeting context.
|
|
144
|
-
:return:
|
|
145
|
-
:rtype:
|
|
151
|
+
:return: EvaluationEvent for the given context.
|
|
152
|
+
:rtype: EvaluationEvent
|
|
146
153
|
"""
|
|
147
|
-
|
|
148
|
-
self._cache = {}
|
|
149
|
-
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
150
|
-
|
|
151
|
-
if not self._cache.get(feature_flag_id):
|
|
152
|
-
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
153
|
-
self._cache[feature_flag_id] = feature_flag
|
|
154
|
-
else:
|
|
155
|
-
feature_flag = self._cache.get(feature_flag_id)
|
|
156
|
-
|
|
157
|
-
evaluation_event = EvaluationEvent(feature_flag)
|
|
158
|
-
if not feature_flag:
|
|
159
|
-
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
160
|
-
# Unknown feature flags are disabled by default
|
|
161
|
-
return evaluation_event
|
|
162
|
-
|
|
163
|
-
if not feature_flag.enabled:
|
|
164
|
-
# Feature flags that are disabled are always disabled
|
|
165
|
-
FeatureManager._check_default_disabled_variant(evaluation_event)
|
|
166
|
-
if feature_flag.allocation:
|
|
167
|
-
variant_name = feature_flag.allocation.default_when_disabled
|
|
168
|
-
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
169
|
-
evaluation_event.feature = feature_flag
|
|
154
|
+
evaluation_event, done = super()._check_feature_base(feature_flag_id)
|
|
170
155
|
|
|
171
|
-
|
|
172
|
-
evaluation_event.enabled = False
|
|
156
|
+
if done:
|
|
173
157
|
return evaluation_event
|
|
174
158
|
|
|
175
159
|
await self._check_feature_filters(evaluation_event, targeting_context, **kwargs)
|
|
@@ -25,6 +25,8 @@ REASON = "VariantAssignmentReason"
|
|
|
25
25
|
|
|
26
26
|
EVENT_NAME = "FeatureEvaluation"
|
|
27
27
|
|
|
28
|
+
EVALUATION_EVENT_VERSION = "1.1.0"
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
def track_event(event_name: str, user: str, event_properties: Optional[Dict[str, Optional[str]]] = None) -> None:
|
|
30
32
|
"""
|
|
@@ -36,10 +38,12 @@ def track_event(event_name: str, user: str, event_properties: Optional[Dict[str,
|
|
|
36
38
|
"""
|
|
37
39
|
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
|
|
38
40
|
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
event_properties = event_properties or {}
|
|
43
|
+
|
|
41
44
|
if user:
|
|
42
45
|
event_properties[TARGETING_ID] = user
|
|
46
|
+
|
|
43
47
|
azure_monitor_track_event(event_name, event_properties)
|
|
44
48
|
|
|
45
49
|
|
|
@@ -51,18 +55,48 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
|
|
|
51
55
|
"""
|
|
52
56
|
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
|
|
53
57
|
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
|
|
59
|
+
feature = evaluation_event.feature
|
|
60
|
+
|
|
61
|
+
if not feature:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
event: Dict[str, Optional[str]] = {
|
|
65
|
+
FEATURE_NAME: feature.name,
|
|
66
|
+
ENABLED: str(evaluation_event.enabled),
|
|
67
|
+
"Version": EVALUATION_EVENT_VERSION,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
reason = evaluation_event.reason
|
|
71
|
+
variant = evaluation_event.variant
|
|
72
|
+
|
|
73
|
+
event[REASON] = reason.value
|
|
74
|
+
|
|
75
|
+
if variant:
|
|
76
|
+
event[VARIANT] = variant.name
|
|
77
|
+
|
|
78
|
+
# VariantAllocationPercentage
|
|
79
|
+
allocation_percentage = 0
|
|
80
|
+
if reason == VariantAssignmentReason.DEFAULT_WHEN_ENABLED:
|
|
81
|
+
event["VariantAssignmentPercentage"] = str(100)
|
|
82
|
+
if feature.allocation:
|
|
83
|
+
for allocation in feature.allocation.percentile:
|
|
84
|
+
allocation_percentage += allocation.percentile_to - allocation.percentile_from
|
|
85
|
+
event["VariantAssignmentPercentage"] = str(100 - allocation_percentage)
|
|
86
|
+
elif reason == VariantAssignmentReason.PERCENTILE:
|
|
87
|
+
if feature.allocation and feature.allocation.percentile:
|
|
88
|
+
for allocation in feature.allocation.percentile:
|
|
89
|
+
if variant and allocation.variant == variant.name:
|
|
90
|
+
allocation_percentage += allocation.percentile_to - allocation.percentile_from
|
|
91
|
+
event["VariantAssignmentPercentage"] = str(allocation_percentage)
|
|
92
|
+
|
|
93
|
+
# DefaultWhenEnabled
|
|
94
|
+
if feature.allocation and feature.allocation.default_when_enabled:
|
|
95
|
+
event["DefaultWhenEnabled"] = feature.allocation.default_when_enabled
|
|
96
|
+
|
|
97
|
+
if feature.telemetry:
|
|
98
|
+
for metadata_key, metadata_value in feature.telemetry.metadata.items():
|
|
66
99
|
if metadata_key not in event:
|
|
67
100
|
event[metadata_key] = metadata_value
|
|
101
|
+
|
|
68
102
|
track_event(EVENT_NAME, evaluation_event.user, event_properties=event)
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
|
+
import unittest
|
|
6
7
|
import pytest
|
|
7
8
|
from featuremanagement import FeatureManager, FeatureFilter
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class TestFeatureManager:
|
|
11
|
+
class TestFeatureManager(unittest.TestCase):
|
|
12
|
+
|
|
13
|
+
def __init__(self, methodName="runTest"):
|
|
14
|
+
super().__init__(methodName=methodName)
|
|
15
|
+
self.called_telemetry = False
|
|
16
|
+
|
|
11
17
|
# method: feature_manager_creation
|
|
12
18
|
def test_empty_feature_manager_creation(self):
|
|
13
19
|
feature_manager = FeatureManager({})
|
|
@@ -29,6 +35,12 @@ class TestFeatureManager:
|
|
|
29
35
|
assert feature_manager.is_enabled("Alpha")
|
|
30
36
|
assert not feature_manager.is_enabled("Beta")
|
|
31
37
|
|
|
38
|
+
# method: feature_manager_creation
|
|
39
|
+
def test_feature_manager_creation_invalid_feature_filter(self):
|
|
40
|
+
feature_flags = {"feature_management": {"feature_flags": []}}
|
|
41
|
+
with self.assertRaises(ValueError):
|
|
42
|
+
FeatureManager(feature_flags, feature_filters=["invalid_filter"])
|
|
43
|
+
|
|
32
44
|
# method: feature_manager_creation
|
|
33
45
|
def test_feature_manager_creation_with_filters(self):
|
|
34
46
|
feature_flags = {
|
|
@@ -119,6 +131,25 @@ class TestFeatureManager:
|
|
|
119
131
|
assert e_info.type == ValueError
|
|
120
132
|
assert e_info.value.args[0] == "Feature flag Alpha has unknown filter UnknownFilter"
|
|
121
133
|
|
|
134
|
+
# method: feature_manager_creation
|
|
135
|
+
def test_feature_with_telemetry(self):
|
|
136
|
+
self.called_telemetry = False
|
|
137
|
+
feature_flags = {
|
|
138
|
+
"feature_management": {
|
|
139
|
+
"feature_flags": [
|
|
140
|
+
{"id": "Alpha", "description": "", "enabled": "true", "telemetry": {"enabled": "true"}},
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
feature_manager = FeatureManager(feature_flags, on_feature_evaluated=self.fake_telemetry_callback)
|
|
145
|
+
assert feature_manager is not None
|
|
146
|
+
assert feature_manager.is_enabled("Alpha")
|
|
147
|
+
assert self.called_telemetry
|
|
148
|
+
|
|
149
|
+
def fake_telemetry_callback(self, evaluation_event):
|
|
150
|
+
assert evaluation_event
|
|
151
|
+
self.called_telemetry = True
|
|
152
|
+
|
|
122
153
|
|
|
123
154
|
class AlwaysOn(FeatureFilter):
|
|
124
155
|
def evaluate(self, context, **kwargs):
|