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.
Files changed (57) hide show
  1. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/CHANGELOG.md +10 -0
  2. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3/FeatureManagement.egg-info}/PKG-INFO +1 -1
  3. {featuremanagement-2.0.0b1/FeatureManagement.egg-info → featuremanagement-2.0.0b3}/PKG-INFO +1 -1
  4. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanager.py +5 -30
  5. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanagerbase.py +91 -52
  6. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_allocation.py +7 -7
  7. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_evaluation_event.py +1 -1
  8. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_flag.py +2 -4
  9. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_version.py +1 -1
  10. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featuremanager.py +20 -36
  11. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/_send_telemetry.py +48 -14
  12. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/pyproject.toml +1 -1
  13. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager.py +32 -1
  14. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager_async.py +54 -1
  15. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_variants.py +78 -1
  16. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_variants_async.py +67 -0
  17. featuremanagement-2.0.0b3/tests/test_send_telemetry_appinsights.py +213 -0
  18. featuremanagement-2.0.0b1/tests/test_send_telemetry_appinsights.py +0 -78
  19. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/CODE_OF_CONDUCT.md +0 -0
  20. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/SOURCES.txt +0 -0
  21. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/dependency_links.txt +0 -0
  22. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/not-zip-safe +0 -0
  23. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/requires.txt +0 -0
  24. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/top_level.txt +0 -0
  25. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/LICENSE +0 -0
  26. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/MANIFEST.in +0 -0
  27. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/README.md +0 -0
  28. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/SECURITY.md +0 -0
  29. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/SUPPORT.md +0 -0
  30. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/__init__.py +0 -0
  31. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_defaultfilters.py +0 -0
  32. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_featurefilters.py +0 -0
  33. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/__init__.py +0 -0
  34. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_constants.py +0 -0
  35. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_conditions.py +0 -0
  36. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_targeting_context.py +0 -0
  37. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_telemetry.py +0 -0
  38. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant.py +0 -0
  39. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
  40. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_reference.py +0 -0
  41. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/__init__.py +0 -0
  42. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_defaultfilters.py +0 -0
  43. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featurefilters.py +0 -0
  44. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/__init__.py +0 -0
  45. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/featuremanagement/py.typed +0 -0
  46. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_flag_sample.py +0 -0
  47. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
  48. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_variant_sample.py +0 -0
  49. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/feature_variant_sample_with_telemetry.py +0 -0
  50. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/formatted_feature_flags.json +0 -0
  51. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/samples/random_filter.py +0 -0
  52. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/setup.cfg +0 -0
  53. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/setup.py +0 -0
  54. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags.py +0 -0
  55. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags_async.py +0 -0
  56. {featuremanagement-2.0.0b1 → featuremanagement-2.0.0b3}/tests/test_feature_manager_refresh.py +0 -0
  57. {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.0b1
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.0b1
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
@@ -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
- :return: True if the feature flag is enabled for the given context.
147
- :rtype: bool
143
+ :param TargetingContext targeting_context: Targeting context.
144
+ :return: EvaluationEvent for the given context.
145
+ :rtype: EvaluationEvent
148
146
  """
149
- if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
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
- # If a feature flag is disabled and override can't enable it
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)
@@ -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 _check_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
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._check_variant_override(
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 _check_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
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._check_variant_override(
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 _check_variant_override(
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
- feature = evaluation_event.feature
160
+ feature_flag = evaluation_event.feature
163
161
  variant_name = None
164
- if not feature or not feature.variants or not feature.allocation:
162
+ if not feature_flag or not feature_flag.variants or not feature_flag.allocation:
165
163
  return
166
- if feature.allocation.user and targeting_context.user_id:
167
- for user_allocation in feature.allocation.user:
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
- elif feature.allocation.group and len(targeting_context.groups) > 0:
172
- for group_allocation in feature.allocation.group:
173
- for group in targeting_context.groups:
174
- if group in group_allocation.groups:
175
- evaluation_event.reason = VariantAssignmentReason.GROUP
176
- variant_name = group_allocation.variant
177
- elif feature.allocation.percentile:
178
- context_id = targeting_context.user_id + "\n" + feature.allocation.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}"
179
183
  box: float = self._is_targeted(context_id)
180
- for percentile_allocation in feature.allocation.percentile:
181
- if box == 100 and percentile_allocation.percentile_to == 100:
182
- variant_name = percentile_allocation.variant
183
- if not percentile_allocation.percentile_to:
184
- continue
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._check_default_enabled_variant(evaluation_event)
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 not feature_flag.variants:
197
- return
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 and isinstance(args[0], str):
226
- return TargetingContext(user_id=args[0], groups=[])
227
- if len(args) == 1 and isinstance(args[0], TargetingContext):
228
- return args[0]
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
- self._assign_variant(feature_flag, targeting_context, evaluation_event)
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
  """
@@ -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: Optional[int] = None
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) -> Optional[int]:
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, feature_name: str) -> None:
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 = "allocation\n" + feature_name
112
+ self._seed = None
113
113
 
