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.
Files changed (69) hide show
  1. featuremanagement-2.1.0/CHANGELOG.md +3 -0
  2. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0/FeatureManagement.egg-info}/PKG-INFO +6 -2
  3. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/SOURCES.txt +10 -0
  4. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/top_level.txt +1 -0
  5. {featuremanagement-2.0.0b2/FeatureManagement.egg-info → featuremanagement-2.1.0}/PKG-INFO +6 -2
  6. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_defaultfilters.py +34 -6
  7. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featuremanager.py +27 -32
  8. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featuremanagerbase.py +102 -58
  9. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_allocation.py +2 -2
  10. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_evaluation_event.py +1 -1
  11. featuremanagement-2.1.0/featuremanagement/_time_window_filter/__init__.py +9 -0
  12. featuremanagement-2.1.0/featuremanagement/_time_window_filter/_models.py +149 -0
  13. featuremanagement-2.1.0/featuremanagement/_time_window_filter/_recurrence_evaluator.py +125 -0
  14. featuremanagement-2.1.0/featuremanagement/_time_window_filter/_recurrence_validator.py +164 -0
  15. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_version.py +1 -1
  16. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_featuremanager.py +45 -37
  17. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/azuremonitor/__init__.py +2 -1
  18. featuremanagement-2.1.0/featuremanagement/azuremonitor/_send_telemetry.py +154 -0
  19. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/pyproject.toml +1 -1
  20. featuremanagement-2.1.0/samples/feature_variant_sample_with_targeting_accessor.py +36 -0
  21. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/formatted_feature_flags.json +1 -0
  22. featuremanagement-2.1.0/samples/quarty_sample.py +60 -0
  23. featuremanagement-2.1.0/tests/__init__.py +0 -0
  24. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_default_feature_flags.py +49 -0
  25. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_default_feature_flags_async.py +50 -0
  26. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager.py +32 -1
  27. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager_async.py +54 -1
  28. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_variants.py +38 -1
  29. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_variants_async.py +67 -0
  30. featuremanagement-2.1.0/tests/test_send_telemetry_appinsights.py +250 -0
  31. featuremanagement-2.1.0/tests/time_window_filter/test_recurrence_evaluator.py +281 -0
  32. featuremanagement-2.1.0/tests/time_window_filter/test_recurrence_validator.py +321 -0
  33. featuremanagement-2.1.0/tests/time_window_filter/test_time_window_filter_models.py +133 -0
  34. featuremanagement-2.0.0b2/CHANGELOG.md +0 -27
  35. featuremanagement-2.0.0b2/featuremanagement/azuremonitor/_send_telemetry.py +0 -102
  36. featuremanagement-2.0.0b2/tests/test_send_telemetry_appinsights.py +0 -78
  37. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/CODE_OF_CONDUCT.md +0 -0
  38. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/dependency_links.txt +0 -0
  39. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/not-zip-safe +0 -0
  40. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/FeatureManagement.egg-info/requires.txt +0 -0
  41. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/LICENSE +0 -0
  42. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/MANIFEST.in +0 -0
  43. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/README.md +0 -0
  44. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/SECURITY.md +0 -0
  45. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/SUPPORT.md +0 -0
  46. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/__init__.py +0 -0
  47. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_featurefilters.py +0 -0
  48. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/__init__.py +0 -0
  49. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_constants.py +0 -0
  50. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_feature_conditions.py +0 -0
  51. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_feature_flag.py +0 -0
  52. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_targeting_context.py +0 -0
  53. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_telemetry.py +0 -0
  54. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant.py +0 -0
  55. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant_assignment_reason.py +0 -0
  56. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/_models/_variant_reference.py +0 -0
  57. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/__init__.py +0 -0
  58. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_defaultfilters.py +0 -0
  59. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/aio/_featurefilters.py +0 -0
  60. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/featuremanagement/py.typed +0 -0
  61. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_flag_sample.py +0 -0
  62. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_flag_with_azure_app_configuration_sample.py +0 -0
  63. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_variant_sample.py +0 -0
  64. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/feature_variant_sample_with_telemetry.py +0 -0
  65. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/samples/random_filter.py +0 -0
  66. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/setup.cfg +0 -0
  67. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/setup.py +0 -0
  68. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/test_feature_manager_refresh.py +0 -0
  69. {featuremanagement-2.0.0b2 → featuremanagement-2.1.0}/tests/validation_tests/test_json_validations.py +0 -0
