FeatureManagement 1.0.0__tar.gz → 2.0.0b1__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-1.0.0 → featuremanagement-2.0.0b1}/CHANGELOG.md +5 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1/FeatureManagement.egg-info}/PKG-INFO +4 -4
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/SOURCES.txt +13 -0
- featuremanagement-2.0.0b1/FeatureManagement.egg-info/requires.txt +3 -0
- {featuremanagement-1.0.0/FeatureManagement.egg-info → featuremanagement-2.0.0b1}/PKG-INFO +4 -4
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/README.md +1 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/__init__.py +5 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_defaultfilters.py +14 -10
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_featurefilters.py +9 -6
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_featuremanager.py +88 -98
- featuremanagement-2.0.0b1/featuremanagement/_featuremanagerbase.py +255 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/__init__.py +12 -2
- featuremanagement-2.0.0b1/featuremanagement/_models/_allocation.py +207 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_constants.py +15 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_evaluation_event.py +8 -4
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_feature_conditions.py +7 -6
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_feature_flag.py +62 -14
- featuremanagement-2.0.0b1/featuremanagement/_models/_telemetry.py +17 -0
- featuremanagement-2.0.0b1/featuremanagement/_models/_variant.py +35 -0
- featuremanagement-2.0.0b1/featuremanagement/_models/_variant_assignment_reason.py +19 -0
- featuremanagement-2.0.0b1/featuremanagement/_models/_variant_reference.py +65 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_version.py +1 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/_defaultfilters.py +5 -5
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/_featurefilters.py +9 -6
- featuremanagement-2.0.0b1/featuremanagement/aio/_featuremanager.py +178 -0
- featuremanagement-2.0.0b1/featuremanagement/azuremonitor/__init__.py +12 -0
- featuremanagement-2.0.0b1/featuremanagement/azuremonitor/_send_telemetry.py +68 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/pyproject.toml +3 -3
- featuremanagement-2.0.0b1/samples/feature_variant_sample.py +25 -0
- featuremanagement-2.0.0b1/samples/feature_variant_sample_with_telemetry.py +36 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/formatted_feature_flags.json +0 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/setup.py +1 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_default_feature_flags.py +1 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_default_feature_flags_async.py +1 -1
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager_refresh.py +1 -1
- featuremanagement-2.0.0b1/tests/test_feature_variants.py +251 -0
- featuremanagement-2.0.0b1/tests/test_feature_variants_async.py +257 -0
- featuremanagement-2.0.0b1/tests/test_send_telemetry_appinsights.py +78 -0
- featuremanagement-1.0.0/FeatureManagement.egg-info/requires.txt +0 -4
- featuremanagement-1.0.0/featuremanagement/aio/_featuremanager.py +0 -142
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/CODE_OF_CONDUCT.md +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/dependency_links.txt +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/not-zip-safe +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/top_level.txt +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/LICENSE +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/MANIFEST.in +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/SECURITY.md +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/SUPPORT.md +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_targeting_context.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/__init__.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/py.typed +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/feature_flag_sample.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/random_filter.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/setup.cfg +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager_async.py +0 -0
- {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/validation_tests/test_json_validations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b1
|
|
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
|
|
@@ -41,9 +41,8 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
41
41
|
Requires-Python: >=3.6
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
|
-
Provides-Extra:
|
|
45
|
-
Requires-Dist: azure-monitor-
|
|
46
|
-
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "appinsights"
|
|
44
|
+
Provides-Extra: azuremonitor
|
|
45
|
+
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
|
|
47
46
|
|
|
48
47
|
# Microsoft Feature Management for Python
|
|
49
48
|
|
|
@@ -60,6 +59,7 @@ Feature management provides a way to develop and expose application functionalit
|
|
|
60
59
|
## Examples
|
|
61
60
|
|
|
62
61
|
* [Python Application](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_sample.py)
|
|
62
|
+
* [Python Application with Feature Variants](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_variant_sample.py)
|
|
63
63
|
* [Python Application with Azure App Configuration](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_with_azure_app_configuration_sample.py)
|
|
64
64
|
* [Django Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-django-webapp-sample)
|
|
65
65
|
* [Flask Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-flask-webapp-sample)
|
{featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/SOURCES.txt
RENAMED
|
@@ -17,20 +17,30 @@ featuremanagement/__init__.py
|
|
|
17
17
|
featuremanagement/_defaultfilters.py
|
|
18
18
|
featuremanagement/_featurefilters.py
|
|
19
19
|
featuremanagement/_featuremanager.py
|
|
20
|
+
featuremanagement/_featuremanagerbase.py
|
|
20
21
|
featuremanagement/_version.py
|
|
21
22
|
featuremanagement/py.typed
|
|
22
23
|
featuremanagement/_models/__init__.py
|
|
24
|
+
featuremanagement/_models/_allocation.py
|
|
23
25
|
featuremanagement/_models/_constants.py
|
|
24
26
|
featuremanagement/_models/_evaluation_event.py
|
|
25
27
|
featuremanagement/_models/_feature_conditions.py
|
|
26
28
|
featuremanagement/_models/_feature_flag.py
|
|
27
29
|
featuremanagement/_models/_targeting_context.py
|
|
30
|
+
featuremanagement/_models/_telemetry.py
|
|
31
|
+
featuremanagement/_models/_variant.py
|
|
32
|
+
featuremanagement/_models/_variant_assignment_reason.py
|
|
33
|
+
featuremanagement/_models/_variant_reference.py
|
|
28
34
|
featuremanagement/aio/__init__.py
|
|
29
35
|
featuremanagement/aio/_defaultfilters.py
|
|
30
36
|
featuremanagement/aio/_featurefilters.py
|
|
31
37
|
featuremanagement/aio/_featuremanager.py
|
|
38
|
+
featuremanagement/azuremonitor/__init__.py
|
|
39
|
+
featuremanagement/azuremonitor/_send_telemetry.py
|
|
32
40
|
samples/feature_flag_sample.py
|
|
33
41
|
samples/feature_flag_with_azure_app_configuration_sample.py
|
|
42
|
+
samples/feature_variant_sample.py
|
|
43
|
+
samples/feature_variant_sample_with_telemetry.py
|
|
34
44
|
samples/formatted_feature_flags.json
|
|
35
45
|
samples/random_filter.py
|
|
36
46
|
tests/test_default_feature_flags.py
|
|
@@ -38,4 +48,7 @@ tests/test_default_feature_flags_async.py
|
|
|
38
48
|
tests/test_feature_manager.py
|
|
39
49
|
tests/test_feature_manager_async.py
|
|
40
50
|
tests/test_feature_manager_refresh.py
|
|
51
|
+
tests/test_feature_variants.py
|
|
52
|
+
tests/test_feature_variants_async.py
|
|
53
|
+
tests/test_send_telemetry_appinsights.py
|
|
41
54
|
tests/validation_tests/test_json_validations.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b1
|
|
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
|
|
@@ -41,9 +41,8 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
41
41
|
Requires-Python: >=3.6
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
|
-
Provides-Extra:
|
|
45
|
-
Requires-Dist: azure-monitor-
|
|
46
|
-
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "appinsights"
|
|
44
|
+
Provides-Extra: azuremonitor
|
|
45
|
+
Requires-Dist: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
|
|
47
46
|
|
|
48
47
|
# Microsoft Feature Management for Python
|
|
49
48
|
|
|
@@ -60,6 +59,7 @@ Feature management provides a way to develop and expose application functionalit
|
|
|
60
59
|
## Examples
|
|
61
60
|
|
|
62
61
|
* [Python Application](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_sample.py)
|
|
62
|
+
* [Python Application with Feature Variants](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_variant_sample.py)
|
|
63
63
|
* [Python Application with Azure App Configuration](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_with_azure_app_configuration_sample.py)
|
|
64
64
|
* [Django Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-django-webapp-sample)
|
|
65
65
|
* [Flask Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-flask-webapp-sample)
|
|
@@ -13,6 +13,7 @@ Feature management provides a way to develop and expose application functionalit
|
|
|
13
13
|
## Examples
|
|
14
14
|
|
|
15
15
|
* [Python Application](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_sample.py)
|
|
16
|
+
* [Python Application with Feature Variants](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_variant_sample.py)
|
|
16
17
|
* [Python Application with Azure App Configuration](https://github.com/microsoft/FeatureManagement-Python/blob/main/samples/feature_flag_with_azure_app_configuration_sample.py)
|
|
17
18
|
* [Django Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-django-webapp-sample)
|
|
18
19
|
* [Flask Application](https://github.com/Azure/AppConfiguration/tree/main/examples/Python/python-flask-webapp-sample)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
from ._featuremanager import FeatureManager
|
|
7
7
|
from ._featurefilters import FeatureFilter
|
|
8
8
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
9
|
-
from ._models import TargetingContext
|
|
9
|
+
from ._models import FeatureFlag, Variant, EvaluationEvent, VariantAssignmentReason, TargetingContext
|
|
10
10
|
|
|
11
11
|
from ._version import VERSION
|
|
12
12
|
|
|
@@ -16,5 +16,9 @@ __all__ = [
|
|
|
16
16
|
"TimeWindowFilter",
|
|
17
17
|
"TargetingFilter",
|
|
18
18
|
"FeatureFilter",
|
|
19
|
+
"FeatureFlag",
|
|
20
|
+
"Variant",
|
|
21
|
+
"EvaluationEvent",
|
|
22
|
+
"VariantAssignmentReason",
|
|
19
23
|
"TargetingContext",
|
|
20
24
|
]
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import logging
|
|
7
7
|
import hashlib
|
|
8
|
-
|
|
9
8
|
from datetime import datetime, timezone
|
|
10
9
|
from email.utils import parsedate_to_datetime
|
|
11
|
-
|
|
10
|
+
from typing import cast, List, Mapping, Optional, Dict, Any
|
|
12
11
|
from ._featurefilters import FeatureFilter
|
|
13
12
|
|
|
14
13
|
FEATURE_FLAG_NAME_KEY = "feature_name"
|
|
@@ -45,7 +44,7 @@ class TimeWindowFilter(FeatureFilter):
|
|
|
45
44
|
Feature Filter that determines if the current time is within the time window.
|
|
46
45
|
"""
|
|
47
46
|
|
|
48
|
-
def evaluate(self, context, **kwargs):
|
|
47
|
+
def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool:
|
|
49
48
|
"""
|
|
50
49
|
Determine if the feature flag is enabled for the given context.
|
|
51
50
|
|
|
@@ -75,7 +74,7 @@ class TargetingFilter(FeatureFilter):
|
|
|
75
74
|
"""
|
|
76
75
|
|
|
77
76
|
@staticmethod
|
|
78
|
-
def _is_targeted(context_id, rollout_percentage):
|
|
77
|
+
def _is_targeted(context_id: str, rollout_percentage: int) -> bool:
|
|
79
78
|
"""Determine if the user is targeted for the given context"""
|
|
80
79
|
# Always return true if rollout percentage is 100
|
|
81
80
|
if rollout_percentage == 100:
|
|
@@ -87,7 +86,9 @@ class TargetingFilter(FeatureFilter):
|
|
|
87
86
|
percentage = (context_marker / (2**32 - 1)) * 100
|
|
88
87
|
return percentage < rollout_percentage
|
|
89
88
|
|
|
90
|
-
def _target_group(
|
|
89
|
+
def _target_group(
|
|
90
|
+
self, target_user: Optional[str], target_group: str, group: Mapping[str, Any], feature_flag_name: str
|
|
91
|
+
) -> bool:
|
|
91
92
|
group_rollout_percentage = group.get(ROLLOUT_PERCENTAGE_KEY, 0)
|
|
92
93
|
if not target_user:
|
|
93
94
|
target_user = ""
|
|
@@ -95,7 +96,7 @@ class TargetingFilter(FeatureFilter):
|
|
|
95
96
|
|
|
96
97
|
return self._is_targeted(audience_context_id, group_rollout_percentage)
|
|
97
98
|
|
|
98
|
-
def evaluate(self, context, **kwargs):
|
|
99
|
+
def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool:
|
|
99
100
|
"""
|
|
100
101
|
Determine if the feature flag is enabled for the given context.
|
|
101
102
|
|
|
@@ -103,8 +104,11 @@ class TargetingFilter(FeatureFilter):
|
|
|
103
104
|
:return: True if the user is targeted for the feature flag.
|
|
104
105
|
:rtype: bool
|
|
105
106
|
"""
|
|
106
|
-
target_user =
|
|
107
|
-
|
|
107
|
+
target_user: Optional[str] = cast(
|
|
108
|
+
str,
|
|
109
|
+
kwargs.get(TARGETED_USER_KEY, None),
|
|
110
|
+
)
|
|
111
|
+
target_groups: List[str] = cast(List[str], kwargs.get(TARGETED_GROUPS_KEY, []))
|
|
108
112
|
|
|
109
113
|
if not target_user and not (target_groups and len(target_groups) > 0):
|
|
110
114
|
logging.warning("%s: Name or Groups are required parameters", TargetingFilter.__name__)
|
|
@@ -152,11 +156,11 @@ class TargetingFilter(FeatureFilter):
|
|
|
152
156
|
return self._is_targeted(context_id, default_rollout_percentage)
|
|
153
157
|
|
|
154
158
|
@staticmethod
|
|
155
|
-
def _validate(groups, default_rollout_percentage):
|
|
159
|
+
def _validate(groups: List[Dict[str, Any]], default_rollout_percentage: int) -> None:
|
|
156
160
|
# Validate the audience settings
|
|
157
161
|
if default_rollout_percentage < 0 or default_rollout_percentage > 100:
|
|
158
162
|
raise TargetingException("DefaultRolloutPercentage must be between 0 and 100")
|
|
159
163
|
|
|
160
164
|
for group in groups:
|
|
161
|
-
if group.get(ROLLOUT_PERCENTAGE_KEY) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY) > 100:
|
|
165
|
+
if group.get(ROLLOUT_PERCENTAGE_KEY, 0) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY, 100) > 100:
|
|
162
166
|
raise TargetingException("RolloutPercentage must be between 0 and 100")
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Mapping, Callable, Any, Optional
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class FeatureFilter(ABC):
|
|
@@ -11,8 +12,10 @@ class FeatureFilter(ABC):
|
|
|
11
12
|
Parent class for all feature filters.
|
|
12
13
|
"""
|
|
13
14
|
|
|
15
|
+
_alias: Optional[str] = None
|
|
16
|
+
|
|
14
17
|
@abstractmethod
|
|
15
|
-
def evaluate(self, context, **kwargs):
|
|
18
|
+
def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool:
|
|
16
19
|
"""
|
|
17
20
|
Determine if the feature flag is enabled for the given context.
|
|
18
21
|
|
|
@@ -20,28 +23,28 @@ class FeatureFilter(ABC):
|
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
25
|
@property
|
|
23
|
-
def name(self):
|
|
26
|
+
def name(self) -> str:
|
|
24
27
|
"""
|
|
25
28
|
Get the name of the filter.
|
|
26
29
|
|
|
27
30
|
:return: Name of the filter, or alias if it exists.
|
|
28
31
|
:rtype: str
|
|
29
32
|
"""
|
|
30
|
-
if hasattr(self, "_alias"):
|
|
33
|
+
if hasattr(self, "_alias") and self._alias:
|
|
31
34
|
return self._alias
|
|
32
35
|
return self.__class__.__name__
|
|
33
36
|
|
|
34
37
|
@staticmethod
|
|
35
|
-
def alias(alias):
|
|
38
|
+
def alias(alias: str) -> Callable[..., Any]:
|
|
36
39
|
"""
|
|
37
40
|
Decorator to set the alias for the filter.
|
|
38
41
|
|
|
39
42
|
:param str alias: Alias for the filter.
|
|
40
43
|
:return: Decorator.
|
|
41
|
-
:rtype:
|
|
44
|
+
:rtype: Callable
|
|
42
45
|
"""
|
|
43
46
|
|
|
44
|
-
def wrapper(cls):
|
|
47
|
+
def wrapper(cls: "FeatureFilter") -> Any:
|
|
45
48
|
cls._alias = alias # pylint: disable=protected-access
|
|
46
49
|
return cls
|
|
47
50
|
|
|
@@ -4,106 +4,44 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
6
|
import logging
|
|
7
|
-
from
|
|
8
|
-
from typing import overload
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
FEATURE_FILTER_NAME
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _get_feature_flag(configuration, feature_flag_name):
|
|
26
|
-
"""
|
|
27
|
-
Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
|
|
28
|
-
|
|
29
|
-
:param Mapping configuration: Configuration object.
|
|
30
|
-
:param str feature_flag_name: Name of the feature flag.
|
|
31
|
-
:return: FeatureFlag
|
|
32
|
-
:rtype: FeatureFlag
|
|
33
|
-
"""
|
|
34
|
-
feature_management = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
35
|
-
if not feature_management or not isinstance(feature_management, Mapping):
|
|
36
|
-
return None
|
|
37
|
-
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
|
|
38
|
-
if not feature_flags or not isinstance(feature_flags, list):
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
for feature_flag in feature_flags:
|
|
42
|
-
if feature_flag.get("id") == feature_flag_name:
|
|
43
|
-
return FeatureFlag.convert_from_json(feature_flag)
|
|
44
|
-
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _list_feature_flag_names(configuration):
|
|
49
|
-
"""
|
|
50
|
-
List of all feature flag names.
|
|
51
|
-
|
|
52
|
-
:param Mapping configuration: Configuration object.
|
|
53
|
-
:return: List of feature flag names.
|
|
54
|
-
"""
|
|
55
|
-
feature_flag_names = []
|
|
56
|
-
feature_management = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
57
|
-
if not feature_management or not isinstance(feature_management, Mapping):
|
|
58
|
-
return []
|
|
59
|
-
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
|
|
60
|
-
if not feature_flags or not isinstance(feature_flags, list):
|
|
61
|
-
return []
|
|
62
|
-
|
|
63
|
-
for feature_flag in feature_flags:
|
|
64
|
-
feature_flag_names.append(feature_flag.get("id"))
|
|
65
|
-
|
|
66
|
-
return feature_flag_names
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class FeatureManager:
|
|
10
|
+
from ._models import EvaluationEvent, Variant, TargetingContext
|
|
11
|
+
from ._featuremanagerbase import (
|
|
12
|
+
_get_feature_flag,
|
|
13
|
+
FeatureManagerBase,
|
|
14
|
+
PROVIDED_FEATURE_FILTERS,
|
|
15
|
+
FEATURE_MANAGEMENT_KEY,
|
|
16
|
+
REQUIREMENT_TYPE_ALL,
|
|
17
|
+
FEATURE_FILTER_NAME,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FeatureManager(FeatureManagerBase):
|
|
70
22
|
"""
|
|
71
23
|
Feature Manager that determines if a feature flag is enabled for the given context.
|
|
72
24
|
|
|
73
25
|
:param Mapping configuration: Configuration object.
|
|
74
26
|
:keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
|
|
27
|
+
:keyword Callable[EvaluationEvent] on_feature_evaluated: Callback function to be called when a feature flag is
|
|
28
|
+
evaluated.
|
|
75
29
|
"""
|
|
76
30
|
|
|
77
|
-
def __init__(self, configuration, **kwargs):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self._copy = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
84
|
-
filters = [TimeWindowFilter(), TargetingFilter()] + kwargs.pop(PROVIDED_FEATURE_FILTERS, [])
|
|
31
|
+
def __init__(self, configuration: Mapping[str, Any], **kwargs: Any):
|
|
32
|
+
super().__init__(configuration, **kwargs)
|
|
33
|
+
self._filters: Dict[str, FeatureFilter] = {}
|
|
34
|
+
filters = [TimeWindowFilter(), TargetingFilter()] + cast(
|
|
35
|
+
List[FeatureFilter], kwargs.pop(PROVIDED_FEATURE_FILTERS, [])
|
|
36
|
+
)
|
|
85
37
|
|
|
86
38
|
for feature_filter in filters:
|
|
87
39
|
if not isinstance(feature_filter, FeatureFilter):
|
|
88
40
|
raise ValueError("Custom filter must be a subclass of FeatureFilter")
|
|
89
41
|
self._filters[feature_filter.name] = feature_filter
|
|
90
42
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
|
|
94
|
-
returns an empty context.
|
|
95
|
-
|
|
96
|
-
:param args: Arguments to build the TargetingContext.
|
|
97
|
-
:return: TargetingContext
|
|
98
|
-
"""
|
|
99
|
-
if len(args) == 1 and isinstance(args[0], str):
|
|
100
|
-
return TargetingContext(user_id=args[0], groups=[])
|
|
101
|
-
if len(args) == 1 and isinstance(args[0], TargetingContext):
|
|
102
|
-
return args[0]
|
|
103
|
-
return TargetingContext()
|
|
104
|
-
|
|
105
|
-
@overload
|
|
106
|
-
def is_enabled(self, feature_flag_id, user_id, **kwargs):
|
|
43
|
+
@overload # type: ignore
|
|
44
|
+
def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> bool:
|
|
107
45
|
"""
|
|
108
46
|
Determine if the feature flag is enabled for the given context.
|
|
109
47
|
|
|
@@ -113,7 +51,7 @@ class FeatureManager:
|
|
|
113
51
|
:rtype: bool
|
|
114
52
|
"""
|
|
115
53
|
|
|
116
|
-
def is_enabled(self, feature_flag_id, *args, **kwargs):
|
|
54
|
+
def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> bool:
|
|
117
55
|
"""
|
|
118
56
|
Determine if the feature flag is enabled for the given context.
|
|
119
57
|
|
|
@@ -124,12 +62,57 @@ class FeatureManager:
|
|
|
124
62
|
targeting_context = self._build_targeting_context(args)
|
|
125
63
|
|
|
126
64
|
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
65
|
+
if (
|
|
66
|
+
self._on_feature_evaluated
|
|
67
|
+
and result.feature
|
|
68
|
+
and result.feature.telemetry.enabled
|
|
69
|
+
and callable(self._on_feature_evaluated)
|
|
70
|
+
):
|
|
71
|
+
result.user = targeting_context.user_id
|
|
72
|
+
self._on_feature_evaluated(result)
|
|
127
73
|
return result.enabled
|
|
128
74
|
|
|
129
|
-
|
|
75
|
+
@overload # type: ignore
|
|
76
|
+
def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> Optional[Variant]:
|
|
77
|
+
"""
|
|
78
|
+
Determine the variant for the given context.
|
|
79
|
+
|
|
80
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
81
|
+
:param str user_id: User identifier.
|
|
82
|
+
:return: return: Variant instance.
|
|
83
|
+
:rtype: Variant
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def get_variant(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> Optional[Variant]:
|
|
87
|
+
"""
|
|
88
|
+
Determine the variant for the given context.
|
|
89
|
+
|
|
90
|
+
:param str feature_flag_id: Name of the feature flag
|
|
91
|
+
:keyword TargetingContext targeting_context: Targeting context.
|
|
92
|
+
:return: Variant instance.
|
|
93
|
+
:rtype: Variant
|
|
94
|
+
"""
|
|
95
|
+
targeting_context = self._build_targeting_context(args)
|
|
96
|
+
|
|
97
|
+
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
98
|
+
if (
|
|
99
|
+
self._on_feature_evaluated
|
|
100
|
+
and result.feature
|
|
101
|
+
and result.feature.telemetry.enabled
|
|
102
|
+
and callable(self._on_feature_evaluated)
|
|
103
|
+
):
|
|
104
|
+
result.user = targeting_context.user_id
|
|
105
|
+
self._on_feature_evaluated(result)
|
|
106
|
+
return result.variant
|
|
107
|
+
|
|
108
|
+
def _check_feature_filters(
|
|
109
|
+
self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Any
|
|
110
|
+
) -> None:
|
|
111
|
+
feature_flag = evaluation_event.feature
|
|
112
|
+
if not feature_flag:
|
|
113
|
+
return
|
|
130
114
|
feature_conditions = feature_flag.conditions
|
|
131
115
|
feature_filters = feature_conditions.client_filters
|
|
132
|
-
evaluation_event = EvaluationEvent(enabled=False)
|
|
133
116
|
|
|
134
117
|
if len(feature_filters) == 0:
|
|
135
118
|
# Feature flags without any filters return evaluate
|
|
@@ -152,9 +135,10 @@ class FeatureManager:
|
|
|
152
135
|
elif self._filters[filter_name].evaluate(feature_filter, **kwargs):
|
|
153
136
|
evaluation_event.enabled = True
|
|
154
137
|
break
|
|
155
|
-
return evaluation_event
|
|
156
138
|
|
|
157
|
-
def _check_feature(
|
|
139
|
+
def _check_feature(
|
|
140
|
+
self, feature_flag_id: str, targeting_context: TargetingContext, **kwargs: Any
|
|
141
|
+
) -> EvaluationEvent:
|
|
158
142
|
"""
|
|
159
143
|
Determine if the feature flag is enabled for the given context.
|
|
160
144
|
|
|
@@ -172,19 +156,25 @@ class FeatureManager:
|
|
|
172
156
|
else:
|
|
173
157
|
feature_flag = self._cache.get(feature_flag_id)
|
|
174
158
|
|
|
159
|
+
evaluation_event = EvaluationEvent(feature_flag)
|
|
175
160
|
if not feature_flag:
|
|
176
161
|
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
177
162
|
# Unknown feature flags are disabled by default
|
|
178
|
-
return
|
|
163
|
+
return evaluation_event
|
|
179
164
|
|
|
180
165
|
if not feature_flag.enabled:
|
|
181
166
|
# Feature flags that are disabled are always disabled
|
|
182
|
-
|
|
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
|
|
183
172
|
|
|
184
|
-
|
|
173
|
+
# If a feature flag is disabled and override can't enable it
|
|
174
|
+
evaluation_event.enabled = False
|
|
175
|
+
return evaluation_event
|
|
185
176
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return _list_feature_flag_names(self._configuration)
|
|
177
|
+
self._check_feature_filters(evaluation_event, targeting_context, **kwargs)
|
|
178
|
+
|
|
179
|
+
self._assign_allocation(evaluation_event, targeting_context)
|
|
180
|
+
return evaluation_event
|