Skip to content

Commit 0498455

Browse files
authored
Small Fixes (#45)
* Feature Management small fixes * added tests * Update CHANGELOG.md * update to b3 * cspell fixes * Moving more shared code to base * pylint fix * more telemetry tests * spelling * Update test_feature_variants_async.py * fixing tests * fixing init * inits * Update _send_telemetry.py * fixes bug where default allocation calc is wrong * Update _send_telemetry.py * modifying the default value of percentile_to * Update _allocation.py * changed to super * removed extra feature flag instance * review comments * Evaluation Event defeult to NONE * removing unneeded code
1 parent 045f959 commit 0498455

16 files changed

+457
-171
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
## 2.0.0b3 (Unreleased)
4+
5+
* Fixes a bug where no allocation reason is set if a user is allocated to exactly 100.
6+
37
## 2.0.0b2 (10/11/2024)
48

59
* Adds VariantAssignmentPercentage, DefaultWhenEnabled, and AllocationId to telemetry.

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
project = "FeatureManagement"
1111
copyright = "2024, Microsoft"
1212
author = "Microsoft"
13-
release = "2.0.0b2"
13+
release = "2.0.0b3"
1414

1515
# -- General configuration ---------------------------------------------------
1616

featuremanagement/_featuremanager.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# -------------------------------------------------------------------------
6-
import logging
76
from typing import cast, overload, Any, Optional, Dict, Mapping, List
87
from ._defaultfilters import TimeWindowFilter, TargetingFilter
98
from ._featurefilters import FeatureFilter
109
from ._models import EvaluationEvent, Variant, TargetingContext
1110
from ._featuremanagerbase import (
12-
_get_feature_flag,
1311
FeatureManagerBase,
1412
PROVIDED_FEATURE_FILTERS,
15-
FEATURE_MANAGEMENT_KEY,
1613
REQUIREMENT_TYPE_ALL,
1714
FEATURE_FILTER_NAME,
1815
)
@@ -143,35 +140,13 @@ def _check_feature(
143140
Determine if the feature flag is enabled for the given context.
144141
145142
: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
148146
"""
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)
172148

173-
# If a feature flag is disabled and override can't enable it
174-
evaluation_event.enabled = False
149+
if done:
175150
return evaluation_event
176151

177152
self._check_feature_filters(evaluation_event, targeting_context, **kwargs)

featuremanagement/_featuremanagerbase.py

Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
import hashlib
7+
import logging
78
from abc import ABC
89
from typing import List, Optional, Dict, Tuple, Any, Mapping
910
from ._models import FeatureFlag, Variant, VariantAssignmentReason, TargetingContext, EvaluationEvent, VariantReference
@@ -78,7 +79,7 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Any):
7879
self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None)
7980

8081
@staticmethod
81-
def _check_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
82+
def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
8283
"""
8384
A method called when the feature flag is disabled, to determine what the default variant should be. If there is
8485
no allocation, then None is set as the value of the variant in the EvaluationEvent.
@@ -88,15 +89,15 @@ def _check_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
8889
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
8990
if not evaluation_event.feature or not evaluation_event.feature.allocation:
9091
return
91-
FeatureManagerBase._check_variant_override(
92+
FeatureManagerBase._assign_variant_override(
9293
evaluation_event.feature.variants,
9394
evaluation_event.feature.allocation.default_when_disabled,
9495
False,
9596
evaluation_event,
9697
)
9798

9899
@staticmethod
99-
def _check_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
100+
def _assign_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
100101
"""
101102
A method called when the feature flag is enabled, to determine what the default variant should be. If there is
102103
no allocation, then None is set as the value of the variant in the EvaluationEvent.
@@ -106,15 +107,15 @@ def _check_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
106107
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
107108
if not evaluation_event.feature or not evaluation_event.feature.allocation:
108109
return
109-
FeatureManagerBase._check_variant_override(
110+
FeatureManagerBase._assign_variant_override(
110111
evaluation_event.feature.variants,
111112
evaluation_event.feature.allocation.default_when_enabled,
112113
True,
113114
evaluation_event,
114115
)
115116

116117
@staticmethod
117-
def _check_variant_override(
118+
def _assign_variant_override(
118119
variants: Optional[List[VariantReference]],
119120
default_variant_name: Optional[str],
120121
status: bool,
@@ -149,56 +150,57 @@ def _is_targeted(context_id: str) -> float:
149150

150151
return (context_marker / (2**32 - 1)) * 100
151152

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:
155154
"""
156155
Assign a variant to the user based on the allocation.
157156
158-
:param FeatureFlag feature_flag: Feature flag object.
159157
:param TargetingContext targeting_context: Targeting context.
160158
:param EvaluationEvent evaluation_event: Evaluation event object.
161159
"""
162-
feature = evaluation_event.feature
160+
feature_flag = evaluation_event.feature
163161
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:
165163
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:
168170
if targeting_context.user_id in user_allocation.users:
169171
evaluation_event.reason = VariantAssignmentReason.USER
170172
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}"
182183
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+
):
189189
evaluation_event.reason = VariantAssignmentReason.PERCENTILE
190190
variant_name = percentile_allocation.variant
191+
break
192+
191193
if not variant_name:
192-
FeatureManagerBase._check_default_enabled_variant(evaluation_event)
194+
FeatureManagerBase._assign_default_enabled_variant(evaluation_event)
193195
if feature_flag.allocation:
194196
evaluation_event.variant = self._variant_name_to_variant(
195197
feature_flag, feature_flag.allocation.default_when_enabled
196198
)
197199
return
200+
198201
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)
202204

203205
def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Optional[str]) -> Optional[Variant]:
204206
"""
@@ -208,10 +210,9 @@ def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Opti
208210
:param str variant_name: Name of the variant.
209211
:return: Variant object.
210212
"""
211-
if not feature_flag.variants:
212-
return None
213-
if not variant_name:
213+
if not feature_flag.variants or not variant_name:
214214
return None
215+
215216
for variant_reference in feature_flag.variants:
216217
if variant_reference.name == variant_name:
217218
return Variant(variant_reference.name, variant_reference.configuration_value)
@@ -225,31 +226,66 @@ def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
225226
:param args: Arguments to build the TargetingContext.
226227
:return: TargetingContext
227228
"""
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
232235
return TargetingContext()
233236

234237
def _assign_allocation(self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext) -> None:
235238
feature_flag = evaluation_event.feature
236239
if not feature_flag:
237240
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
251241

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
253289

254290
def list_feature_flag_names(self) -> List[str]:
255291
"""

featuremanagement/_models/_allocation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PercentileAllocation:
3636
def __init__(self) -> None:
3737
self._variant: Optional[str] = None
3838
self._percentile_from: int = 0
39-
self._percentile_to: Optional[int] = None
39+
self._percentile_to: int = 0
4040

4141
@classmethod
4242
def convert_from_json(cls, json: Mapping[str, Union[str, int]]) -> "PercentileAllocation":
@@ -88,7 +88,7 @@ def percentile_from(self) -> int:
8888
return self._percentile_from
8989

9090
@property
91-
def percentile_to(self) -> Optional[int]:
91+
def percentile_to(self) -> int:
9292
"""
9393
Get the ending percentile for the allocation.
9494

featuremanagement/_models/_evaluation_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ def __init__(self, feature_flag: Optional[FeatureFlag]):
2424
self.user = ""
2525
self.enabled = False
2626
self.variant: Optional[Variant] = None
27-
self.reason: Optional[VariantAssignmentReason] = None
27+
self.reason: VariantAssignmentReason = VariantAssignmentReason.NONE

featuremanagement/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# license information.
55
# -------------------------------------------------------------------------
66

7-
VERSION = "2.0.0b2"
7+
VERSION = "2.0.0b3"

0 commit comments

Comments
 (0)