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.
Files changed (64) hide show
  1. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0/FeatureManagement.egg-info}/PKG-INFO +23 -8
  2. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/SOURCES.txt +5 -15
  3. featuremanagement-2.2.0/FeatureManagement.egg-info/requires.txt +20 -0
  4. {featuremanagement-2.0.0b3/FeatureManagement.egg-info → featuremanagement-2.2.0}/PKG-INFO +23 -8
  5. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/__init__.py +2 -0
  6. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_defaultfilters.py +36 -6
  7. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featurefilters.py +2 -0
  8. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featuremanager.py +25 -3
  9. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_featuremanagerbase.py +51 -49
  10. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/__init__.py +2 -0
  11. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_allocation.py +2 -0
  12. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_constants.py +1 -0
  13. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_evaluation_event.py +2 -0
  14. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_feature_conditions.py +2 -0
  15. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_feature_flag.py +2 -0
  16. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_targeting_context.py +1 -0
  17. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_telemetry.py +2 -0
  18. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant.py +2 -0
  19. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant_assignment_reason.py +2 -0
  20. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_models/_variant_reference.py +2 -0
  21. featuremanagement-2.2.0/featuremanagement/_time_window_filter/__init__.py +11 -0
  22. featuremanagement-2.2.0/featuremanagement/_time_window_filter/_models.py +151 -0
  23. featuremanagement-2.2.0/featuremanagement/_time_window_filter/_recurrence_evaluator.py +127 -0
  24. featuremanagement-2.2.0/featuremanagement/_time_window_filter/_recurrence_validator.py +165 -0
  25. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/_version.py +2 -1
  26. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/__init__.py +2 -0
  27. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_defaultfilters.py +2 -0
  28. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_featurefilters.py +2 -0
  29. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/aio/_featuremanager.py +29 -3
  30. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/azuremonitor/__init__.py +3 -1
  31. featuremanagement-2.2.0/featuremanagement/azuremonitor/_send_telemetry.py +179 -0
  32. featuremanagement-2.2.0/pyproject.toml +78 -0
  33. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_default_feature_flags.py +51 -0
  34. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_default_feature_flags_async.py +52 -0
  35. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager.py +32 -0
  36. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager_async.py +34 -0
  37. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_manager_refresh.py +2 -0
  38. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_variants.py +2 -0
  39. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/tests/test_feature_variants_async.py +2 -0
  40. featuremanagement-2.2.0/tests/test_send_telemetry_appinsights.py +298 -0
  41. featuremanagement-2.0.0b3/CHANGELOG.md +0 -32
  42. featuremanagement-2.0.0b3/CODE_OF_CONDUCT.md +0 -9
  43. featuremanagement-2.0.0b3/FeatureManagement.egg-info/not-zip-safe +0 -1
  44. featuremanagement-2.0.0b3/FeatureManagement.egg-info/requires.txt +0 -3
  45. featuremanagement-2.0.0b3/MANIFEST.in +0 -6
  46. featuremanagement-2.0.0b3/SECURITY.md +0 -41
  47. featuremanagement-2.0.0b3/SUPPORT.md +0 -25
  48. featuremanagement-2.0.0b3/featuremanagement/azuremonitor/_send_telemetry.py +0 -102
  49. featuremanagement-2.0.0b3/pyproject.toml +0 -47
  50. featuremanagement-2.0.0b3/samples/feature_flag_sample.py +0 -34
  51. featuremanagement-2.0.0b3/samples/feature_flag_with_azure_app_configuration_sample.py +0 -46
  52. featuremanagement-2.0.0b3/samples/feature_variant_sample.py +0 -25
  53. featuremanagement-2.0.0b3/samples/feature_variant_sample_with_telemetry.py +0 -36
  54. featuremanagement-2.0.0b3/samples/formatted_feature_flags.json +0 -229
  55. featuremanagement-2.0.0b3/samples/random_filter.py +0 -21
  56. featuremanagement-2.0.0b3/setup.py +0 -64
  57. featuremanagement-2.0.0b3/tests/test_send_telemetry_appinsights.py +0 -213
  58. featuremanagement-2.0.0b3/tests/validation_tests/test_json_validations.py +0 -101
  59. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/dependency_links.txt +0 -0
  60. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/FeatureManagement.egg-info/top_level.txt +0 -0
  61. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/LICENSE +0 -0
  62. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/README.md +0 -0
  63. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/featuremanagement/py.typed +0 -0
  64. {featuremanagement-2.0.0b3 → featuremanagement-2.2.0}/setup.cfg +0 -0
@@ -1,9 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: FeatureManagement
3
- Version: 2.0.0b3
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.6
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: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
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
 
@@ -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
1
+ Metadata-Version: 2.4
2
2
  Name: FeatureManagement
3
- Version: 2.0.0b3
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.6
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: azure-monitor-events-extension<2.0.0; extra == "azuremonitor"
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
- logging.warning("%s: At least one of Start or End is required.", TimeWindowFilter.__name__)
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 (start_time is None or start_time <= current_time) and (end_time is None or current_time < end_time)
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
- from typing import cast, overload, Any, Optional, Dict, Mapping, List
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:
@@ -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
- def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]:
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 TargetingContext()
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(self._configuration, feature_flag_id)
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
- logging.warning("Feature flag %s not found", feature_flag_id)
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
- return _list_feature_flag_names(self._configuration)
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
@@ -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
@@ -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"
@@ -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 (
@@ -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
 
@@ -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
+ """Enum for variant assignment reasons."""
7
+
6
8
  from enum import Enum
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"]