Skip to content

Commit b878601

Browse files
committed
Merge branch 'main' into gh-pages
2 parents d7ec43d + e6f53c9 commit b878601

38 files changed

Lines changed: 2070 additions & 276 deletions

.github/workflows/validate.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Validate
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.8", "3.9", "3.10"]
11+
steps:
12+
- uses: actions/checkout@v3
13+
- name: Set up Python ${{ matrix.python-version }}
14+
uses: actions/setup-python@v3
15+
with:
16+
python-version: ${{ matrix.python-version }}
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install -r dev_requirements.txt
21+
pip install .
22+
- name: Analysing the code/samples with pylint
23+
run: |
24+
pylint featuremanagement
25+
pylint --disable=missing-function-docstring,missing-class-docstring samples tests
26+
- uses: psf/black@stable
27+
- name: Test with pytest
28+
run: |
29+
pytest tests --doctest-modules --cov-report=xml --cov-report=html

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,16 @@ The Targeting Filter provides the capability to enable a feature for a target au
220220
You can provide the current user info through `kwargs` when calling `isEnabled`.
221221

222222
```python
223+
from featuremanagement import FeatureManager, TargetingContext
224+
223225
# Returns true, because user1 is in the Users list
224-
feature_manager.is_enabled("Beta", user="user1", groups=["group1"])
226+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user1", groups=["group1"]))
225227

226228
# Returns false, because group2 is in the Exclusion.Groups list
227-
feature_manager.is_enabled("Beta", user="user1", groups=["group2"])
229+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user1", groups=["group2"]))
228230

229231
# Has a 50% chance of returning true, but will be conisistent for the same user
230-
feature_manager.is_enabled("Beta", user="user4")
232+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user4"))
231233
```
232234

233235
#### Custom Filters

dev_requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
pytest
2+
pytest-cov
23
pytest-asyncio
34
black
45
pylint
56
mypy
67
sphinx
78
sphinx_rtd_theme
8-
myst_parser
9+
myst_parser
10+
azure-appconfiguration-provider

featuremanagement/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
from ._featuremanager import FeatureManager
77
from ._featurefilters import FeatureFilter
88
from ._defaultfilters import TimeWindowFilter, TargetingFilter
9+
from ._models import TargetingContext
910

1011
from ._version import VERSION
1112

1213
__version__ = VERSION
13-
__all__ = ["FeatureManager", "TimeWindowFilter", "TargetingFilter", "FeatureFilter"]
14+
__all__ = [
15+
"FeatureManager",
16+
"TimeWindowFilter",
17+
"TargetingFilter",
18+
"FeatureFilter",
19+
"TargetingContext",
20+
]

featuremanagement/_defaultfilters.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from ._featurefilters import FeatureFilter
1313

14-
FEATURE_FLAG_NAME_KEY = "name"
14+
FEATURE_FLAG_NAME_KEY = "feature_name"
1515
ROLLOUT_PERCENTAGE_KEY = "RolloutPercentage"
1616
DEFAULT_ROLLOUT_PERCENTAGE_KEY = "DefaultRolloutPercentage"
1717
PARAMETERS_KEY = "parameters"
@@ -35,23 +35,22 @@
3535

3636
class TargetingException(Exception):
3737
"""
38-
Exception raised when the targeting filter is not configured correctly
38+
Exception raised when the targeting filter is not configured correctly.
3939
"""
4040

4141

4242
@FeatureFilter.alias("Microsoft.TimeWindow")
4343
class TimeWindowFilter(FeatureFilter):
4444
"""
45-
Feature Filter that determines if the current time is within the time window
45+
Feature Filter that determines if the current time is within the time window.
4646
"""
4747

4848
def evaluate(self, context, **kwargs):
4949
"""
50-
Determine if the feature flag is enabled for the given context
50+
Determine if the feature flag is enabled for the given context.
5151
52-
:keyword Mapping context: Mapping with the Start and End time for the feature flag
53-
:paramtype context: Mapping
54-
:return: True if the current time is within the time window
52+
:keyword Mapping context: Mapping with the Start and End time for the feature flag.
53+
:return: True if the current time is within the time window.
5554
:rtype: bool
5655
"""
5756
start = context.get(PARAMETERS_KEY, {}).get(START_KEY)
@@ -72,7 +71,7 @@ def evaluate(self, context, **kwargs):
7271
@FeatureFilter.alias("Microsoft.Targeting")
7372
class TargetingFilter(FeatureFilter):
7473
"""
75-
Feature Filter that determines if the user is targeted for the feature flag
74+
Feature Filter that determines if the user is targeted for the feature flag.
7675
"""
7776