114
114
  @classmethod
115
- def convert_from_json(cls, json: Dict[str, Any], feature_name: str) -> Optional["Allocation"]:
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(feature_name)
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
 
@@ -24,4 +24,4 @@ class EvaluationEvent:
24
24
  self.user = ""
25
25
  self.enabled = False
26
26
  self.variant: Optional[Variant] = None
27
- self.reason: Optional[VariantAssignmentReason] = None
27
+ self.reason: VariantAssignmentReason = VariantAssignmentReason.NONE
@@ -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) -> Optional[str]:
67
+ def name(self) -> str:
70
68
  """
71
69
  Get the name of the feature flag.
72
70
 
@@ -4,4 +4,4 @@
4
4
  # license information.
5
5
  # -------------------------------------------------------------------------
6
6
 
7
- VERSION = "2.0.0b1"
7
+ VERSION = "2.0.0b3"
@@ -4,16 +4,13 @@
4
4
  # license information.
5
5
  # -------------------------------------------------------------------------
6
6
  import inspect
7
- import logging
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, TargetingContext, Variant
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 self._on_feature_evaluated and result.feature and result.feature.telemetry.enabled:
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
- elif callable(self._on_feature_evaluated):
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 self._on_feature_evaluated and result.feature and result.feature.telemetry.enabled:
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
- elif callable(self._on_feature_evaluated):
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: True if the feature flag is enabled for the given context.
145
- :rtype: bool
151
+ :return: EvaluationEvent for the given context.
152
+ :rtype: EvaluationEvent
146
153
  """
147
- if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
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
- # If a feature flag is disabled and override can't enable it
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
- if event_properties is None:
40
- event_properties = {}
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
- event = {}
55
- if evaluation_event.feature:
56
- event[FEATURE_NAME] = evaluation_event.feature.name
57
- event[ENABLED] = str(evaluation_event.enabled)
58
-
59
- if evaluation_event.reason and evaluation_event.reason != VariantAssignmentReason.NONE:
60
- if evaluation_event.variant:
61
- event[VARIANT] = evaluation_event.variant.name
62
- event[REASON] = evaluation_event.reason.value
63
-
64
- if evaluation_event.feature and evaluation_event.feature.telemetry:
65
- for metadata_key, metadata_value in evaluation_event.feature.telemetry.metadata.items():
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)
@@ -19,7 +19,7 @@ build-backend = "setuptools.build_meta"
19
19
 
20
20
  [project]
21
21
  name = "FeatureManagement"
22
- version = "2.0.0b1"
22
+ version = "2.0.0b3"
23
23
  authors = [
24
24
  { name="Microsoft Corporation", email="appconfig@microsoft.com" },
25
25
  ]
@@ -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):