FeatureManagement 2.0.0b3__tar.gz → 2.2.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.0.0b3 → featuremanagement-2.2.0/FeatureManagement.egg-info}/PKG-INFO +23 -8
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/SOURCES.txt +5 -15
- featuremanagement-2.2.0/FeatureManagement.egg-info/requires.txt +20 -0
- {featuremanagement-2.0.0b3/FeatureManagement.egg-info → featuremanagement-2.2.0}/PKG-INFO +23 -8
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/__init__.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_defaultfilters.py +36 -6
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featurefilters.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featuremanager.py +25 -3
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featuremanagerbase.py +51 -49
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/__init__.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_allocation.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_constants.py +1 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_evaluation_event.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_feature_conditions.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_feature_flag.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_targeting_context.py +1 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_telemetry.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant_assignment_reason.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant_reference.py +2 -0
- featuremanagement-2.2.0/featuremanagement/_time_window_filter/__init__.py +11 -0
- featuremanagement-2.2.0/featuremanagement/_time_window_filter/_models.py +151 -0
- featuremanagement-2.2.0/featuremanagement/_time_window_filter/_recurrence_evaluator.py +127 -0
- featuremanagement-2.2.0/featuremanagement/_time_window_filter/_recurrence_validator.py +165 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_version.py +2 -1
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/__init__.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_defaultfilters.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_featurefilters.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_featuremanager.py +29 -3
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/azuremonitor/__init__.py +3 -1
- featuremanagement-2.2.0/featuremanagement/azuremonitor/_send_telemetry.py +179 -0
- featuremanagement-2.2.0/pyproject.toml +78 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_default_feature_flags.py +51 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_default_feature_flags_async.py +52 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager.py +32 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager_async.py +34 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager_refresh.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_variants.py +2 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_variants_async.py +2 -0
- featuremanagement-2.2.0/tests/test_send_telemetry_appinsights.py +298 -0
- featuremanagement-2.0.0b3/CHANGELOG.md +0 -32
- featuremanagement-2.0.0b3/CODE_OF_CONDUCT.md +0 -9
- featuremanagement-2.0.0b3/FeatureManagement.egg-info/not-zip-safe +0 -1
- featuremanagement-2.0.0b3/FeatureManagement.egg-info/requires.txt +0 -3
- featuremanagement-2.0.0b3/MANIFEST.in +0 -6
- featuremanagement-2.0.0b3/SECURITY.md +0 -41
- featuremanagement-2.0.0b3/SUPPORT.md +0 -25
- featuremanagement-2.0.0b3/featuremanagement/azuremonitor/_send_telemetry.py +0 -102
- featuremanagement-2.0.0b3/pyproject.toml +0 -47
- featuremanagement-2.0.0b3/samples/feature_flag_sample.py +0 -34
- featuremanagement-2.0.0b3/samples/feature_flag_with_azure_app_configuration_sample.py +0 -46
- featuremanagement-2.0.0b3/samples/feature_variant_sample.py +0 -25
- featuremanagement-2.0.0b3/samples/feature_variant_sample_with_telemetry.py +0 -36
- featuremanagement-2.0.0b3/samples/formatted_feature_flags.json +0 -229
- featuremanagement-2.0.0b3/samples/random_filter.py +0 -21
- featuremanagement-2.0.0b3/setup.py +0 -64
- featuremanagement-2.0.0b3/tests/test_send_telemetry_appinsights.py +0 -213
- featuremanagement-2.0.0b3/tests/validation_tests/test_json_validations.py +0 -101
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/dependency_links.txt +0 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/top_level.txt +0 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/LICENSE +0 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/README.md +0 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/py.typed +0 -0
- {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/setup.cfg +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A library for enabling/disabling features at runtime.
|
|
5
|
-
Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/appconfiguration/feature-management
|
|
6
|
-
Author: Microsoft Corporation
|
|
7
5
|
Author-email: Microsoft Corporation <appconfig@microsoft.com>
|
|
8
6
|
License: MIT License
|
|
9
7
|
|
|
@@ -33,16 +31,33 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
33
31
|
Classifier: Programming Language :: Python
|
|
34
32
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
33
|
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
38
34
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
35
|
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
40
39
|
Classifier: License :: OSI Approved :: MIT License
|
|
41
|
-
Requires-Python: >=3.
|
|
40
|
+
Requires-Python: >=3.10
|
|
42
41
|
Description-Content-Type: text/markdown
|
|
43
42
|
License-File: LICENSE
|
|
44
43
|
Provides-Extra: azuremonitor
|
|
45
|
-
Requires-Dist:
|
|
44
|
+
Requires-Dist: opentelemetry-sdk~=1.20; extra == "azuremonitor"
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: azure-monitor-opentelemetry<2.0.0; extra == "test"
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pytest<10.0.0,>=7.0.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-cov<8.0.0,>=4.0.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-asyncio<2.0.0,>=0.21.0; extra == "dev"
|
|
51
|
+
Requires-Dist: black<27.0.0,>=23.0.0; extra == "dev"
|
|
52
|
+
Requires-Dist: pylint<5.0.0,>=3.0.0; extra == "dev"
|
|
53
|
+
Requires-Dist: mypy<2.0.0,>=1.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: sphinx<10.0.0,>=7.0.0; extra == "dev"
|
|
55
|
+
Requires-Dist: sphinx_rtd_theme<4.0.0,>=2.0.0; extra == "dev"
|
|
56
|
+
Requires-Dist: sphinx-toolbox<5.0.0,>=3.0.0; extra == "dev"
|
|
57
|
+
Requires-Dist: myst_parser<6.0.0,>=2.0.0; extra == "dev"
|
|
58
|
+
Requires-Dist: opentelemetry-api~=1.20; extra == "dev"
|
|
59
|
+
Requires-Dist: opentelemetry-sdk~=1.20; extra == "dev"
|
|
60
|
+
Dynamic: license-file
|
|
46
61
|
|
|
47
62
|
# Microsoft Feature Management for Python
|
|
48
63
|
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/SOURCES.txt
RENAMED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
CHANGELOG.md
|
|
2
|
-
CODE_OF_CONDUCT.md
|
|
3
1
|
LICENSE
|
|
4
|
-
MANIFEST.in
|
|
5
2
|
README.md
|
|
6
|
-
SECURITY.md
|
|
7
|
-
SUPPORT.md
|
|
8
3
|
pyproject.toml
|
|
9
|
-
setup.py
|
|
10
4
|
FeatureManagement.egg-info/PKG-INFO
|
|
11
5
|
FeatureManagement.egg-info/SOURCES.txt
|
|
12
6
|
FeatureManagement.egg-info/dependency_links.txt
|
|
13
|
-
FeatureManagement.egg-info/not-zip-safe
|
|
14
7
|
FeatureManagement.egg-info/requires.txt
|
|
15
8
|
FeatureManagement.egg-info/top_level.txt
|
|
16
9
|
featuremanagement/__init__.py
|
|
@@ -31,18 +24,16 @@ featuremanagement/_models/_telemetry.py
|
|
|
31
24
|
featuremanagement/_models/_variant.py
|
|
32
25
|
featuremanagement/_models/_variant_assignment_reason.py
|
|
33
26
|
featuremanagement/_models/_variant_reference.py
|
|
27
|
+
featuremanagement/_time_window_filter/__init__.py
|
|
28
|
+
featuremanagement/_time_window_filter/_models.py
|
|
29
|
+
featuremanagement/_time_window_filter/_recurrence_evaluator.py
|
|
30
|
+
featuremanagement/_time_window_filter/_recurrence_validator.py
|
|
34
31
|
featuremanagement/aio/__init__.py
|
|
35
32
|
featuremanagement/aio/_defaultfilters.py
|
|
36
33
|
featuremanagement/aio/_featurefilters.py
|
|
37
34
|
featuremanagement/aio/_featuremanager.py
|
|
38
35
|
featuremanagement/azuremonitor/__init__.py
|
|
39
36
|
featuremanagement/azuremonitor/_send_telemetry.py
|
|
40
|
-
samples/feature_flag_sample.py
|
|
41
|
-
samples/feature_flag_with_azure_app_configuration_sample.py
|
|
42
|
-
samples/feature_variant_sample.py
|
|
43
|
-
samples/feature_variant_sample_with_telemetry.py
|
|
44
|
-
samples/formatted_feature_flags.json
|
|
45
|
-
samples/random_filter.py
|
|
46
37
|
tests/test_default_feature_flags.py
|
|
47
38
|
tests/test_default_feature_flags_async.py
|
|
48
39
|
tests/test_feature_manager.py
|
|
@@ -50,5 +41,4 @@ tests/test_feature_manager_async.py
|
|
|
50
41
|
tests/test_feature_manager_refresh.py
|
|
51
42
|
tests/test_feature_variants.py
|
|
52
43
|
tests/test_feature_variants_async.py
|
|
53
|
-
tests/test_send_telemetry_appinsights.py
|
|
54
|
-
tests/validation_tests/test_json_validations.py
|
|
44
|
+
tests/test_send_telemetry_appinsights.py
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
[AzureMonitor]
|
|
3
|
+
opentelemetry-sdk~=1.20
|
|
4
|
+
|
|
5
|
+
[dev]
|
|
6
|
+
pytest<10.0.0,>=7.0.0
|
|
7
|
+
pytest-cov<8.0.0,>=4.0.0
|
|
8
|
+
pytest-asyncio<2.0.0,>=0.21.0
|
|
9
|
+
black<27.0.0,>=23.0.0
|
|
10
|
+
pylint<5.0.0,>=3.0.0
|
|
11
|
+
mypy<2.0.0,>=1.0.0
|
|
12
|
+
sphinx<10.0.0,>=7.0.0
|
|
13
|
+
sphinx_rtd_theme<4.0.0,>=2.0.0
|
|
14
|
+
sphinx-toolbox<5.0.0,>=3.0.0
|
|
15
|
+
myst_parser<6.0.0,>=2.0.0
|
|
16
|
+
opentelemetry-api~=1.20
|
|
17
|
+
opentelemetry-sdk~=1.20
|
|
18
|
+
|
|
19
|
+
[test]
|
|
20
|
+
azure-monitor-opentelemetry<2.0.0
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: FeatureManagement
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A library for enabling/disabling features at runtime.
|
|
5
|
-
Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/appconfiguration/feature-management
|
|
6
|
-
Author: Microsoft Corporation
|
|
7
5
|
Author-email: Microsoft Corporation <appconfig@microsoft.com>
|
|
8
6
|
License: MIT License
|
|
9
7
|
|
|
@@ -33,16 +31,33 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
33
31
|
Classifier: Programming Language :: Python
|
|
34
32
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
33
|
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
38
34
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
35
|
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
40
39
|
Classifier: License :: OSI Approved :: MIT License
|
|
41
|
-
Requires-Python: >=3.
|
|
40
|
+
Requires-Python: >=3.10
|
|
42
41
|
Description-Content-Type: text/markdown
|
|
43
42
|
License-File: LICENSE
|
|
44
43
|
Provides-Extra: azuremonitor
|
|
45
|
-
Requires-Dist:
|
|
44
|
+
Requires-Dist: opentelemetry-sdk~=1.20; extra == "azuremonitor"
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: azure-monitor-opentelemetry<2.0.0; extra == "test"
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pytest<10.0.0,>=7.0.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-cov<8.0.0,>=4.0.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-asyncio<2.0.0,>=0.21.0; extra == "dev"
|
|
51
|
+
Requires-Dist: black<27.0.0,>=23.0.0; extra == "dev"
|
|
52
|
+
Requires-Dist: pylint<5.0.0,>=3.0.0; extra == "dev"
|
|
53
|
+
Requires-Dist: mypy<2.0.0,>=1.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: sphinx<10.0.0,>=7.0.0; extra == "dev"
|
|
55
|
+
Requires-Dist: sphinx_rtd_theme<4.0.0,>=2.0.0; extra == "dev"
|
|
56
|
+
Requires-Dist: sphinx-toolbox<5.0.0,>=3.0.0; extra == "dev"
|
|
57
|
+
Requires-Dist: myst_parser<6.0.0,>=2.0.0; extra == "dev"
|
|
58
|
+
Requires-Dist: opentelemetry-api~=1.20; extra == "dev"
|
|
59
|
+
Requires-Dist: opentelemetry-sdk~=1.20; extra == "dev"
|
|
60
|
+
Dynamic: license-file
|
|
46
61
|
|
|
47
62
|
# Microsoft Feature Management for Python
|
|
48
63
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Feature management library for Python."""
|
|
7
|
+
|
|
6
8
|
from ._featuremanager import FeatureManager
|
|
7
9
|
from ._featurefilters import FeatureFilter
|
|
8
10
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Built-in feature filter implementations."""
|
|
7
|
+
|
|
6
8
|
import logging
|
|
7
9
|
import hashlib
|
|
8
10
|
from datetime import datetime, timezone
|
|
9
11
|
from email.utils import parsedate_to_datetime
|
|
10
12
|
from typing import cast, List, Mapping, Optional, Dict, Any
|
|
11
13
|
from ._featurefilters import FeatureFilter
|
|
14
|
+
from ._time_window_filter import Recurrence, is_match, TimeWindowFilterSettings
|
|
12
15
|
|
|
13
16
|
FEATURE_FLAG_NAME_KEY = "feature_name"
|
|
14
17
|
ROLLOUT_PERCENTAGE_KEY = "RolloutPercentage"
|
|
@@ -18,6 +21,15 @@ PARAMETERS_KEY = "parameters"
|
|
|
18
21
|
# Time Window Constants
|
|
19
22
|
START_KEY = "Start"
|
|
20
23
|
END_KEY = "End"
|
|
24
|
+
TIME_WINDOW_FILTER_SETTING_RECURRENCE = "Recurrence"
|
|
25
|
+
|
|
26
|
+
# Time Window Exceptions
|
|
27
|
+
TIME_WINDOW_FILTER_INVALID = (
|
|
28
|
+
"{}: The {} feature filter is not valid for feature {}. It must specify either {}, {}, or both."
|
|
29
|
+
)
|
|
30
|
+
TIME_WINDOW_FILTER_INVALID_RECURRENCE = (
|
|
31
|
+
"{}: The {} feature filter is not valid for feature {}. It must specify both {} and {} when Recurrence is not None."
|
|
32
|
+
)
|
|
21
33
|
|
|
22
34
|
# Targeting kwargs
|
|
23
35
|
TARGETED_USER_KEY = "user"
|
|
@@ -31,6 +43,8 @@ EXCLUSION_KEY = "Exclusion"
|
|
|
31
43
|
FEATURE_FILTER_NAME_KEY = "Name"
|
|
32
44
|
IGNORE_CASE_KEY = "ignore_case"
|
|
33
45
|
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
34
48
|
|
|
35
49
|
class TargetingException(Exception):
|
|
36
50
|
"""
|
|
@@ -52,19 +66,35 @@ class TimeWindowFilter(FeatureFilter):
|
|
|
52
66
|
:return: True if the current time is within the time window.
|
|
53
67
|
:rtype: bool
|
|
54
68
|
"""
|
|
55
|
-
start = context.get(PARAMETERS_KEY, {}).get(START_KEY)
|
|
56
|
-
end = context.get(PARAMETERS_KEY, {}).get(END_KEY)
|
|
69
|
+
start = context.get(PARAMETERS_KEY, {}).get(START_KEY, None)
|
|
70
|
+
end = context.get(PARAMETERS_KEY, {}).get(END_KEY, None)
|
|
71
|
+
recurrence_data = context.get(PARAMETERS_KEY, {}).get(TIME_WINDOW_FILTER_SETTING_RECURRENCE, None)
|
|
72
|
+
recurrence = None
|
|
57
73
|
|
|
58
74
|
current_time = datetime.now(timezone.utc)
|
|
59
75
|
|
|
60
76
|
if not start and not end:
|
|
61
|
-
|
|
77
|
+
logger.warning(
|
|
78
|
+
TIME_WINDOW_FILTER_INVALID,
|
|
79
|
+
TimeWindowFilter.__name__,
|
|
80
|
+
context.get(FEATURE_FLAG_NAME_KEY),
|
|
81
|
+
START_KEY,
|
|
82
|
+
END_KEY,
|
|
83
|
+
)
|
|
62
84
|
return False
|
|
63
85
|
|
|
64
|
-
start_time = parsedate_to_datetime(start) if start else None
|
|
65
|
-
end_time = parsedate_to_datetime(end) if end else None
|
|
86
|
+
start_time: Optional[datetime] = parsedate_to_datetime(start) if start else None
|
|
87
|
+
end_time: Optional[datetime] = parsedate_to_datetime(end) if end else None
|
|
88
|
+
|
|
89
|
+
if (start_time is None or start_time <= current_time) and (end_time is None or current_time < end_time):
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
if recurrence_data:
|
|
93
|
+
recurrence = Recurrence(recurrence_data)
|
|
94
|
+
settings = TimeWindowFilterSettings(start_time, end_time, recurrence)
|
|
95
|
+
return is_match(settings, current_time)
|
|
66
96
|
|
|
67
|
-
return
|
|
97
|
+
return False
|
|
68
98
|
|
|
69
99
|
|
|
70
100
|
@FeatureFilter.alias("Microsoft.Targeting")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Base class for feature filters."""
|
|
7
|
+
|
|
6
8
|
from abc import ABC, abstractmethod
|
|
7
9
|
from typing import Mapping, Callable, Any, Optional
|
|
8
10
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
-
|
|
6
|
+
"""Synchronous feature manager implementation."""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import cast, overload, Any, Optional, Dict, Mapping, List, Tuple
|
|
7
10
|
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
8
11
|
from ._featurefilters import FeatureFilter
|
|
9
12
|
from ._models import EvaluationEvent, Variant, TargetingContext
|
|
@@ -14,6 +17,8 @@ from ._featuremanagerbase import (
|
|
|
14
17
|
FEATURE_FILTER_NAME,
|
|
15
18
|
)
|
|
16
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
class FeatureManager(FeatureManagerBase):
|
|
19
24
|
"""
|
|
@@ -23,6 +28,8 @@ class FeatureManager(FeatureManagerBase):
|
|
|
23
28
|
:keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
|
|
24
29
|
:keyword Callable[EvaluationEvent] on_feature_evaluated: Callback function to be called when a feature flag is
|
|
25
30
|
evaluated.
|
|
31
|
+
:keyword Callable[[], TargetingContext] targeting_context_accessor: Callback function to get the current targeting
|
|
32
|
+
context if one isn't provided.
|
|
26
33
|
"""
|
|
27
34
|
|
|
28
35
|
def __init__(self, configuration: Mapping[str, Any], **kwargs: Any):
|
|
@@ -56,7 +63,7 @@ class FeatureManager(FeatureManagerBase):
|
|
|
56
63
|
:return: True if the feature flag is enabled for the given context.
|
|
57
64
|
:rtype: bool
|
|
58
65
|
"""
|
|
59
|
-
targeting_context = self._build_targeting_context(args)
|
|
66
|
+
targeting_context: TargetingContext = self._build_targeting_context(args)
|
|
60
67
|
|
|
61
68
|
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
62
69
|
if (
|
|
@@ -89,7 +96,7 @@ class FeatureManager(FeatureManagerBase):
|
|
|
89
96
|
:return: Variant instance.
|
|
90
97
|
:rtype: Variant
|
|
91
98
|
"""
|
|
92
|
-
targeting_context = self._build_targeting_context(args)
|
|
99
|
+
targeting_context: TargetingContext = self._build_targeting_context(args)
|
|
93
100
|
|
|
94
101
|
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
95
102
|
if (
|
|
@@ -102,6 +109,21 @@ class FeatureManager(FeatureManagerBase):
|
|
|
102
109
|
self._on_feature_evaluated(result)
|
|
103
110
|
return result.variant
|
|
104
111
|
|
|
112
|
+
def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
|
|
113
|
+
targeting_context = super()._build_targeting_context(args)
|
|
114
|
+
if targeting_context:
|
|
115
|
+
return targeting_context
|
|
116
|
+
if not targeting_context and self._targeting_context_accessor and callable(self._targeting_context_accessor):
|
|
117
|
+
targeting_context = self._targeting_context_accessor()
|
|
118
|
+
if targeting_context and isinstance(targeting_context, TargetingContext):
|
|
119
|
+
return targeting_context
|
|
120
|
+
logger.warning(
|
|
121
|
+
"targeting_context_accessor did not return a TargetingContext. Received type %s.",
|
|
122
|
+
type(targeting_context),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return TargetingContext()
|
|
126
|
+
|
|
105
127
|
def _check_feature_filters(
|
|
106
128
|
self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Any
|
|
107
129
|
) -> None:
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featuremanagerbase.py
RENAMED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Base class for feature manager implementations."""
|
|
7
|
+
|
|
6
8
|
import hashlib
|
|
7
9
|
import logging
|
|
8
10
|
from abc import ABC
|
|
9
|
-
from typing import List, Optional, Dict, Tuple, Any, Mapping
|
|
11
|
+
from typing import List, Optional, Dict, Tuple, Any, Mapping, Callable, cast
|
|
10
12
|
from ._models import FeatureFlag, Variant, VariantAssignmentReason, TargetingContext, EvaluationEvent, VariantReference
|
|
11
13
|
|
|
12
|
-
|
|
13
14
|
FEATURE_MANAGEMENT_KEY = "feature_management"
|
|
14
15
|
FEATURE_FLAG_KEY = "feature_flags"
|
|
15
16
|
|
|
@@ -21,48 +22,7 @@ REQUIREMENT_TYPE_ANY = "Any"
|
|
|
21
22
|
FEATURE_FILTER_PARAMETERS = "parameters"
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
"""
|
|
26
|
-
Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
|
|
27
|
-
|
|
28
|
-
:param Mapping configuration: Configuration object.
|
|
29
|
-
:param str feature_flag_name: Name of the feature flag.
|
|
30
|
-
:return: FeatureFlag
|
|
31
|
-
:rtype: FeatureFlag
|
|
32
|
-
"""
|
|
33
|
-
feature_management = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
34
|
-
if not feature_management or not isinstance(feature_management, Mapping):
|
|
35
|
-
return None
|
|
36
|
-
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
|
|
37
|
-
if not feature_flags or not isinstance(feature_flags, list):
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
for feature_flag in feature_flags:
|
|
41
|
-
if feature_flag.get("id") == feature_flag_name:
|
|
42
|
-
return FeatureFlag.convert_from_json(feature_flag)
|
|
43
|
-
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]:
|
|
48
|
-
"""
|
|
49
|
-
List of all feature flag names.
|
|
50
|
-
|
|
51
|
-
:param Mapping configuration: Configuration object.
|
|
52
|
-
:return: List of feature flag names.
|
|
53
|
-
"""
|
|
54
|
-
feature_flag_names = []
|
|
55
|
-
feature_management = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
56
|
-
if not feature_management or not isinstance(feature_management, Mapping):
|
|
57
|
-
return []
|
|
58
|
-
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
|
|
59
|
-
if not feature_flags or not isinstance(feature_flags, list):
|
|
60
|
-
return []
|
|
61
|
-
|
|
62
|
-
for feature_flag in feature_flags:
|
|
63
|
-
feature_flag_names.append(feature_flag.get("id"))
|
|
64
|
-
|
|
65
|
-
return feature_flag_names
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
66
26
|
|
|
67
27
|
|
|
68
28
|
class FeatureManagerBase(ABC):
|
|
@@ -77,6 +37,9 @@ class FeatureManagerBase(ABC):
|
|
|
77
37
|
self._cache: Dict[str, Optional[FeatureFlag]] = {}
|
|
78
38
|
self._copy = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
79
39
|
self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None)
|
|
40
|
+
self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop(
|
|
41
|
+
"targeting_context_accessor", None
|
|
42
|
+
)
|
|
80
43
|
|
|
81
44
|
@staticmethod
|
|
82
45
|
def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
|
|
@@ -218,7 +181,7 @@ class FeatureManagerBase(ABC):
|
|
|
218
181
|
return Variant(variant_reference.name, variant_reference.configuration_value)
|
|
219
182
|
return None
|
|
220
183
|
|
|
221
|
-
def _build_targeting_context(self, args: Tuple[Any]) -> TargetingContext:
|
|
184
|
+
def _build_targeting_context(self, args: Tuple[Any]) -> Optional[TargetingContext]:
|
|
222
185
|
"""
|
|
223
186
|
Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
|
|
224
187
|
returns an empty context.
|
|
@@ -229,10 +192,12 @@ class FeatureManagerBase(ABC):
|
|
|
229
192
|
if len(args) == 1:
|
|
230
193
|
arg = args[0]
|
|
231
194
|
if isinstance(arg, str):
|
|
195
|
+
# If the user_id is provided, return a TargetingContext with the user_id
|
|
232
196
|
return TargetingContext(user_id=arg, groups=[])
|
|
233
197
|
if isinstance(arg, TargetingContext):
|
|
198
|
+
# If a TargetingContext is provided, return it
|
|
234
199
|
return arg
|
|
235
|
-
return
|
|
200
|
+
return None
|
|
236
201
|
|
|
237
202
|
def _assign_allocation(self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext) -> None:
|
|
238
203
|
feature_flag = evaluation_event.feature
|
|
@@ -264,14 +229,14 @@ class FeatureManagerBase(ABC):
|
|
|
264
229
|
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
265
230
|
|
|
266
231
|
if not self._cache.get(feature_flag_id):
|
|
267
|
-
feature_flag = _get_feature_flag(
|
|
232
|
+
feature_flag = self._get_feature_flag(feature_flag_id)
|
|
268
233
|
self._cache[feature_flag_id] = feature_flag
|
|
269
234
|
else:
|
|
270
235
|
feature_flag = self._cache.get(feature_flag_id)
|
|
271
236
|
|
|
272
237
|
evaluation_event = EvaluationEvent(feature_flag)
|
|
273
238
|
if not feature_flag:
|
|
274
|
-
|
|
239
|
+
logger.warning("Feature flag %s not found", feature_flag_id)
|
|
275
240
|
# Unknown feature flags are disabled by default
|
|
276
241
|
return evaluation_event, True
|
|
277
242
|
|
|
@@ -291,4 +256,41 @@ class FeatureManagerBase(ABC):
|
|
|
291
256
|
"""
|
|
292
257
|
List of all feature flag names.
|
|
293
258
|
"""
|
|
294
|
-
|
|
259
|
+
feature_flag_names: Dict[str, None] = {}
|
|
260
|
+
for feature_flag in self._get_feature_flags():
|
|
261
|
+
feature_flag_name = feature_flag.get("id")
|
|
262
|
+
# Only include entries with a valid string id; duplicates are listed once.
|
|
263
|
+
if isinstance(feature_flag_name, str):
|
|
264
|
+
feature_flag_names[feature_flag_name] = None
|
|
265
|
+
|
|
266
|
+
return list(feature_flag_names)
|
|
267
|
+
|
|
268
|
+
def _get_feature_flag(self, feature_flag_name: str) -> Optional[FeatureFlag]:
|
|
269
|
+
"""
|
|
270
|
+
Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
|
|
271
|
+
|
|
272
|
+
:param str feature_flag_name: Name of the feature flag.
|
|
273
|
+
:return: FeatureFlag
|
|
274
|
+
:rtype: FeatureFlag
|
|
275
|
+
"""
|
|
276
|
+
for feature_flag in reversed(self._get_feature_flags()):
|
|
277
|
+
# If multiple feature flags share the same id, the last one defined wins.
|
|
278
|
+
if feature_flag.get("id") == feature_flag_name:
|
|
279
|
+
return FeatureFlag.convert_from_json(feature_flag)
|
|
280
|
+
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def _get_feature_flags(self) -> List[Any]:
|
|
284
|
+
"""
|
|
285
|
+
Gets the list of raw feature flag definitions from the configuration.
|
|
286
|
+
|
|
287
|
+
:return: List of feature flag definitions, or an empty list if none are configured.
|
|
288
|
+
:rtype: list
|
|
289
|
+
"""
|
|
290
|
+
feature_management = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
291
|
+
if not feature_management or not isinstance(feature_management, Mapping):
|
|
292
|
+
return []
|
|
293
|
+
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
|
|
294
|
+
if not feature_flags or not isinstance(feature_flags, list):
|
|
295
|
+
return []
|
|
296
|
+
return cast(List[Any], feature_flags)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Data models for feature management."""
|
|
7
|
+
|
|
6
8
|
from ._feature_flag import FeatureFlag
|
|
7
9
|
from ._variant import Variant
|
|
8
10
|
from ._evaluation_event import EvaluationEvent
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_allocation.py
RENAMED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Allocation model for feature variant assignment."""
|
|
7
|
+
|
|
6
8
|
from typing import cast, List, Optional, Mapping, Dict, Any, Union
|
|
7
9
|
from dataclasses import dataclass
|
|
8
10
|
from ._constants import DEFAULT_WHEN_ENABLED, DEFAULT_WHEN_DISABLED, USER, GROUP, PERCENTILE, SEED
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_constants.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Constants used by feature management models."""
|
|
6
7
|
|
|
7
8
|
# Feature Flag
|
|
8
9
|
FEATURE_FLAG_ID = "id"
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_evaluation_event.py
RENAMED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Evaluation event model for feature flag telemetry."""
|
|
7
|
+
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from typing import Optional
|
|
8
10
|
from ._feature_flag import FeatureFlag
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Feature flag condition models."""
|
|
7
|
+
|
|
6
8
|
from collections.abc import Mapping
|
|
7
9
|
from typing import Any, Dict, List
|
|
8
10
|
from ._constants import (
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_feature_flag.py
RENAMED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Feature flag model."""
|
|
7
|
+
|
|
6
8
|
from typing import cast, List, Union, Optional, Mapping, Any
|
|
7
9
|
from ._feature_conditions import FeatureConditions
|
|
8
10
|
from ._allocation import Allocation
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
|
+
"""Targeting context model for user and group targeting."""
|
|
6
7
|
|
|
7
8
|
from typing import NamedTuple, List
|
|
8
9
|
|
{featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_telemetry.py
RENAMED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Telemetry metadata model."""
|
|
7
|
+
|
|
6
8
|
from typing import Dict
|
|
7
9
|
from dataclasses import dataclass, field
|
|
8
10
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Variant model representing a feature flag variant."""
|
|
7
|
+
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
8
10
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# -------------------------------------------------------------------------
|
|
6
|
+
"""Variant reference model."""
|
|
7
|
+
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from typing import Optional, Mapping, Any
|
|
8
10
|
from ._constants import VARIANT_REFERENCE_NAME, CONFIGURATION_VALUE, STATUS_OVERRIDE
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
"""Time window filter with recurrence support."""
|
|
7
|
+
|
|
8
|
+
from ._recurrence_evaluator import is_match
|
|
9
|
+
from ._models import Recurrence, TimeWindowFilterSettings
|
|
10
|
+
|
|
11
|
+
__all__ = ["is_match", "Recurrence", "TimeWindowFilterSettings"]
|