7877
@staticmethod
@@ -82,27 +81,26 @@ def _is_targeted(context_id, rollout_percentage):
8281
if rollout_percentage == 100:
8382
return True
8483

85-
hashed_context_id = hashlib.sha256(context_id.encode()).hexdigest()
86-
context_marker = abs(int(hashed_context_id, 16))
87-
percentage = (context_marker / (2**256 - 1)) * 100
84+
hashed_context_id = hashlib.sha256(context_id.encode()).digest()
85+
context_marker = int.from_bytes(hashed_context_id[:4], byteorder="little", signed=False)
8886

87+
percentage = (context_marker / (2**32 - 1)) * 100
8988
return percentage < rollout_percentage
9089

9190
def _target_group(self, target_user, target_group, group, feature_flag_name):
9291
group_rollout_percentage = group.get(ROLLOUT_PERCENTAGE_KEY, 0)
93-
audience_context_id = (
94-
target_user + "\n" + target_group + "\n" + feature_flag_name + "\n" + group.get(FEATURE_FILTER_NAME_KEY, "")
95-
)
92+
if not target_user:
93+
target_user = ""
94+
audience_context_id = target_user + "\n" + feature_flag_name + "\n" + target_group
9695

9796
return self._is_targeted(audience_context_id, group_rollout_percentage)
9897

9998
def evaluate(self, context, **kwargs):
10099
"""
101-
Determine if the feature flag is enabled for the given context
100+
Determine if the feature flag is enabled for the given context.
102101
103-
:keyword Mapping context: Context for evaluating the user/group
104-
:paramtype context: Mapping
105-
:return: True if the user is targeted for the feature flag
102+
:keyword Mapping context: Context for evaluating the user/group.
103+
:return: True if the user is targeted for the feature flag.
106104
:rtype: bool
107105
"""
108106
target_user = kwargs.get(TARGETED_USER_KEY, None)
@@ -129,7 +127,7 @@ def evaluate(self, context, **kwargs):
129127

130128
# Check if the user is in an excluded group
131129
for group in audience.get(EXCLUSION_KEY, {}).get(GROUPS_KEY, []):
132-
if group.get(FEATURE_FILTER_NAME_KEY) in target_groups:
130+
if group in target_groups:
133131
return False
134132

135133
# Check if the user is targeted
@@ -147,6 +145,8 @@ def evaluate(self, context, **kwargs):
147145
if self._target_group(target_user, target_group, group, feature_flag_name):
148146
return True
149147

148+
if not target_user:
149+
target_user = ""
150150
# Check if the user is in the default rollout
151151
context_id = target_user + "\n" + feature_flag_name
152152
return self._is_targeted(context_id, default_rollout_percentage)

featuremanagement/_featurefilters.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,23 @@
88

99
class FeatureFilter(ABC):
1010
"""
11-
Parent class for all feature filters
11+
Parent class for all feature filters.
1212
"""
1313

1414
@abstractmethod
1515
def evaluate(self, context, **kwargs):
1616
"""
17-
Determine if the feature flag is enabled for the given context
18-
:param Mapping context: Context for the feature flag
19-
:paramtype context: Mapping
17+
Determine if the feature flag is enabled for the given context.
18+
19+
:param Mapping context: Context for the feature flag.
2020
"""
2121

2222
@property
2323
def name(self):
2424
"""
25-
Get the name of the filter
26-
:return: Name of the filter, or alias if it exists
25+
Get the name of the filter.
26+
27+
:return: Name of the filter, or alias if it exists.
2728
:rtype: str
2829
"""
2930
if hasattr(self, "_alias"):
@@ -32,8 +33,16 @@ def name(self):
3233

3334
@staticmethod
3435
def alias(alias):
36+
"""
37+
Decorator to set the alias for the filter.
38+
39+
:param str alias: Alias for the filter.
40+
:return: Decorator.
41+
:rtype: callable
42+
"""
43+
3544
def wrapper(cls):
36-
cls._alias = alias
45+
cls._alias = alias # pylint: disable=protected-access
3746
return cls
3847

3948
return wrapper

0 commit comments

Comments
 (0)