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.
Files changed (58) hide show
  1. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/CHANGELOG.md +5 -0
  2. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1/FeatureManagement.egg-info}/PKG-INFO +4 -4
  3. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/SOURCES.txt +13 -0
  4. featuremanagement-2.0.0b1/FeatureManagement.egg-info/requires.txt +3 -0
  5. {featuremanagement-1.0.0/FeatureManagement.egg-info → featuremanagement-2.0.0b1}/PKG-INFO +4 -4
  6. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/README.md +1 -0
  7. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/__init__.py +5 -1
  8. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_defaultfilters.py +14 -10
  9. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_featurefilters.py +9 -6
  10. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_featuremanager.py +88 -98
  11. featuremanagement-2.0.0b1/featuremanagement/_featuremanagerbase.py +255 -0
  12. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/__init__.py +12 -2
  13. featuremanagement-2.0.0b1/featuremanagement/_models/_allocation.py +207 -0
  14. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_constants.py +15 -0
  15. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_evaluation_event.py +8 -4
  16. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_feature_conditions.py +7 -6
  17. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_feature_flag.py +62 -14
  18. featuremanagement-2.0.0b1/featuremanagement/_models/_telemetry.py +17 -0
  19. featuremanagement-2.0.0b1/featuremanagement/_models/_variant.py +35 -0
  20. featuremanagement-2.0.0b1/featuremanagement/_models/_variant_assignment_reason.py +19 -0
  21. featuremanagement-2.0.0b1/featuremanagement/_models/_variant_reference.py +65 -0
  22. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_version.py +1 -1
  23. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/_defaultfilters.py +5 -5
  24. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/_featurefilters.py +9 -6
  25. featuremanagement-2.0.0b1/featuremanagement/aio/_featuremanager.py +178 -0
  26. featuremanagement-2.0.0b1/featuremanagement/azuremonitor/__init__.py +12 -0
  27. featuremanagement-2.0.0b1/featuremanagement/azuremonitor/_send_telemetry.py +68 -0
  28. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/pyproject.toml +3 -3
  29. featuremanagement-2.0.0b1/samples/feature_variant_sample.py +25 -0
  30. featuremanagement-2.0.0b1/samples/feature_variant_sample_with_telemetry.py +36 -0
  31. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/formatted_feature_flags.json +0 -1
  32. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/setup.py +1 -1
  33. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_default_feature_flags.py +1 -1
  34. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_default_feature_flags_async.py +1 -1
  35. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager_refresh.py +1 -1
  36. featuremanagement-2.0.0b1/tests/test_feature_variants.py +251 -0
  37. featuremanagement-2.0.0b1/tests/test_feature_variants_async.py +257 -0
  38. featuremanagement-2.0.0b1/tests/test_send_telemetry_appinsights.py +78 -0
  39. featuremanagement-1.0.0/FeatureManagement.egg-info/requires.txt +0 -4
  40. featuremanagement-1.0.0/featuremanagement/aio/_featuremanager.py +0 -142
  41. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/CODE_OF_CONDUCT.md +0 -0
  42. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/dependency_links.txt +0 -0
  43. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/not-zip-safe +0 -0
  44. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/FeatureManagement.egg-info/top_level.txt +0 -0
  45. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/LICENSE +0 -0
  46. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/MANIFEST.in +0 -0
  47. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/SECURITY.md +0 -0
  48. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/SUPPORT.md +0 -0
  49. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/_models/_targeting_context.py +0 -0
  50. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/aio/__init__.py +0 -0
  51. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/featuremanagement/py.typed +0 -0
  52. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/feature_flag_sample.py +0 -0
  53. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
  54. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/samples/random_filter.py +0 -0
  55. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/setup.cfg +0 -0
  56. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager.py +0 -0
  57. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/test_feature_manager_async.py +0 -0
  58. {featuremanagement-1.0.0 → featuremanagement-2.0.0b1}/tests/validation_tests/test_json_validations.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Release History
2
2
 
3
+ ## 2.0.0b1 (09/10/2024)
4
+
5
+ * Adds support for Feature Variants.
6
+ * Adds support for Telemetry.
7
+
3
8
  ## 1.0.0 (06/26/2024)
4
9
 
5
10
  Updated version to 1.0.0.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FeatureManagement
3
- Version: 1.0.0
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: appinsights
45
- Requires-Dist: azure-monitor-opentelemetry<2.0.0,>=1.3.0; extra == "appinsights"
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)
@@ -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
@@ -0,0 +1,3 @@
1
+
2
+ [AzureMonitor]
3
+ azure-monitor-events-extension<2.0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FeatureManagement
3
- Version: 1.0.0
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: appinsights
45
- Requires-Dist: azure-monitor-opentelemetry<2.0.0,>=1.3.0; extra == "appinsights"
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(self, target_user, target_group, group, feature_flag_name):
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 = kwargs.get(TARGETED_USER_KEY, None)
107
- target_groups = kwargs.get(TARGETED_GROUPS_KEY, [])
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: callable
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 collections.abc import Mapping
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 FeatureFlag, EvaluationEvent, TargetingContext
12
-
13
-
14
- FEATURE_MANAGEMENT_KEY = "feature_management"
15
- FEATURE_FLAG_KEY = "feature_flags"
16
-
17
- PROVIDED_FEATURE_FILTERS = "feature_filters"
18
- FEATURE_FILTER_NAME = "name"
19
- REQUIREMENT_TYPE_ALL = "All"
20
- REQUIREMENT_TYPE_ANY = "Any"
21
-
22
- FEATURE_FILTER_PARAMETERS = "parameters"
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
- self._filters = {}
79
- if configuration is None or not isinstance(configuration, Mapping):
80
- raise AttributeError("Configuration must be a non-empty dictionary")
81
- self._configuration = configuration
82
- self._cache = {}
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
- def _build_targeting_context(self, args):
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
- def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
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(self, feature_flag_id, targeting_context, **kwargs):
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 EvaluationEvent(enabled=False)
163
+ return evaluation_event
179
164
 
180
165
  if not feature_flag.enabled:
181
166
  # Feature flags that are disabled are always disabled
182
- return EvaluationEvent(enabled=False)
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
- return self._check_feature_filters(feature_flag, targeting_context, **kwargs)
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
- def list_feature_flag_names(self):
187
- """
188
- List of all feature flag names.
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