FeatureManagement 2.0.0b2__tar.gz → 2.1.0__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.1.0/CHANGELOG.md +3 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0/FeatureManagement.egg-info}/PKG-INFO +6 -2
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/SOURCES.txt +10 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/top_level.txt +1 -0
- {featuremanagement-2.0.0b2/FeatureManagement.egg-info → featuremanagement-2.1.0}/PKG-INFO +6 -2
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_defaultfilters.py +34 -6
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featuremanager.py +27 -32
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featuremanagerbase.py +102 -58
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_allocation.py +2 -2
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_evaluation_event.py +1 -1
- featuremanagement-2.1.0/featuremanagement/_time_window_filter/__init__.py +9 -0
- featuremanagement-2.1.0/featuremanagement/_time_window_filter/_models.py +149 -0
- featuremanagement-2.1.0/featuremanagement/_time_window_filter/_recurrence_evaluator.py +125 -0
- featuremanagement-2.1.0/featuremanagement/_time_window_filter/_recurrence_validator.py +164 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_version.py +1 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_featuremanager.py +45 -37
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/azuremonitor/__init__.py +2 -1
- featuremanagement-2.1.0/featuremanagement/azuremonitor/_send_telemetry.py +154 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/pyproject.toml +1 -1
- featuremanagement-2.1.0/samples/feature_variant_sample_with_targeting_accessor.py +36 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/formatted_feature_flags.json +1 -0
- featuremanagement-2.1.0/samples/quarty_sample.py +60 -0
- featuremanagement-2.1.0/tests/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_default_feature_flags.py +49 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_default_feature_flags_async.py +50 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager.py +32 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager_async.py +54 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_variants.py +38 -1
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_variants_async.py +67 -0
- featuremanagement-2.1.0/tests/test_send_telemetry_appinsights.py +250 -0
- featuremanagement-2.1.0/tests/time_window_filter/test_recurrence_evaluator.py +281 -0
- featuremanagement-2.1.0/tests/time_window_filter/test_recurrence_validator.py +321 -0
- featuremanagement-2.1.0/tests/time_window_filter/test_time_window_filter_models.py +133 -0
- featuremanagement-2.0.0b2/CHANGELOG.md +0 -27
- 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.1.0}/CODE_OF_CONDUCT.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/dependency_links.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/not-zip-safe +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/requires.txt +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/LICENSE +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/MANIFEST.in +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/README.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/SECURITY.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/SUPPORT.md +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_constants.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_feature_conditions.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_feature_flag.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_targeting_context.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_telemetry.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant_reference.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/__init__.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_defaultfilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_featurefilters.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/py.typed +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_flag_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_variant_sample.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_variant_sample_with_telemetry.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/random_filter.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/setup.cfg +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/setup.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager_refresh.py +0 -0
- {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/validation_tests/test_json_validations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -43,6 +43,10 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Provides-Extra: azuremonitor
|
|
45
45
|
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
|
|
46
|
+
Dynamic: author
|
|
47
|
+
Dynamic: home-page
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
Dynamic: requires-python
|
|
46
50
|
|
|
47
51
|
# Microsoft Feature Management for Python
|
|
48
52
|
|
{featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/SOURCES.txt
RENAMED
|
@@ -31,6 +31,10 @@ featuremanagement/_models/_telemetry.py
|
|
|
31
31
|
featuremanagement/_models/_variant.py
|
|
32
32
|
featuremanagement/_models/_variant_assignment_reason.py
|
|
33
33
|
featuremanagement/_models/_variant_reference.py
|
|
34
|
+
featuremanagement/_time_window_filter/__init__.py
|
|
35
|
+
featuremanagement/_time_window_filter/_models.py
|
|
36
|
+
featuremanagement/_time_window_filter/_recurrence_evaluator.py
|
|
37
|
+
featuremanagement/_time_window_filter/_recurrence_validator.py
|
|
34
38
|
featuremanagement/aio/__init__.py
|
|
35
39
|
featuremanagement/aio/_defaultfilters.py
|
|
36
40
|
featuremanagement/aio/_featurefilters.py
|
|
@@ -40,9 +44,12 @@ featuremanagement/azuremonitor/_send_telemetry.py
|
|
|
40
44
|
samples/feature_flag_sample.py
|
|
41
45
|
samples/feature_flag_with_azure_app_configuration_sample.py
|
|
42
46
|
samples/feature_variant_sample.py
|
|
47
|
+
samples/feature_variant_sample_with_targeting_accessor.py
|
|
43
48
|
samples/feature_variant_sample_with_telemetry.py
|
|
44
49
|
samples/formatted_feature_flags.json
|
|
50
|
+
samples/quarty_sample.py
|
|
45
51
|
samples/random_filter.py
|
|
52
|
+
tests/__init__.py
|
|
46
53
|
tests/test_default_feature_flags.py
|
|
47
54
|
tests/test_default_feature_flags_async.py
|
|
48
55
|
tests/test_feature_manager.py
|
|
@@ -51,4 +58,7 @@ tests/test_feature_manager_refresh.py
|
|
|
51
58
|
tests/test_feature_variants.py
|
|
52
59
|
tests/test_feature_variants_async.py
|
|
53
60
|
tests/test_send_telemetry_appinsights.py
|
|
61
|
+
tests/time_window_filter/test_recurrence_evaluator.py
|
|
62
|
+
tests/time_window_filter/test_recurrence_validator.py
|
|
63
|
+
tests/time_window_filter/test_time_window_filter_models.py
|
|
54
64
|
tests/validation_tests/test_json_validations.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -43,6 +43,10 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Provides-Extra: azuremonitor
|
|
45
45
|
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
|
|
46
|
+
Dynamic: author
|
|
47
|
+
Dynamic: home-page
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
Dynamic: requires-python
|
|
46
50
|
|
|
47
51
|
# Microsoft Feature Management for Python
|
|
48
52
|
|
|
@@ -9,6 +9,7 @@ from datetime import datetime, timezone
|
|
|
9
9
|
from email.utils import parsedate_to_datetime
|
|
10
10
|
from typing import cast, List, Mapping, Optional, Dict, Any
|
|
11
11
|
from ._featurefilters import FeatureFilter
|
|
12
|
+
from ._time_window_filter import Recurrence, is_match, TimeWindowFilterSettings
|
|
12
13
|
|
|
13
14
|
FEATURE_FLAG_NAME_KEY = "feature_name"
|
|
14
15
|
ROLLOUT_PERCENTAGE_KEY = "RolloutPercentage"
|
|
@@ -18,6 +19,15 @@ PARAMETERS_KEY = "parameters"
|
|
|
18
19
|
# Time Window Constants
|
|
19
20
|
START_KEY = "Start"
|
|
20
21
|
END_KEY = "End"
|
|
22
|
+
TIME_WINDOW_FILTER_SETTING_RECURRENCE = "Recurrence"
|
|
23
|
+
|
|
24
|
+
# Time Window Exceptions
|
|
25
|
+
TIME_WINDOW_FILTER_INVALID = (
|
|
26
|
+
"{}: The {} feature filter is not valid for feature {}. It must specify either {}, {}, or both."
|
|
27
|
+
)
|
|
28
|
+
TIME_WINDOW_FILTER_INVALID_RECURRENCE = (
|
|
29
|
+
"{}: The {} feature filter is not valid for feature {}. It must specify both {} and {} when Recurrence is not None."
|
|
30
|
+
)
|
|
21
31
|
|
|
22
32
|
# Targeting kwargs
|
|
23
33
|
TARGETED_USER_KEY = "user"
|
|
@@ -31,6 +41,8 @@ EXCLUSION_KEY = "Exclusion"
|
|
|
31
41
|
FEATURE_FILTER_NAME_KEY = "Name"
|
|
32
42
|
IGNORE_CASE_KEY = "ignore_case"
|
|
33
43
|
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
34
46
|
|
|
35
47
|
class TargetingException(Exception):
|
|
36
48
|
"""
|
|
@@ -52,19 +64,35 @@ class TimeWindowFilter(FeatureFilter):
|
|
|
52
64
|
:return: True if the current time is within the time window.
|
|
53
65
|
:rtype: bool
|
|
54
66
|
"""
|
|
55
|
-
start = context.get(PARAMETERS_KEY, {}).get(START_KEY)
|
|
56
|
-
end = context.get(PARAMETERS_KEY, {}).get(END_KEY)
|
|
67
|
+
start = context.get(PARAMETERS_KEY, {}).get(START_KEY, None)
|
|
68
|
+
end = context.get(PARAMETERS_KEY, {}).get(END_KEY, None)
|
|
69
|
+
recurrence_data = context.get(PARAMETERS_KEY, {}).get(TIME_WINDOW_FILTER_SETTING_RECURRENCE, None)
|
|
70
|
+
recurrence = None
|
|
57
71
|
|
|
58
72
|
current_time = datetime.now(timezone.utc)
|
|
59
73
|
|
|
60
74
|
if not start and not end:
|
|
61
|
-
|
|
75
|
+
logger.warning(
|
|
76
|
+
TIME_WINDOW_FILTER_INVALID,
|
|
77
|
+
TimeWindowFilter.__name__,
|
|
78
|
+
context.get(FEATURE_FLAG_NAME_KEY),
|
|
79
|
+
START_KEY,
|
|
80
|
+
END_KEY,
|
|
81
|
+
)
|
|
62
82
|
return False
|
|
63
83
|
|
|
64
|
-
start_time = parsedate_to_datetime(start) if start else None
|
|
65
|
-
end_time = parsedate_to_datetime(end) if end else None
|
|
84
|
+
start_time: Optional[datetime] = parsedate_to_datetime(start) if start else None
|
|
85
|
+
end_time: Optional[datetime] = parsedate_to_datetime(end) if end else None
|
|
86
|
+
|
|
87
|
+
if (start_time is None or start_time <= current_time) and (end_time is None or current_time < end_time):
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
if recurrence_data:
|
|
91
|
+
recurrence = Recurrence(recurrence_data)
|
|
92
|
+
settings = TimeWindowFilterSettings(start_time, end_time, recurrence)
|
|
93
|
+
return is_match(settings, current_time)
|
|
66
94
|
|
|
67
|
-
return
|
|
95
|
+
return False
|
|
68
96
|
|
|
69
97
|
|
|
70
98
|
@FeatureFilter.alias("Microsoft.Targeting")
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import logging
|
|
7
|
-
from typing import cast, overload, Any, Optional, Dict, Mapping, List
|
|
7
|
+
from typing import cast, overload, Any, Optional, Dict, Mapping, List, Tuple
|
|
8
8
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
9
9
|
from ._featurefilters import FeatureFilter
|
|
10
10
|
from ._models import EvaluationEvent, Variant, TargetingContext
|
|
11
11
|
from ._featuremanagerbase import (
|
|
12
|
-
_get_feature_flag,
|
|
13
12
|
FeatureManagerBase,
|
|
14
13
|
PROVIDED_FEATURE_FILTERS,
|
|
15
|
-
FEATURE_MANAGEMENT_KEY,
|
|
16
14
|
REQUIREMENT_TYPE_ALL,
|
|
17
15
|
FEATURE_FILTER_NAME,
|
|
18
16
|
)
|
|
19
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
class FeatureManager(FeatureManagerBase):
|
|
22
22
|
"""
|
|
@@ -26,6 +26,8 @@ class FeatureManager(FeatureManagerBase):
|
|
|
26
26
|
:keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
|
|
27
27
|
:keyword Callable[EvaluationEvent] on_feature_evaluated: Callback function to be called when a feature flag is
|
|
28
28
|
evaluated.
|
|
29
|
+
:keyword Callable[[], TargetingContext] targeting_context_accessor: Callback function to get the current targeting
|
|
30
|
+
context if one isn't provided.
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
33
|
def __init__(self, configuration: Mapping[str, Any], **kwargs: Any):
|
|
@@ -59,7 +61,7 @@ class FeatureManager(FeatureManagerBase):
|
|
|
59
61
|
:return: True if the feature flag is enabled for the given context.
|
|
60
62
|
:rtype: bool
|
|
61
63
|
"""
|
|
62
|
-
targeting_context = self._build_targeting_context(args)
|
|
64
|
+
targeting_context: TargetingContext = self._build_targeting_context(args)
|
|
63
65
|
|
|
64
66
|
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
65
67
|
if (
|
|
@@ -92,7 +94,7 @@ class FeatureManager(FeatureManagerBase):
|
|
|
92
94
|
:return: Variant instance.
|
|
93
95
|
:rtype: Variant
|
|
94
96
|
"""
|
|
95
|
-
targeting_context = self._build_targeting_context(args)
|
|
97
|
+
targeting_context: TargetingContext = self._build_targeting_context(args)
|
|
96
98
|
|
|
97
99
|
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
98
100
|
if (
|
|
@@ -105,6 +107,21 @@ class FeatureManager(FeatureManagerBase):
|
|
|
105
107
|
self._on_feature_evaluated(result)
|
|
106
108
|
return result.variant
|
|
107
109
|
|
|
110
|
+
def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
|
|
111
|
+
targeting_context = super()._build_targeting_context(args)
|
|
112
|
+
if targeting_context:
|
|
113
|
+
return targeting_context
|
|
114
|
+
if not targeting_context and self._targeting_context_accessor and callable(self._targeting_context_accessor):
|
|
115
|
+
targeting_context = self._targeting_context_accessor()
|
|
116
|
+
if targeting_context and isinstance(targeting_context, TargetingContext):
|
|
117
|
+
return targeting_context
|
|
118
|
+
logger.warning(
|
|
119
|
+
"targeting_context_accessor did not return a TargetingContext. Received type %s.",
|
|
120
|
+
type(targeting_context),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return TargetingContext()
|
|
124
|
+
|
|
108
125
|
def _check_feature_filters(
|
|
109
126
|
self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Any
|
|
110
127
|
) -> None:
|
|
@@ -143,35 +160,13 @@ class FeatureManager(FeatureManagerBase):
|
|
|
143
160
|
Determine if the feature flag is enabled for the given context.
|
|
144
161
|
|
|
145
162
|
:param str feature_flag_id: Name of the feature flag.
|
|
146
|
-
:
|
|
147
|
-
:
|
|
163
|
+
:param TargetingContext targeting_context: Targeting context.
|
|
164
|
+
:return: EvaluationEvent for the given context.
|
|
165
|
+
:rtype: EvaluationEvent
|
|
148
166
|
"""
|
|
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
|
|
167
|
+
evaluation_event, done = super()._check_feature_base(feature_flag_id)
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
evaluation_event.enabled = False
|
|
169
|
+
if done:
|
|
175
170
|
return evaluation_event
|
|
176
171
|
|
|
177
172
|
self._check_feature_filters(evaluation_event, targeting_context, **kwargs)
|
{featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featuremanagerbase.py
RENAMED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import hashlib
|
|
7
|
+
import logging
|
|
7
8
|
from abc import ABC
|
|
8
|
-
from typing import List, Optional, Dict, Tuple, Any, Mapping
|
|
9
|
+
from typing import List, Optional, Dict, Tuple, Any, Mapping, Callable
|
|
9
10
|
from ._models import FeatureFlag, Variant, VariantAssignmentReason, TargetingContext, EvaluationEvent, VariantReference
|
|
10
11
|
|
|
11
12
|
|
|
@@ -20,6 +21,9 @@ REQUIREMENT_TYPE_ANY = "Any"
|
|
|
20
21
|
FEATURE_FILTER_PARAMETERS = "parameters"
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]:
|
|
24
28
|
"""
|
|
25
29
|
Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
|
|
@@ -76,9 +80,12 @@ class FeatureManagerBase(ABC):
|
|
|
76
80
|
self._cache: Dict[str, Optional[FeatureFlag]] = {}
|
|
77
81
|
self._copy = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
78
82
|
self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None)
|
|
83
|
+
self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop(
|
|
84
|
+
"targeting_context_accessor", None
|
|
85
|
+
)
|
|
79
86
|
|
|
80
87
|
@staticmethod
|
|
81
|
-
def
|
|
88
|
+
def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
|
|
82
89
|
"""
|
|
83
90
|
A method called when the feature flag is disabled, to determine what the default variant should be. If there is
|
|
84
91
|
no allocation, then None is set as the value of the variant in the EvaluationEvent.
|
|
@@ -88,7 +95,7 @@ class FeatureManagerBase(ABC):
|
|
|
88
95
|
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
|
|
89
96
|
if not evaluation_event.feature or not evaluation_event.feature.allocation:
|
|
90
97
|
return
|
|
91
|
-
FeatureManagerBase.
|
|
98
|
+
FeatureManagerBase._assign_variant_override(
|
|
92
99
|
evaluation_event.feature.variants,
|
|
93
100
|
evaluation_event.feature.allocation.default_when_disabled,
|
|
94
101
|
False,
|
|
@@ -96,7 +103,7 @@ class FeatureManagerBase(ABC):
|
|
|
96
103
|
)
|
|
97
104
|
|
|
98
105
|
@staticmethod
|
|
99
|
-
def
|
|
106
|
+
def _assign_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
|
|
100
107
|
"""
|
|
101
108
|
A method called when the feature flag is enabled, to determine what the default variant should be. If there is
|
|
102
109
|
no allocation, then None is set as the value of the variant in the EvaluationEvent.
|
|
@@ -106,7 +113,7 @@ class FeatureManagerBase(ABC):
|
|
|
106
113
|
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
|
|
107
114
|
if not evaluation_event.feature or not evaluation_event.feature.allocation:
|
|
108
115
|
return
|
|
109
|
-
FeatureManagerBase.
|
|
116
|
+
FeatureManagerBase._assign_variant_override(
|
|
110
117
|
evaluation_event.feature.variants,
|
|
111
118
|
evaluation_event.feature.allocation.default_when_enabled,
|
|
112
119
|
True,
|
|
@@ -114,7 +121,7 @@ class FeatureManagerBase(ABC):
|
|
|
114
121
|
)
|
|
115
122
|
|
|
116
123
|
@staticmethod
|
|
117
|
-
def
|
|
124
|
+
def _assign_variant_override(
|
|
118
125
|
variants: Optional[List[VariantReference]],
|
|
119
126
|
default_variant_name: Optional[str],
|
|
120
127
|
status: bool,
|
|
@@ -149,56 +156,57 @@ class FeatureManagerBase(ABC):
|
|
|
149
156
|
|
|
150
157
|
return (context_marker / (2**32 - 1)) * 100
|
|
151
158
|
|
|
152
|
-
def _assign_variant(
|
|
153
|
-
self, feature_flag: FeatureFlag, targeting_context: TargetingContext, evaluation_event: EvaluationEvent
|
|
154
|
-
) -> None:
|
|
159
|
+
def _assign_variant(self, targeting_context: TargetingContext, evaluation_event: EvaluationEvent) -> None:
|
|
155
160
|
"""
|
|
156
161
|
Assign a variant to the user based on the allocation.
|
|
157
162
|
|
|
158
|
-
:param FeatureFlag feature_flag: Feature flag object.
|
|
159
163
|
:param TargetingContext targeting_context: Targeting context.
|
|
160
164
|
:param EvaluationEvent evaluation_event: Evaluation event object.
|
|
161
165
|
"""
|
|
162
|
-
|
|
166
|
+
feature_flag = evaluation_event.feature
|
|
163
167
|
variant_name = None
|
|
164
|
-
if not
|
|
168
|
+
if not feature_flag or not feature_flag.variants or not feature_flag.allocation:
|
|
165
169
|
return
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
|
|
171
|
+
allocation = feature_flag.allocation
|
|
172
|
+
groups = targeting_context.groups
|
|
173
|
+
|
|
174
|
+
if allocation.user and targeting_context.user_id:
|
|
175
|
+
for user_allocation in allocation.user:
|
|
168
176
|
if targeting_context.user_id in user_allocation.users:
|
|
169
177
|
evaluation_event.reason = VariantAssignmentReason.USER
|
|
170
178
|
variant_name = user_allocation.variant
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
context_id = targeting_context.user_id + "\n" + seed
|
|
179
|
+
|
|
180
|
+
if not variant_name and allocation.group and groups:
|
|
181
|
+
for group_allocation in allocation.group:
|
|
182
|
+
if any(group in group_allocation.groups for group in groups):
|
|
183
|
+
evaluation_event.reason = VariantAssignmentReason.GROUP
|
|
184
|
+
variant_name = group_allocation.variant
|
|
185
|
+
|
|
186
|
+
if not variant_name and allocation.percentile:
|
|
187
|
+
seed = allocation.seed or f"allocation\n{feature_flag.name}"
|
|
188
|
+
context_id = f"{targeting_context.user_id}\n{seed}"
|
|
182
189
|
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:
|
|
190
|
+
for percentile_allocation in allocation.percentile:
|
|
191
|
+
percentile_to: int = percentile_allocation.percentile_to
|
|
192
|
+
if (box == 100 and percentile_to == 100) or (
|
|
193
|
+
percentile_allocation.percentile_from <= box < percentile_to
|
|
194
|
+
):
|
|
189
195
|
evaluation_event.reason = VariantAssignmentReason.PERCENTILE
|
|
190
196
|
variant_name = percentile_allocation.variant
|
|
197
|
+
break
|
|
198
|
+
|
|
191
199
|
if not variant_name:
|
|
192
|
-
FeatureManagerBase.
|
|
200
|
+
FeatureManagerBase._assign_default_enabled_variant(evaluation_event)
|
|
193
201
|
if feature_flag.allocation:
|
|
194
202
|
evaluation_event.variant = self._variant_name_to_variant(
|
|
195
203
|
feature_flag, feature_flag.allocation.default_when_enabled
|
|
196
204
|
)
|
|
197
205
|
return
|
|
206
|
+
|
|
198
207
|
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)
|
|
208
|
+
if feature_flag.variants:
|
|
209
|
+
FeatureManagerBase._assign_variant_override(feature_flag.variants, variant_name, True, evaluation_event)
|
|
202
210
|
|
|
203
211
|
def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Optional[str]) -> Optional[Variant]:
|
|
204
212
|
"""
|
|
@@ -208,16 +216,15 @@ class FeatureManagerBase(ABC):
|
|
|
208
216
|
:param str variant_name: Name of the variant.
|
|
209
217
|
:return: Variant object.
|
|
210
218
|
"""
|
|
211
|
-
if not feature_flag.variants:
|
|
212
|
-
return None
|
|
213
|
-
if not variant_name:
|
|
219
|
+
if not feature_flag.variants or not variant_name:
|
|
214
220
|
return None
|
|
221
|
+
|
|
215
222
|
for variant_reference in feature_flag.variants:
|
|
216
223
|
if variant_reference.name == variant_name:
|
|
217
224
|
return Variant(variant_reference.name, variant_reference.configuration_value)
|
|
218
225
|
return None
|
|
219
226
|
|
|
220
|
-
def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
|
|
227
|
+
def _build_targeting_context(self, args: Tuple[Any]) -> Optional[TargetingContext]:
|
|
221
228
|
"""
|
|
222
229
|
Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
|
|
223
230
|
returns an empty context.
|
|
@@ -225,31 +232,68 @@ class FeatureManagerBase(ABC):
|
|
|
225
232
|
:param args: Arguments to build the TargetingContext.
|
|
226
233
|
:return: TargetingContext
|
|
227
234
|
"""
|
|
228
|
-
if len(args) == 1
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
if len(args) == 1:
|
|
236
|
+
arg = args[0]
|
|
237
|
+
if isinstance(arg, str):
|
|
238
|
+
# If the user_id is provided, return a TargetingContext with the user_id
|
|
239
|
+
return TargetingContext(user_id=arg, groups=[])
|
|
240
|
+
if isinstance(arg, TargetingContext):
|
|
241
|
+
# If a TargetingContext is provided, return it
|
|
242
|
+
return arg
|
|
243
|
+
return None
|
|
233
244
|
|
|
234
245
|
def _assign_allocation(self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext) -> None:
|
|
235
246
|
feature_flag = evaluation_event.feature
|
|
236
247
|
if not feature_flag:
|
|
237
248
|
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
249
|
|
|
252
|
-
|
|
250
|
+
if not feature_flag.variants or not feature_flag.allocation:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if not evaluation_event.enabled:
|
|
254
|
+
FeatureManagerBase._assign_default_disabled_variant(evaluation_event)
|
|
255
|
+
evaluation_event.variant = self._variant_name_to_variant(
|
|
256
|
+
feature_flag, feature_flag.allocation.default_when_disabled
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
self._assign_variant(targeting_context, evaluation_event)
|
|
261
|
+
|
|
262
|
+
def _check_feature_base(self, feature_flag_id: str) -> Tuple[EvaluationEvent, bool]:
|
|
263
|
+
"""
|
|
264
|
+
Determine if the feature flag is enabled for the given context.
|
|
265
|
+
|
|
266
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
267
|
+
:return: The evaluation event and if the feature filters need to be checked.
|
|
268
|
+
:rtype: evaluation_event, bool
|
|
269
|
+
"""
|
|
270
|
+
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
|
|
271
|
+
self._cache = {}
|
|
272
|
+
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
273
|
+
|
|
274
|
+
if not self._cache.get(feature_flag_id):
|
|
275
|
+
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
276
|
+
self._cache[feature_flag_id] = feature_flag
|
|
277
|
+
else:
|
|
278
|
+
feature_flag = self._cache.get(feature_flag_id)
|
|
279
|
+
|
|
280
|
+
evaluation_event = EvaluationEvent(feature_flag)
|
|
281
|
+
if not feature_flag:
|
|
282
|
+
logger.warning("Feature flag %s not found", feature_flag_id)
|
|
283
|
+
# Unknown feature flags are disabled by default
|
|
284
|
+
return evaluation_event, True
|
|
285
|
+
|
|
286
|
+
if not feature_flag.enabled:
|
|
287
|
+
# Feature flags that are disabled are always disabled
|
|
288
|
+
self._assign_default_disabled_variant(evaluation_event)
|
|
289
|
+
if feature_flag.allocation:
|
|
290
|
+
variant_name = feature_flag.allocation.default_when_disabled
|
|
291
|
+
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
|
|
292
|
+
|
|
293
|
+
# If a feature flag is disabled and override can't enable it
|
|
294
|
+
evaluation_event.enabled = False
|
|
295
|
+
return evaluation_event, True
|
|
296
|
+
return evaluation_event, False
|
|
253
297
|
|
|
254
298
|
def list_feature_flag_names(self) -> List[str]:
|
|
255
299
|
"""
|
{featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/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
|
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
from ._recurrence_evaluator import is_match
|
|
7
|
+
from ._models import Recurrence, TimeWindowFilterSettings
|
|
8
|
+
|
|
9
|
+
__all__ = ["is_match", "Recurrence", "TimeWindowFilterSettings"]
|