@@ -0,0 +1,3 @@
1
+ # Release History
2
+
3
+ See [Release Notes](https://github.com/Azure/AppConfiguration/blob/main/releaseNotes/PythonFeatureManagement.md) for the full release history.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: FeatureManagement
3
- Version: 2.0.0b2
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
 
@@ -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
1
+ Metadata-Version: 2.4
2
2
  Name: FeatureManagement
3
- Version: 2.0.0b2
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
- logging.warning("%s: At least one of Start or End is required.", TimeWindowFilter.__name__)
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 (start_time is None or start_time <= current_time) and (end_time is None or current_time < end_time)
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
- :return: True if the feature flag is enabled for the given context.
147
- :rtype: bool
163
+ :param TargetingContext targeting_context: Targeting context.
164
+ :return: EvaluationEvent for the given context.
165
+ :rtype: EvaluationEvent
148
166
  """
149
- if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
150
- self._cache = {}
151
- self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
152
-
153
- if not self._cache.get(feature_flag_id):
154
- feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
155
- self._cache[feature_flag_id] = feature_flag
156
- else:
157
- feature_flag = self._cache.get(feature_flag_id)
158
-
159
- evaluation_event = EvaluationEvent(feature_flag)
160
- if not feature_flag:
161
- logging.warning("Feature flag %s not found", feature_flag_id)
162
- # Unknown feature flags are disabled by default
163
- return evaluation_event
164
-
165
- if not feature_flag.enabled:
166
- # Feature flags that are disabled are always disabled
167
- FeatureManager._check_default_disabled_variant(evaluation_event)
168
- if feature_flag.allocation:
169
- variant_name = feature_flag.allocation.default_when_disabled
170
- evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
171
- evaluation_event.feature = feature_flag
167
+ evaluation_event, done = super()._check_feature_base(feature_flag_id)
172
168
 
173
- # If a feature flag is disabled and override can't enable it
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)
@@ -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 _check_default_disabled_variant(evaluation_event: EvaluationEvent) -> None:
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._check_variant_override(
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 _check_default_enabled_variant(evaluation_event: EvaluationEvent) -> None:
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._check_variant_override(
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 _check_variant_override(
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
- feature = evaluation_event.feature
166
+ feature_flag = evaluation_event.feature
163
167
  variant_name = None
164
- if not feature or not feature.variants or not feature.allocation:
168
+ if not feature_flag or not feature_flag.variants or not feature_flag.allocation:
165
169
  return
166
- if feature.allocation.user and targeting_context.user_id:
167
- for user_allocation in feature.allocation.user:
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
- if not variant_name and feature.allocation.group and len(targeting_context.groups) > 0:
172
- for group_allocation in feature.allocation.group:
173
- for group in targeting_context.groups:
174
- if group in group_allocation.groups:
175
- evaluation_event.reason = VariantAssignmentReason.GROUP
176
- variant_name = group_allocation.variant
177
- if not variant_name and feature.allocation.percentile:
178
- seed = feature.allocation.seed
179
- if not seed:
180
- seed = "allocation\n" + feature.name
181
- context_id = targeting_context.user_id + "\n" + seed
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 feature.allocation.percentile:
184
- if box == 100 and percentile_allocation.percentile_to == 100:
185
- variant_name = percentile_allocation.variant
186
- if not percentile_allocation.percentile_to:
187
- continue
188
- if percentile_allocation.percentile_from <= box < percentile_allocation.percentile_to:
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._check_default_enabled_variant(evaluation_event)
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 not feature_flag.variants:
200
- return
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 and isinstance(args[0], str):
229
- return TargetingContext(user_id=args[0], groups=[])
230
- if len(args) == 1 and isinstance(args[0], TargetingContext):
231
- return args[0]
232
- return TargetingContext()
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
- self._assign_variant(feature_flag, targeting_context, evaluation_event)
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
  """
@@ -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: Optional[int] = None
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) -> Optional[int]:
91
+ def percentile_to(self) -> int:
92
92
  """
93
93
  Get the ending percentile for the allocation.
94
94
 
@@ -24,4 +24,4 @@ class EvaluationEvent:
24
24
  self.user = ""
25
25
  self.enabled = False
26
26
  self.variant: Optional[Variant] = None
27
- self.reason: Optional[VariantAssignmentReason] = None
27
+ self.reason: VariantAssignmentReason = VariantAssignmentReason.NONE
@@ -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"]