FeatureManagement 2.0.0b2__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.0b2 → featuremanagement-2.0.0b3}/CHANGELOG.md +6 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3/FeatureManagement.egg-info}/PKG-INFO +1 -1
- {featuremanagement-2.0.0b2/FeatureManagement.egg-info → featuremanagement-2.0.0b3}/PKG-INFO +1 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanager.py +5 -30
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanagerbase.py +91 -55
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_allocation.py +2 -2
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_evaluation_event.py +1 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_version.py +1 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featuremanager.py +20 -36
- featuremanagement-2.0.0b3/featuremanagement/azuremonitor/_send_telemetry.py +102 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/pyproject.toml +1 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager.py +32 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager_async.py +54 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_variants.py +38 -1
- {featuremanagement-2.0.0b2 → 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.0b2/featuremanagement/azuremonitor/_send_telemetry.py +0 -102
- featuremanagement-2.0.0b2/tests/test_send_telemetry_appinsights.py +0 -78
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/CODE_OF_CONDUCT.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/SOURCES.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/dependency_links.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/not-zip-safe +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/requires.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/top_level.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/LICENSE +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/MANIFEST.in +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/README.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/SECURITY.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/SUPPORT.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_defaultfilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_constants.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_conditions.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_flag.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_targeting_context.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_telemetry.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_reference.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_defaultfilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/py.typed +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_flag_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_variant_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_variant_sample_with_telemetry.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/formatted_feature_flags.json +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/random_filter.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/setup.cfg +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/setup.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags_async.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager_refresh.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/validation_tests/test_json_validations.py +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
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
|
+
|
|
3
8
|
## 2.0.0b2 (10/11/2024)
|
|
4
9
|
|
|
5
|
-
* Adds
|
|
10
|
+
* Adds VariantAssignmentPercentage, DefaultWhenEnabled, and AllocationId to telemetry.
|
|
6
11
|
* Allocation seed value is now None by default, and only defaults to `allocation\n<feature.id>` when assigning variants.
|
|
7
12
|
|
|
8
13
|
## 2.0.0b1 (09/10/2024)
|
|
@@ -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.0b2 → 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.0b2 → 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,56 +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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
context_id = targeting_context.user_id + "\n" + seed
|
|
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}"
|
|
182
183
|
box: float = self._is_targeted(context_id)
|
|
183
|
-
for percentile_allocation in
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
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
|
+
):
|
|
189
189
|
evaluation_event.reason = VariantAssignmentReason.PERCENTILE
|
|
190
190
|
variant_name = percentile_allocation.variant
|
|
191
|
+
break
|
|
192
|
+
|
|
191
193
|
if not variant_name:
|
|
192
|
-
FeatureManagerBase.
|
|
194
|
+
FeatureManagerBase._assign_default_enabled_variant(evaluation_event)
|
|
193
195
|
if feature_flag.allocation:
|
|
194
196
|
evaluation_event.variant = self._variant_name_to_variant(
|
|
195
197
|
feature_flag, feature_flag.allocation.default_when_enabled
|
|
196
198
|
)
|
|
197
199
|
return
|
|
200
|
+
|
|
198
201
|
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
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)
|
|
202
204
|
|
|
203
205
|
def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Optional[str]) -> Optional[Variant]:
|
|
204
206
|
"""
|
|
@@ -208,10 +210,9 @@ class FeatureManagerBase(ABC):
|
|
|
208
210
|
:param str variant_name: Name of the variant.
|
|
209
211
|
:return: Variant object.
|
|
210
212
|
"""
|
|
211
|
-
if not feature_flag.variants:
|
|
212
|
-
return None
|
|
213
|
-
if not variant_name:
|
|
213
|
+
if not feature_flag.variants or not variant_name:
|
|
214
214
|
return None
|
|
215
|
+
|
|
215
216
|
for variant_reference in feature_flag.variants:
|
|
216
217
|
if variant_reference.name == variant_name:
|
|
217
218
|
return Variant(variant_reference.name, variant_reference.configuration_value)
|
|
@@ -225,31 +226,66 @@ class FeatureManagerBase(ABC):
|
|
|
225
226
|
:param args: Arguments to build the TargetingContext.
|
|
226
227
|
:return: TargetingContext
|
|
227
228
|
"""
|
|
228
|
-
if len(args) == 1
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
232
235
|
return TargetingContext()
|
|
233
236
|
|
|
234
237
|
def _assign_allocation(self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext) -> None:
|
|
235
238
|
feature_flag = evaluation_event.feature
|
|
236
239
|
if not feature_flag:
|
|
237
240
|
return
|
|
238
|
-
if feature_flag.variants:
|
|
239
|
-
if not feature_flag.allocation:
|
|
240
|
-
if evaluation_event.enabled:
|
|
241
|
-
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
|
|
242
|
-
return
|
|
243
|
-
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
|
|
244
|
-
return
|
|
245
|
-
if not evaluation_event.enabled:
|
|
246
|
-
FeatureManagerBase._check_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
241
|
|
|
252
|
-
|
|
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
|
|
253
289
|
|
|
254
290
|
def list_feature_flag_names(self) -> List[str]:
|
|
255
291
|
"""
|
{featuremanagement-2.0.0b2 → 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
|
|
{featuremanagement-2.0.0b2 → 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)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
|
+
# license information.
|
|
5
|
+
# --------------------------------------------------------------------------
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
from .._models import VariantAssignmentReason, EvaluationEvent
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore
|
|
12
|
+
|
|
13
|
+
HAS_AZURE_MONITOR_EVENTS_EXTENSION = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HAS_AZURE_MONITOR_EVENTS_EXTENSION = False
|
|
16
|
+
logging.warning(
|
|
17
|
+
"azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FEATURE_NAME = "FeatureName"
|
|
21
|
+
ENABLED = "Enabled"
|
|
22
|
+
TARGETING_ID = "TargetingId"
|
|
23
|
+
VARIANT = "Variant"
|
|
24
|
+
REASON = "VariantAssignmentReason"
|
|
25
|
+
|
|
26
|
+
EVENT_NAME = "FeatureEvaluation"
|
|
27
|
+
|
|
28
|
+
EVALUATION_EVENT_VERSION = "1.1.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def track_event(event_name: str, user: str, event_properties: Optional[Dict[str, Optional[str]]] = None) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Tracks an event with the specified name and properties.
|
|
34
|
+
|
|
35
|
+
:param str event_name: The name of the event.
|
|
36
|
+
:param str user: The user ID to associate with the event.
|
|
37
|
+
:param dict[str, str] event_properties: A dictionary of named string properties.
|
|
38
|
+
"""
|
|
39
|
+
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
event_properties = event_properties or {}
|
|
43
|
+
|
|
44
|
+
if user:
|
|
45
|
+
event_properties[TARGETING_ID] = user
|
|
46
|
+
|
|
47
|
+
azure_monitor_track_event(event_name, event_properties)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Publishes the telemetry for a feature's evaluation event.
|
|
53
|
+
|
|
54
|
+
:param EvaluationEvent evaluation_event: The evaluation event to publish telemetry for.
|
|
55
|
+
"""
|
|
56
|
+
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
|
|
57
|
+
return
|
|
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():
|
|
99
|
+
if metadata_key not in event:
|
|
100
|
+
event[metadata_key] = metadata_value
|
|
101
|
+
|
|
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):
|