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.
Files changed (58) hide show
  1. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/CHANGELOG.md +6 -1
  2. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3/FeatureManagement.egg-info}/PKG-INFO +1 -1
  3. {featuremanagement-2.0.0b2/FeatureManagement.egg-info → featuremanagement-2.0.0b3}/PKG-INFO +1 -1
  4. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanager.py +5 -30
  5. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featuremanagerbase.py +91 -55
  6. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_allocation.py +2 -2
  7. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_evaluation_event.py +1 -1
  8. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_version.py +1 -1
  9. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featuremanager.py +20 -36
  10. featuremanagement-2.0.0b3/featuremanagement/azuremonitor/_send_telemetry.py +102 -0
  11. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/pyproject.toml +1 -1
  12. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager.py +32 -1
  13. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager_async.py +54 -1
  14. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_variants.py +38 -1
  15. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_variants_async.py +67 -0
  16. featuremanagement-2.0.0b3/tests/test_send_telemetry_appinsights.py +213 -0
  17. featuremanagement-2.0.0b2/featuremanagement/azuremonitor/_send_telemetry.py +0 -102
  18. featuremanagement-2.0.0b2/tests/test_send_telemetry_appinsights.py +0 -78
  19. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/CODE_OF_CONDUCT.md +0 -0
  20. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/SOURCES.txt +0 -0
  21. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/dependency_links.txt +0 -0
  22. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/not-zip-safe +0 -0
  23. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/requires.txt +0 -0
  24. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/FeatureManagement.egg-info/top_level.txt +0 -0
  25. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/LICENSE +0 -0
  26. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/MANIFEST.in +0 -0
  27. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/README.md +0 -0
  28. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/SECURITY.md +0 -0
  29. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/SUPPORT.md +0 -0
  30. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/__init__.py +0 -0
  31. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_defaultfilters.py +0 -0
  32. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_featurefilters.py +0 -0
  33. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/__init__.py +0 -0
  34. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_constants.py +0 -0
  35. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_conditions.py +0 -0
  36. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_feature_flag.py +0 -0
  37. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_targeting_context.py +0 -0
  38. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_telemetry.py +0 -0
  39. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant.py +0 -0
  40. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
  41. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/_models/_variant_reference.py +0 -0
  42. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/__init__.py +0 -0
  43. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_defaultfilters.py +0 -0
  44. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/aio/_featurefilters.py +0 -0
  45. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/azuremonitor/__init__.py +0 -0
  46. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/featuremanagement/py.typed +0 -0
  47. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_flag_sample.py +0 -0
  48. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
  49. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_variant_sample.py +0 -0
  50. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/feature_variant_sample_with_telemetry.py +0 -0
  51. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/formatted_feature_flags.json +0 -0
  52. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/samples/random_filter.py +0 -0
  53. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/setup.cfg +0 -0
  54. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/setup.py +0 -0
  55. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags.py +0 -0
  56. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_default_feature_flags_async.py +0 -0
  57. {featuremanagement-2.0.0b2 → featuremanagement-2.0.0b3}/tests/test_feature_manager_refresh.py +0 -0
  58. {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 VariantAllocationPercentage, DefaultWhenEnabled, and AllocationId to telemetry.
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.0b2
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.0b2
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,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
- 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
- if not variant_name and 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
- if not variant_name and feature.allocation.percentile:
178
- seed = feature.allocation.seed
179
- if not seed:
180
- seed = "allocation\n" + feature.name
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 feature.allocation.percentile:
184
- if box == 100 and percentile_allocation.percentile_to == 100:
185
- variant_name = percentile_allocation.variant
186
- if not percentile_allocation.percentile_to:
187
- continue
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._check_default_enabled_variant(evaluation_event)
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 not feature_flag.variants:
200
- return
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 and isinstance(args[0], str):
229
- return TargetingContext(user_id=args[0], groups=[])
230
- if len(args) == 1 and isinstance(args[0], TargetingContext):
231
- 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
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
- 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
253
289
 
254
290
  def list_feature_flag_names(self) -> List[str]:
255
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
 
@@ -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
@@ -4,4 +4,4 @@
4
4
  # license information.
5
5
  # -------------------------------------------------------------------------
6
6
 
7
- VERSION = "2.0.0b2"
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)
@@ -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)
@@ -19,7 +19,7 @@ build-backend = "setuptools.build_meta"
19
19
 
20
20
  [project]
21
21
  name = "FeatureManagement"
22
- version = "2.0.0b2"
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):