FeatureManagement 1.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- FeatureManagement-1.0.0b1.dist-info/LICENSE +21 -0
- FeatureManagement-1.0.0b1.dist-info/METADATA +340 -0
- FeatureManagement-1.0.0b1.dist-info/RECORD +21 -0
- FeatureManagement-1.0.0b1.dist-info/WHEEL +5 -0
- FeatureManagement-1.0.0b1.dist-info/top_level.txt +1 -0
- featuremanagement/__init__.py +20 -0
- featuremanagement/_defaultfilters.py +162 -0
- featuremanagement/_featurefilters.py +48 -0
- featuremanagement/_featuremanager.py +190 -0
- featuremanagement/_models/__init__.py +12 -0
- featuremanagement/_models/_constants.py +18 -0
- featuremanagement/_models/_evaluation_event.py +23 -0
- featuremanagement/_models/_feature_conditions.py +70 -0
- featuremanagement/_models/_feature_flag.py +102 -0
- featuremanagement/_models/_targeting_context.py +27 -0
- featuremanagement/_version.py +7 -0
- featuremanagement/aio/__init__.py +10 -0
- featuremanagement/aio/_defaultfilters.py +51 -0
- featuremanagement/aio/_featurefilters.py +48 -0
- featuremanagement/aio/_featuremanager.py +142 -0
- featuremanagement/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: FeatureManagement
|
|
3
|
+
Version: 1.0.0b1
|
|
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
|
+
Author-email: Microsoft Corporation <appconfig@microsoft.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) Microsoft Corporation.
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/microsoft/FeatureManagement-Python
|
|
31
|
+
Project-URL: Issues, https://github.com/microsoft/FeatureManagement-Python/issues
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Programming Language :: Python
|
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
41
|
+
Requires-Python: >=3.6
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
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'
|
|
47
|
+
|
|
48
|
+
# Microsoft Feature Management for Python
|
|
49
|
+
|
|
50
|
+
Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.
|
|
51
|
+
|
|
52
|
+
## Getting started
|
|
53
|
+
|
|
54
|
+
### Prerequisites
|
|
55
|
+
|
|
56
|
+
* Python 3.7 or later is required to use this package.
|
|
57
|
+
|
|
58
|
+
### Install the package
|
|
59
|
+
|
|
60
|
+
Install the Python feature management client library for Python with [pip][pip]:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install microsoft-featuremanagement
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
You can use feature flags from the Azure App Configuration service, a json file, or a dictionary.
|
|
69
|
+
|
|
70
|
+
### Use feature flags from Azure App Configuration
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from featuremanagement import FeatureManager
|
|
74
|
+
from azure.appconfiguration.provider import load
|
|
75
|
+
from azure.identity import DefaultAzureCredential
|
|
76
|
+
import os
|
|
77
|
+
|
|
78
|
+
endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING")
|
|
79
|
+
|
|
80
|
+
# If no setting selector is set then feature flags with no label are loaded.
|
|
81
|
+
selects = {SettingSelector(key_filter=".appconfig.featureflag*")}
|
|
82
|
+
|
|
83
|
+
config = load(endpoint=endpoint, credential=DefaultAzureCredential(), selects=selects)
|
|
84
|
+
|
|
85
|
+
feature_manager = FeatureManager(config)
|
|
86
|
+
|
|
87
|
+
# Prints the value of the feature flag Alpha
|
|
88
|
+
print("Alpha is ", feature_manager.is_enabled("Alpha"))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Use feature flags from a json file
|
|
92
|
+
|
|
93
|
+
A Json file with the following format can be used to load feature flags.
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"feature_management": {
|
|
98
|
+
"feature_flags": [
|
|
99
|
+
{
|
|
100
|
+
"id": "Alpha",
|
|
101
|
+
"description": "",
|
|
102
|
+
"enabled": "true",
|
|
103
|
+
"conditions": {
|
|
104
|
+
"client_filters": []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Load feature flags from a json file.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from featuremanagement import FeatureManager
|
|
116
|
+
import json
|
|
117
|
+
import os
|
|
118
|
+
import sys
|
|
119
|
+
|
|
120
|
+
script_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
121
|
+
|
|
122
|
+
f = open(script_directory + "/my_json_file.json", "r")
|
|
123
|
+
|
|
124
|
+
feature_flags = json.load(f)
|
|
125
|
+
|
|
126
|
+
feature_manager = FeatureManager(feature_flags)
|
|
127
|
+
|
|
128
|
+
# Returns the value of Alpha, based on the result of the feature filter
|
|
129
|
+
print("Alpha is ", feature_manager.is_enabled("Alpha"))
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Use feature flags from a dictionary
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from featuremanagement import FeatureManager
|
|
136
|
+
|
|
137
|
+
feature_flags = {
|
|
138
|
+
"feature_management": {
|
|
139
|
+
"feature_flags": [
|
|
140
|
+
{
|
|
141
|
+
"id": "Alpha",
|
|
142
|
+
"description": "",
|
|
143
|
+
"enabled": "true",
|
|
144
|
+
"conditions": {
|
|
145
|
+
"client_filters": []
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
feature_manager = FeatureManager(feature_flags)
|
|
153
|
+
|
|
154
|
+
# Is always true
|
|
155
|
+
print("Alpha is ", feature_manager.is_enabled("Alpha"))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Key concepts
|
|
159
|
+
|
|
160
|
+
### FeatureManager
|
|
161
|
+
|
|
162
|
+
The `FeatureManager` is the main entry point for using feature flags. It is initialized with a dictionary of feature flags, and optional feature filters. The `FeatureManager` can then be used to check if a feature is enabled or disabled.
|
|
163
|
+
|
|
164
|
+
### Feature Flags
|
|
165
|
+
|
|
166
|
+
Feature Flags are objects that define how Feature Management enables/disables a feature. It contains an `id` and `enabled` property. The `id` is a string that uniquely identifies the feature flag. The `enabled` property is a boolean that indicates if the feature flag is enabled or disabled. The `conditions` object contains a property `client_filters` which is a list of `FeatureFilter` objects that are used to determine if the feature flag is enabled or disabled. The Feature Filters only run if the feature flag is enabled.
|
|
167
|
+
|
|
168
|
+
The full schema for a feature Flag can be found [here](https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json).
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
{
|
|
172
|
+
"id": "Alpha",
|
|
173
|
+
"enabled": "true",
|
|
174
|
+
"conditions": {
|
|
175
|
+
"client_filters": [
|
|
176
|
+
{
|
|
177
|
+
"name": "MyFilter",
|
|
178
|
+
"parameters": {
|
|
179
|
+
...
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This object is passed into the `FeatureManager` when it is initialized.
|
|
188
|
+
|
|
189
|
+
### Feature Filters
|
|
190
|
+
|
|
191
|
+
Feature filters enable dynamic evaluation of feature flags. The Python feature management library includes two built-in filters:
|
|
192
|
+
|
|
193
|
+
- `Microsoft.TimeWindow` - Enables a feature flag based on a time window.
|
|
194
|
+
- `Microsoft.Targeting` - Enables a feature flag based on a list of users, groups, or rollout percentages.
|
|
195
|
+
|
|
196
|
+
#### Time Window Filter
|
|
197
|
+
|
|
198
|
+
The Time Window Filter enables a feature flag based on a time window. It has two parameters:
|
|
199
|
+
|
|
200
|
+
- `Start` - The start time of the time window.
|
|
201
|
+
- `End` - The end time of the time window.
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"name": "Microsoft.TimeWindow",
|
|
206
|
+
"parameters": {
|
|
207
|
+
"Start": "2020-01-01T00:00:00Z",
|
|
208
|
+
"End": "2020-12-31T00:00:00Z"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Both parameters are optional, but at least one is required. The time window filter is enabled after the start time and before the end time. If the start time is not specified, it is enabled immediately. If the end time is not specified, it will remain enabled after the start time.
|
|
214
|
+
|
|
215
|
+
#### Targeting Filter
|
|
216
|
+
|
|
217
|
+
Targeting is a feature management strategy that enables developers to progressively roll out new features to their user base. The strategy is built on the concept of targeting a set of users known as the target audience. An audience is made up of specific users, groups, excluded users/groups, and a designated percentage of the entire user base. The groups that are included in the audience can be broken down further into percentages of their total members.
|
|
218
|
+
|
|
219
|
+
The following steps demonstrate an example of a progressive rollout for a new 'Beta' feature:
|
|
220
|
+
|
|
221
|
+
1. Individual users Jeff and Alicia are granted access to the Beta
|
|
222
|
+
1. Another user, Mark, asks to opt-in and is included.
|
|
223
|
+
1. Twenty percent of a group known as "Ring1" users are included in the Beta.
|
|
224
|
+
1. The number of "Ring1" users included in the beta is bumped up to 100 percent.
|
|
225
|
+
1. Five percent of the user base is included in the beta.
|
|
226
|
+
1. The rollout percentage is bumped up to 100 percent and the feature is completely rolled out.
|
|
227
|
+
|
|
228
|
+
This strategy for rolling out a feature is built in to the library through the included Microsoft.Targeting feature filter.
|
|
229
|
+
|
|
230
|
+
##### Defining a Targeting Feature Filter
|
|
231
|
+
|
|
232
|
+
The Targeting Filter provides the capability to enable a feature for a target audience. The filter parameters include an `Audience` object which describes users, groups, excluded users/groups, and a default percentage of the user base that should have access to the feature. The `Audience` object contains the following fields:
|
|
233
|
+
|
|
234
|
+
- `Users` - A list of users that the feature flag is enabled for.
|
|
235
|
+
- `Groups` - A list of groups that the feature flag is enabled for and a rollout percentage for each group.
|
|
236
|
+
- `Name` - The name of the group.
|
|
237
|
+
- `RolloutPercentage` - A percentage value that the feature flag is enabled for in the given group.
|
|
238
|
+
- `DefaultRolloutPercentage` - A percentage value that the feature flag is enabled for.
|
|
239
|
+
- `Exclusion` - An object that contains a list of users and groups that the feature flag is disabled for.
|
|
240
|
+
- `Users` - A list of users that the feature flag is disabled for.
|
|
241
|
+
- `Groups` - A list of groups that the feature flag is disabled for.
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"name": "Microsoft.Targeting",
|
|
246
|
+
"parameters": {
|
|
247
|
+
"Audience": {
|
|
248
|
+
"Users": ["user1", "user2"],
|
|
249
|
+
"Groups": [
|
|
250
|
+
{
|
|
251
|
+
"Name": "group1",
|
|
252
|
+
"RolloutPercentage": 100
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"DefaultRolloutPercentage": 50,
|
|
256
|
+
"Exclusion": {
|
|
257
|
+
"Users": ["user3"],
|
|
258
|
+
"Groups": ["group2"]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
##### Using Targeting Feature Filter
|
|
266
|
+
|
|
267
|
+
You can provide the current user info through `kwargs` when calling `isEnabled`.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from featuremanagement import FeatureManager, TargetingContext
|
|
271
|
+
|
|
272
|
+
# Returns true, because user1 is in the Users list
|
|
273
|
+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user1", groups=["group1"]))
|
|
274
|
+
|
|
275
|
+
# Returns false, because group2 is in the Exclusion.Groups list
|
|
276
|
+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user1", groups=["group2"]))
|
|
277
|
+
|
|
278
|
+
# Has a 50% chance of returning true, but will be conisistent for the same user
|
|
279
|
+
feature_manager.is_enabled("Beta", TargetingContext(user_id="user4"))
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Custom Filters
|
|
283
|
+
|
|
284
|
+
You can also create your own feature filters by implementing the `FeatureFilter` interface.
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
class MyCustomFilter(FeatureFilter):
|
|
288
|
+
|
|
289
|
+
def evaluate(self, context, **kwargs):
|
|
290
|
+
...
|
|
291
|
+
return True
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
They can then be passed into the `FeatureManager` when it is initialized. By default, the name of a feature filter is the name of the class. You can override this by setting a class attribute `alias` to the modified class name.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
|
|
298
|
+
feature_manager = FeatureManager(feature_flags, feature_filters={MyCustomFilter(), MyOtherFilter()})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The `evaluate` method is called when checking if a feature flag is enabled. The `context` parameter contains information about the feature filter from the `parameters` field of the feature filter. Any additional parameters can be passed in as keyword arguments when calling `is_enabled`.
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
{
|
|
305
|
+
"name": "CustomFilter",
|
|
306
|
+
"parameters": {
|
|
307
|
+
...
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
You can modify the name of a feature flag by using the `@FeatureFilter.alias` decorator. The alias overrides the name of the feature filter and needs to match the name of the feature filter in the feature flag json.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
@FeatureFilter.alias("AliasFilter")
|
|
316
|
+
class MyCustomFilter(FeatureFilter):
|
|
317
|
+
...
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Contributing
|
|
321
|
+
|
|
322
|
+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
|
323
|
+
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
|
324
|
+
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
|
325
|
+
|
|
326
|
+
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
|
327
|
+
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
|
328
|
+
provided by the bot. You will only need to do this once across all repos using our CLA.
|
|
329
|
+
|
|
330
|
+
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
331
|
+
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
|
332
|
+
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
333
|
+
|
|
334
|
+
## Trademarks
|
|
335
|
+
|
|
336
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
337
|
+
trademarks or logos is subject to and must follow
|
|
338
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
339
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
340
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
featuremanagement/__init__.py,sha256=h5PFIFzLm68Gt_KDtJeTgdh88XwrOUZvV5v9Clpd8qM,678
|
|
2
|
+
featuremanagement/_defaultfilters.py,sha256=NAc85ckoD3gfnl4SXvRZNQ9gN6iIPwOz4vJX_PHJv9M,6121
|
|
3
|
+
featuremanagement/_featurefilters.py,sha256=xuvXzw4p6ECkHavDpnnvedJhvGLhTHtEWK4kdwKhM_M,1294
|
|
4
|
+
featuremanagement/_featuremanager.py,sha256=uDMbl2w0SSt7isBRnY0P0ScaMXzqC86D5yl-CZQX40E,7661
|
|
5
|
+
featuremanagement/_version.py,sha256=IZPHLteR881KNqqgCCcB8hknN83UrwORIDn87-TUlAU,329
|
|
6
|
+
featuremanagement/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
featuremanagement/_models/__init__.py,sha256=S-xzmIgVGw7GO1lZGob8h_9WBXpXFNg98kJypZ8uSbk,591
|
|
8
|
+
featuremanagement/_models/_constants.py,sha256=puNDkZHUoB2eCOQ4CeP4LL9xwogPdIP9uRAmTpOSM9c,621
|
|
9
|
+
featuremanagement/_models/_evaluation_event.py,sha256=p2OGowvhY5cSHs6LhpsziOAtztn29Z5EwGWg1OECtDg,713
|
|
10
|
+
featuremanagement/_models/_feature_conditions.py,sha256=hNEslIhv1Zdiue4mXhuLZNg0uQdw5deyhoFAgCcgxX0,2482
|
|
11
|
+
featuremanagement/_models/_feature_flag.py,sha256=JC4AW7BsL17YELJ4cVZSRjnYJPFODOGqKgBmg10Ik0I,3139
|
|
12
|
+
featuremanagement/_models/_targeting_context.py,sha256=pD8yc7dO94WcC0TKW_Z3pGoMuDEv8hygXVlVd6lrpFI,626
|
|
13
|
+
featuremanagement/aio/__init__.py,sha256=-7pTXT_eszJr2Mc34lTS1ICuI67xZ9lDpZ0C01uKbCY,544
|
|
14
|
+
featuremanagement/aio/_defaultfilters.py,sha256=QCrwlzH0738o2nTDmj8XXPgcCrAmxHM8HB11qVJqawA,1732
|
|
15
|
+
featuremanagement/aio/_featurefilters.py,sha256=eF7JXO9Sdxu8rIZivOyWQIc8NDaK-FBGMTEOvQcwqAo,1305
|
|
16
|
+
featuremanagement/aio/_featuremanager.py,sha256=i2LyqTByT5FRGq_3txuondUKufp6g3KQC6H4Pan7oOQ,6141
|
|
17
|
+
FeatureManagement-1.0.0b1.dist-info/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
|
18
|
+
FeatureManagement-1.0.0b1.dist-info/METADATA,sha256=VgiORco0Cy1rLDejYXQQ1kPIO8Rs2kGh-9IKOqns9n8,13556
|
|
19
|
+
FeatureManagement-1.0.0b1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
20
|
+
FeatureManagement-1.0.0b1.dist-info/top_level.txt,sha256=S0iNu3dPTzNkYFDt3OHOEQXs-gQf1cQaw4Usl7kgPdI,18
|
|
21
|
+
FeatureManagement-1.0.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
featuremanagement
|
|
@@ -0,0 +1,20 @@
|
|
|
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 ._featuremanager import FeatureManager
|
|
7
|
+
from ._featurefilters import FeatureFilter
|
|
8
|
+
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
9
|
+
from ._models import TargetingContext
|
|
10
|
+
|
|
11
|
+
from ._version import VERSION
|
|
12
|
+
|
|
13
|
+
__version__ = VERSION
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FeatureManager",
|
|
16
|
+
"TimeWindowFilter",
|
|
17
|
+
"TargetingFilter",
|
|
18
|
+
"FeatureFilter",
|
|
19
|
+
"TargetingContext",
|
|
20
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
import logging
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from email.utils import parsedate_to_datetime
|
|
11
|
+
|
|
12
|
+
from ._featurefilters import FeatureFilter
|
|
13
|
+
|
|
14
|
+
FEATURE_FLAG_NAME_KEY = "feature_name"
|
|
15
|
+
ROLLOUT_PERCENTAGE_KEY = "RolloutPercentage"
|
|
16
|
+
DEFAULT_ROLLOUT_PERCENTAGE_KEY = "DefaultRolloutPercentage"
|
|
17
|
+
PARAMETERS_KEY = "parameters"
|
|
18
|
+
|
|
19
|
+
# Time Window Constants
|
|
20
|
+
START_KEY = "Start"
|
|
21
|
+
END_KEY = "End"
|
|
22
|
+
|
|
23
|
+
# Targeting kwargs
|
|
24
|
+
TARGETED_USER_KEY = "user"
|
|
25
|
+
TARGETED_GROUPS_KEY = "groups"
|
|
26
|
+
|
|
27
|
+
# Targeting Constants
|
|
28
|
+
AUDIENCE_KEY = "Audience"
|
|
29
|
+
USERS_KEY = "Users"
|
|
30
|
+
GROUPS_KEY = "Groups"
|
|
31
|
+
EXCLUSION_KEY = "Exclusion"
|
|
32
|
+
FEATURE_FILTER_NAME_KEY = "Name"
|
|
33
|
+
IGNORE_CASE_KEY = "ignore_case"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TargetingException(Exception):
|
|
37
|
+
"""
|
|
38
|
+
Exception raised when the targeting filter is not configured correctly.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@FeatureFilter.alias("Microsoft.TimeWindow")
|
|
43
|
+
class TimeWindowFilter(FeatureFilter):
|
|
44
|
+
"""
|
|
45
|
+
Feature Filter that determines if the current time is within the time window.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def evaluate(self, context, **kwargs):
|
|
49
|
+
"""
|
|
50
|
+
Determine if the feature flag is enabled for the given context.
|
|
51
|
+
|
|
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.
|
|
54
|
+
:rtype: bool
|
|
55
|
+
"""
|
|
56
|
+
start = context.get(PARAMETERS_KEY, {}).get(START_KEY)
|
|
57
|
+
end = context.get(PARAMETERS_KEY, {}).get(END_KEY)
|
|
58
|
+
|
|
59
|
+
current_time = datetime.now(timezone.utc)
|
|
60
|
+
|
|
61
|
+
if not start and not end:
|
|
62
|
+
logging.warning("%s: At least one of Start or End is required.", TimeWindowFilter.__name__)
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
start_time = parsedate_to_datetime(start) if start else None
|
|
66
|
+
end_time = parsedate_to_datetime(end) if end else None
|
|
67
|
+
|
|
68
|
+
return (start_time is None or start_time <= current_time) and (end_time is None or current_time < end_time)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@FeatureFilter.alias("Microsoft.Targeting")
|
|
72
|
+
class TargetingFilter(FeatureFilter):
|
|
73
|
+
"""
|
|
74
|
+
Feature Filter that determines if the user is targeted for the feature flag.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _is_targeted(context_id, rollout_percentage):
|
|
79
|
+
"""Determine if the user is targeted for the given context"""
|
|
80
|
+
# Always return true if rollout percentage is 100
|
|
81
|
+
if rollout_percentage == 100:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
hashed_context_id = hashlib.sha256(context_id.encode()).digest()
|
|
85
|
+
context_marker = int.from_bytes(hashed_context_id[:4], byteorder="little", signed=False)
|
|
86
|
+
|
|
87
|
+
percentage = (context_marker / (2**32 - 1)) * 100
|
|
88
|
+
return percentage < rollout_percentage
|
|
89
|
+
|
|
90
|
+
def _target_group(self, target_user, target_group, group, feature_flag_name):
|
|
91
|
+
group_rollout_percentage = group.get(ROLLOUT_PERCENTAGE_KEY, 0)
|
|
92
|
+
if not target_user:
|
|
93
|
+
target_user = ""
|
|
94
|
+
audience_context_id = target_user + "\n" + feature_flag_name + "\n" + target_group
|
|
95
|
+
|
|
96
|
+
return self._is_targeted(audience_context_id, group_rollout_percentage)
|
|
97
|
+
|
|
98
|
+
def evaluate(self, context, **kwargs):
|
|
99
|
+
"""
|
|
100
|
+
Determine if the feature flag is enabled for the given context.
|
|
101
|
+
|
|
102
|
+
:keyword Mapping context: Context for evaluating the user/group.
|
|
103
|
+
:return: True if the user is targeted for the feature flag.
|
|
104
|
+
:rtype: bool
|
|
105
|
+
"""
|
|
106
|
+
target_user = kwargs.get(TARGETED_USER_KEY, None)
|
|
107
|
+
target_groups = kwargs.get(TARGETED_GROUPS_KEY, [])
|
|
108
|
+
|
|
109
|
+
if not target_user and not (target_groups and len(target_groups) > 0):
|
|
110
|
+
logging.warning("%s: Name or Groups are required parameters", TargetingFilter.__name__)
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
audience = context.get(PARAMETERS_KEY, {}).get(AUDIENCE_KEY, None)
|
|
114
|
+
feature_flag_name = context.get(FEATURE_FLAG_NAME_KEY, None)
|
|
115
|
+
|
|
116
|
+
if not audience:
|
|
117
|
+
raise TargetingException("Audience is required for " + TargetingFilter.__name__)
|
|
118
|
+
|
|
119
|
+
groups = audience.get(GROUPS_KEY, [])
|
|
120
|
+
default_rollout_percentage = audience.get(DEFAULT_ROLLOUT_PERCENTAGE_KEY, 0)
|
|
121
|
+
|
|
122
|
+
self._validate(groups, default_rollout_percentage)
|
|
123
|
+
|
|
124
|
+
# Check if the user is excluded
|
|
125
|
+
if target_user in audience.get(EXCLUSION_KEY, {}).get(USERS_KEY, []):
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
# Check if the user is in an excluded group
|
|
129
|
+
for group in audience.get(EXCLUSION_KEY, {}).get(GROUPS_KEY, []):
|
|
130
|
+
if group in target_groups:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# Check if the user is targeted
|
|
134
|
+
if target_user in audience.get(USERS_KEY, []):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
# Check if the user is in a targeted group
|
|
138
|
+
for group in groups:
|
|
139
|
+
for target_group in target_groups:
|
|
140
|
+
group_name = group.get(FEATURE_FILTER_NAME_KEY, "")
|
|
141
|
+
if kwargs.get(IGNORE_CASE_KEY, False):
|
|
142
|
+
target_group = target_group.lower()
|
|
143
|
+
group_name = group_name.lower()
|
|
144
|
+
if group_name == target_group:
|
|
145
|
+
if self._target_group(target_user, target_group, group, feature_flag_name):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
if not target_user:
|
|
149
|
+
target_user = ""
|
|
150
|
+
# Check if the user is in the default rollout
|
|
151
|
+
context_id = target_user + "\n" + feature_flag_name
|
|
152
|
+
return self._is_targeted(context_id, default_rollout_percentage)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _validate(groups, default_rollout_percentage):
|
|
156
|
+
# Validate the audience settings
|
|
157
|
+
if default_rollout_percentage < 0 or default_rollout_percentage > 100:
|
|
158
|
+
raise TargetingException("DefaultRolloutPercentage must be between 0 and 100")
|
|
159
|
+
|
|
160
|
+
for group in groups:
|
|
161
|
+
if group.get(ROLLOUT_PERCENTAGE_KEY) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY) > 100:
|
|
162
|
+
raise TargetingException("RolloutPercentage must be between 0 and 100")
|
|
@@ -0,0 +1,48 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FeatureFilter(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Parent class for all feature filters.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def evaluate(self, context, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Determine if the feature flag is enabled for the given context.
|
|
18
|
+
|
|
19
|
+
:param Mapping context: Context for the feature flag.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self):
|
|
24
|
+
"""
|
|
25
|
+
Get the name of the filter.
|
|
26
|
+
|
|
27
|
+
:return: Name of the filter, or alias if it exists.
|
|
28
|
+
:rtype: str
|
|
29
|
+
"""
|
|
30
|
+
if hasattr(self, "_alias"):
|
|
31
|
+
return self._alias
|
|
32
|
+
return self.__class__.__name__
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
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
|
+
|
|
44
|
+
def wrapper(cls):
|
|
45
|
+
cls._alias = alias # pylint: disable=protected-access
|
|
46
|
+
return cls
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
import logging
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import overload
|
|
9
|
+
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
10
|
+
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:
|
|
70
|
+
"""
|
|
71
|
+
Feature Manager that determines if a feature flag is enabled for the given context.
|
|
72
|
+
|
|
73
|
+
:param Mapping configuration: Configuration object.
|
|
74
|
+
:keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
|
|
75
|
+
"""
|
|
76
|
+
|
|
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, [])
|
|
85
|
+
|
|
86
|
+
for feature_filter in filters:
|
|
87
|
+
if not isinstance(feature_filter, FeatureFilter):
|
|
88
|
+
raise ValueError("Custom filter must be a subclass of FeatureFilter")
|
|
89
|
+
self._filters[feature_filter.name] = feature_filter
|
|
90
|
+
|
|
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):
|
|
107
|
+
"""
|
|
108
|
+
Determine if the feature flag is enabled for the given context.
|
|
109
|
+
|
|
110
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
111
|
+
:param str user_id: User identifier.
|
|
112
|
+
:return: True if the feature flag is enabled for the given context.
|
|
113
|
+
:rtype: bool
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def is_enabled(self, feature_flag_id, *args, **kwargs):
|
|
117
|
+
"""
|
|
118
|
+
Determine if the feature flag is enabled for the given context.
|
|
119
|
+
|
|
120
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
121
|
+
:return: True if the feature flag is enabled for the given context.
|
|
122
|
+
:rtype: bool
|
|
123
|
+
"""
|
|
124
|
+
targeting_context = self._build_targeting_context(args)
|
|
125
|
+
|
|
126
|
+
result = self._check_feature(feature_flag_id, targeting_context, **kwargs)
|
|
127
|
+
return result.enabled
|
|
128
|
+
|
|
129
|
+
def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
|
|
130
|
+
feature_conditions = feature_flag.conditions
|
|
131
|
+
feature_filters = feature_conditions.client_filters
|
|
132
|
+
evaluation_event = EvaluationEvent(enabled=False)
|
|
133
|
+
|
|
134
|
+
if len(feature_filters) == 0:
|
|
135
|
+
# Feature flags without any filters return evaluate
|
|
136
|
+
evaluation_event.enabled = True
|
|
137
|
+
else:
|
|
138
|
+
# The assumed value is no filters is based on the requirement type.
|
|
139
|
+
# Requirement type Any assumes false until proven true, All assumes true until proven false
|
|
140
|
+
evaluation_event.enabled = feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL
|
|
141
|
+
|
|
142
|
+
for feature_filter in feature_filters:
|
|
143
|
+
filter_name = feature_filter[FEATURE_FILTER_NAME]
|
|
144
|
+
kwargs["user"] = targeting_context.user_id
|
|
145
|
+
kwargs["groups"] = targeting_context.groups
|
|
146
|
+
if filter_name not in self._filters:
|
|
147
|
+
raise ValueError(f"Feature flag {feature_flag.name} has unknown filter {filter_name}")
|
|
148
|
+
if feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL:
|
|
149
|
+
if not self._filters[filter_name].evaluate(feature_filter, **kwargs):
|
|
150
|
+
evaluation_event.enabled = False
|
|
151
|
+
break
|
|
152
|
+
elif self._filters[filter_name].evaluate(feature_filter, **kwargs):
|
|
153
|
+
evaluation_event.enabled = True
|
|
154
|
+
break
|
|
155
|
+
return evaluation_event
|
|
156
|
+
|
|
157
|
+
def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
|
|
158
|
+
"""
|
|
159
|
+
Determine if the feature flag is enabled for the given context.
|
|
160
|
+
|
|
161
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
162
|
+
:return: True if the feature flag is enabled for the given context.
|
|
163
|
+
:rtype: bool
|
|
164
|
+
"""
|
|
165
|
+
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
|
|
166
|
+
self._cache = {}
|
|
167
|
+
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
168
|
+
|
|
169
|
+
if not self._cache.get(feature_flag_id):
|
|
170
|
+
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
171
|
+
self._cache[feature_flag_id] = feature_flag
|
|
172
|
+
else:
|
|
173
|
+
feature_flag = self._cache.get(feature_flag_id)
|
|
174
|
+
|
|
175
|
+
if not feature_flag:
|
|
176
|
+
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
177
|
+
# Unknown feature flags are disabled by default
|
|
178
|
+
return EvaluationEvent(enabled=False)
|
|
179
|
+
|
|
180
|
+
if not feature_flag.enabled:
|
|
181
|
+
# Feature flags that are disabled are always disabled
|
|
182
|
+
return EvaluationEvent(enabled=False)
|
|
183
|
+
|
|
184
|
+
return self._check_feature_filters(feature_flag, targeting_context, **kwargs)
|
|
185
|
+
|
|
186
|
+
def list_feature_flag_names(self):
|
|
187
|
+
"""
|
|
188
|
+
List of all feature flag names.
|
|
189
|
+
"""
|
|
190
|
+
return _list_feature_flag_names(self._configuration)
|
|
@@ -0,0 +1,12 @@
|
|
|
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 ._feature_flag import FeatureFlag
|
|
7
|
+
from ._evaluation_event import EvaluationEvent
|
|
8
|
+
from ._targeting_context import TargetingContext
|
|
9
|
+
|
|
10
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
|
|
11
|
+
|
|
12
|
+
__all__ = ["FeatureFlag", "EvaluationEvent", "TargetingContext"]
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
|
|
7
|
+
# Feature Flag
|
|
8
|
+
FEATURE_FLAG_ID = "id"
|
|
9
|
+
FEATURE_FLAG_ENABLED = "enabled"
|
|
10
|
+
FEATURE_FLAG_CONDITIONS = "conditions"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Conditions
|
|
14
|
+
FEATURE_FILTER_REQUIREMENT_TYPE = "requirement_type"
|
|
15
|
+
REQUIREMENT_TYPE_ALL = "All"
|
|
16
|
+
REQUIREMENT_TYPE_ANY = "Any"
|
|
17
|
+
FEATURE_FLAG_CLIENT_FILTERS = "client_filters"
|
|
18
|
+
FEATURE_FILTER_NAME = "name"
|
|
@@ -0,0 +1,23 @@
|
|
|
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 dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class EvaluationEvent:
|
|
11
|
+
"""
|
|
12
|
+
Represents a feature flag evaluation event.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *, enabled=False, feature_flag=None):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the EvaluationEvent.
|
|
18
|
+
"""
|
|
19
|
+
self.feature = feature_flag
|
|
20
|
+
self.user = ""
|
|
21
|
+
self.enabled = enabled
|
|
22
|
+
self.variant = None
|
|
23
|
+
self.reason = None
|
|
@@ -0,0 +1,70 @@
|
|
|
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 collections.abc import Mapping
|
|
7
|
+
from ._constants import (
|
|
8
|
+
FEATURE_FLAG_CLIENT_FILTERS,
|
|
9
|
+
FEATURE_FILTER_NAME,
|
|
10
|
+
FEATURE_FILTER_REQUIREMENT_TYPE,
|
|
11
|
+
REQUIREMENT_TYPE_ALL,
|
|
12
|
+
REQUIREMENT_TYPE_ANY,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FeatureConditions:
|
|
17
|
+
"""
|
|
18
|
+
Represents the conditions for a feature flag.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._requirement_type = REQUIREMENT_TYPE_ANY
|
|
23
|
+
self._client_filters = []
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def convert_from_json(cls, feature_name, json_value):
|
|
27
|
+
"""
|
|
28
|
+
Convert a JSON object to FeatureConditions.
|
|
29
|
+
|
|
30
|
+
:param dict json: JSON object.
|
|
31
|
+
:return: FeatureConditions.
|
|
32
|
+
:rtype: FeatureConditions
|
|
33
|
+
"""
|
|
34
|
+
conditions = cls()
|
|
35
|
+
if json_value is not None and not isinstance(json_value, Mapping):
|
|
36
|
+
raise AttributeError("Feature flag conditions must be a dictionary")
|
|
37
|
+
conditions._requirement_type = json_value.get(FEATURE_FILTER_REQUIREMENT_TYPE, REQUIREMENT_TYPE_ANY)
|
|
38
|
+
conditions._client_filters = json_value.get(FEATURE_FLAG_CLIENT_FILTERS, [])
|
|
39
|
+
if not isinstance(conditions._client_filters, list):
|
|
40
|
+
conditions._client_filters = []
|
|
41
|
+
for feature_filter in conditions._client_filters:
|
|
42
|
+
feature_filter["feature_name"] = feature_name
|
|
43
|
+
return conditions
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def requirement_type(self):
|
|
47
|
+
"""
|
|
48
|
+
Get the requirement type for the feature flag.
|
|
49
|
+
|
|
50
|
+
:return: Requirement type.
|
|
51
|
+
:rtype: str
|
|
52
|
+
"""
|
|
53
|
+
return self._requirement_type
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def client_filters(self):
|
|
57
|
+
"""
|
|
58
|
+
Get the client filters for the feature flag.
|
|
59
|
+
|
|
60
|
+
:return: Client filters.
|
|
61
|
+
:rtype: list[dict]
|
|
62
|
+
"""
|
|
63
|
+
return self._client_filters
|
|
64
|
+
|
|
65
|
+
def _validate(self, feature_flag_id):
|
|
66
|
+
if self._requirement_type not in [REQUIREMENT_TYPE_ALL, REQUIREMENT_TYPE_ANY]:
|
|
67
|
+
raise ValueError(f"Feature flag {feature_flag_id} has invalid requirement type.")
|
|
68
|
+
for feature_filter in self._client_filters:
|
|
69
|
+
if feature_filter.get(FEATURE_FILTER_NAME) is None:
|
|
70
|
+
raise ValueError(f"Feature flag {feature_flag_id} is missing filter name.")
|
|
@@ -0,0 +1,102 @@
|
|
|
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 ._feature_conditions import FeatureConditions
|
|
7
|
+
from ._constants import (
|
|
8
|
+
FEATURE_FLAG_ID,
|
|
9
|
+
FEATURE_FLAG_ENABLED,
|
|
10
|
+
FEATURE_FLAG_CONDITIONS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FeatureFlag:
|
|
15
|
+
"""
|
|
16
|
+
Represents a feature flag.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._id = None
|
|
21
|
+
self._enabled = False
|
|
22
|
+
self._conditions = FeatureConditions()
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def convert_from_json(cls, json_value):
|
|
26
|
+
"""
|
|
27
|
+
Convert a JSON object to FeatureFlag.
|
|
28
|
+
|
|
29
|
+
:param dict json_value: JSON object
|
|
30
|
+
:return: FeatureFlag.
|
|
31
|
+
:rtype: FeatureFlag
|
|
32
|
+
"""
|
|
33
|
+
feature_flag = cls()
|
|
34
|
+
if not isinstance(json_value, dict):
|
|
35
|
+
raise ValueError("Feature flag must be a dictionary.")
|
|
36
|
+
feature_flag._id = json_value.get(FEATURE_FLAG_ID)
|
|
37
|
+
feature_flag._enabled = _convert_boolean_value(json_value.get(FEATURE_FLAG_ENABLED, False))
|
|
38
|
+
feature_flag._conditions = FeatureConditions.convert_from_json(
|
|
39
|
+
feature_flag._id, json_value.get(FEATURE_FLAG_CONDITIONS, {})
|
|
40
|
+
)
|
|
41
|
+
if FEATURE_FLAG_CONDITIONS in json_value:
|
|
42
|
+
feature_flag._conditions = FeatureConditions.convert_from_json(
|
|
43
|
+
feature_flag._id, json_value.get(FEATURE_FLAG_CONDITIONS, {})
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
feature_flag._conditions = FeatureConditions()
|
|
47
|
+
feature_flag._validate()
|
|
48
|
+
return feature_flag
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def name(self):
|
|
52
|
+
"""
|
|
53
|
+
Get the name of the feature flag.
|
|
54
|
+
|
|
55
|
+
:return: Name of the feature flag.
|
|
56
|
+
:rtype: str
|
|
57
|
+
"""
|
|
58
|
+
return self._id
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def enabled(self):
|
|
62
|
+
"""
|
|
63
|
+
Get the status of the feature flag.
|
|
64
|
+
|
|
65
|
+
:return: Status of the feature flag.
|
|
66
|
+
:rtype: bool
|
|
67
|
+
"""
|
|
68
|
+
return self._enabled
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def conditions(self):
|
|
72
|
+
"""
|
|
73
|
+
Get the conditions for the feature flag.
|
|
74
|
+
|
|
75
|
+
:return: Conditions for the feature flag.
|
|
76
|
+
:rtype: FeatureConditions
|
|
77
|
+
"""
|
|
78
|
+
return self._conditions
|
|
79
|
+
|
|
80
|
+
def _validate(self):
|
|
81
|
+
if not isinstance(self._id, str):
|
|
82
|
+
raise ValueError(f"Invalid setting 'id' with value '{self._id}' for feature '{self._id}'.")
|
|
83
|
+
if not isinstance(self._enabled, bool):
|
|
84
|
+
raise ValueError(f"Invalid setting 'enabled' with value '{self._enabled}' for feature '{self._id}'.")
|
|
85
|
+
self.conditions._validate(self._id) # pylint: disable=protected-access
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _convert_boolean_value(enabled):
|
|
89
|
+
"""
|
|
90
|
+
Convert the value to a boolean if it is a string.
|
|
91
|
+
|
|
92
|
+
:param Union[str, bool] enabled: Value to be converted.
|
|
93
|
+
:return: Converted value.
|
|
94
|
+
:rtype: bool
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(enabled, bool):
|
|
97
|
+
return enabled
|
|
98
|
+
if enabled.lower() == "true":
|
|
99
|
+
return True
|
|
100
|
+
if enabled.lower() == "false":
|
|
101
|
+
return False
|
|
102
|
+
return enabled
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
7
|
+
from typing import NamedTuple, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TargetingContext(NamedTuple):
|
|
11
|
+
"""
|
|
12
|
+
Represents the context for targeting a feature flag.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
user_id: str = ""
|
|
16
|
+
"""
|
|
17
|
+
The user ID.
|
|
18
|
+
|
|
19
|
+
:type: str
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
groups: List[str] = []
|
|
23
|
+
"""
|
|
24
|
+
The users groups.
|
|
25
|
+
|
|
26
|
+
:type: List[str]
|
|
27
|
+
"""
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
|
|
7
|
+
VERSION = "1.0.0b1"
|
|
@@ -0,0 +1,10 @@
|
|
|
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 ._featuremanager import FeatureManager
|
|
7
|
+
from ._featurefilters import FeatureFilter
|
|
8
|
+
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
9
|
+
|
|
10
|
+
__all__ = ["FeatureManager", "TimeWindowFilter", "TargetingFilter", "FeatureFilter"]
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
|
|
7
|
+
from ._featurefilters import FeatureFilter
|
|
8
|
+
from .._defaultfilters import (
|
|
9
|
+
TargetingFilter as SyncTargetingFilter,
|
|
10
|
+
TimeWindowFilter as SyncTimeWindowFilter,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@FeatureFilter.alias("Microsoft.TimeWindow")
|
|
15
|
+
class TimeWindowFilter(FeatureFilter):
|
|
16
|
+
"""
|
|
17
|
+
Feature Filter that determines if the current time is within the time window.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._filter = SyncTimeWindowFilter()
|
|
22
|
+
|
|
23
|
+
async def evaluate(self, context, **kwargs):
|
|
24
|
+
"""
|
|
25
|
+
Determine if the feature flag is enabled for the given context.
|
|
26
|
+
|
|
27
|
+
:keyword Mapping context: Mapping with the Start and End time for the feature flag.
|
|
28
|
+
:return: True if the current time is within the time window.
|
|
29
|
+
:rtype: bool
|
|
30
|
+
"""
|
|
31
|
+
return self._filter.evaluate(context, **kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@FeatureFilter.alias("Microsoft.Targeting")
|
|
35
|
+
class TargetingFilter(FeatureFilter):
|
|
36
|
+
"""
|
|
37
|
+
Feature Filter that determines if the user is targeted for the feature flag.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._filter = SyncTargetingFilter()
|
|
42
|
+
|
|
43
|
+
async def evaluate(self, context, **kwargs):
|
|
44
|
+
"""
|
|
45
|
+
Determine if the feature flag is enabled for the given context.
|
|
46
|
+
|
|
47
|
+
:keyword Mapping context: Context for evaluating the user/group.
|
|
48
|
+
:return: True if the user is targeted for the feature flag.
|
|
49
|
+
:rtype: bool
|
|
50
|
+
"""
|
|
51
|
+
return self._filter.evaluate(context, **kwargs)
|
|
@@ -0,0 +1,48 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FeatureFilter(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Parent class for all async feature filters.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def evaluate(self, context, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Determine if the feature flag is enabled for the given context.
|
|
18
|
+
|
|
19
|
+
:param Mapping context: Context for the feature flag.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self):
|
|
24
|
+
"""
|
|
25
|
+
Get the name of the filter.
|
|
26
|
+
|
|
27
|
+
:return: Name of the filter, or alias if it exists.
|
|
28
|
+
:rtype: str
|
|
29
|
+
"""
|
|
30
|
+
if hasattr(self, "_alias"):
|
|
31
|
+
return self._alias
|
|
32
|
+
return self.__class__.__name__
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
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
|
+
|
|
44
|
+
def wrapper(cls):
|
|
45
|
+
cls._alias = alias # pylint: disable=protected-access
|
|
46
|
+
return cls
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
@@ -0,0 +1,142 @@
|
|
|
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 collections.abc import Mapping
|
|
7
|
+
import logging
|
|
8
|
+
from typing import overload
|
|
9
|
+
from ._defaultfilters import TimeWindowFilter, TargetingFilter
|
|
10
|
+
from ._featurefilters import FeatureFilter
|
|
11
|
+
from .._featuremanager import (
|
|
12
|
+
PROVIDED_FEATURE_FILTERS,
|
|
13
|
+
FEATURE_FILTER_NAME,
|
|
14
|
+
REQUIREMENT_TYPE_ALL,
|
|
15
|
+
FEATURE_MANAGEMENT_KEY,
|
|
16
|
+
_get_feature_flag,
|
|
17
|
+
_list_feature_flag_names,
|
|
18
|
+
)
|
|
19
|
+
from .._models import EvaluationEvent, TargetingContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FeatureManager:
|
|
23
|
+
"""
|
|
24
|
+
Feature Manager that determines if a feature flag is enabled for the given context.
|
|
25
|
+
|
|
26
|
+
:param Mapping configuration: Configuration object.
|
|
27
|
+
:keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, configuration, **kwargs):
|
|
31
|
+
self._filters = {}
|
|
32
|
+
if configuration is None or not isinstance(configuration, Mapping):
|
|
33
|
+
raise AttributeError("Configuration must be a non-empty dictionary")
|
|
34
|
+
self._configuration = configuration
|
|
35
|
+
self._cache = {}
|
|
36
|
+
self._copy = configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
37
|
+
filters = [TimeWindowFilter(), TargetingFilter()] + kwargs.pop(PROVIDED_FEATURE_FILTERS, [])
|
|
38
|
+
|
|
39
|
+
for feature_filter in filters:
|
|
40
|
+
if not isinstance(feature_filter, FeatureFilter):
|
|
41
|
+
raise ValueError("Custom filter must be a subclass of FeatureFilter")
|
|
42
|
+
self._filters[feature_filter.name] = feature_filter
|
|
43
|
+
|
|
44
|
+
def _build_targeting_context(self, args):
|
|
45
|
+
"""
|
|
46
|
+
Builds a TargetingContext, either returns a provided context, takes the provided user_id to make a context, or
|
|
47
|
+
returns an empty context.
|
|
48
|
+
|
|
49
|
+
:param args: Arguments to build the TargetingContext.
|
|
50
|
+
:return: TargetingContext
|
|
51
|
+
"""
|
|
52
|
+
if len(args) == 1 and isinstance(args[0], str):
|
|
53
|
+
return TargetingContext(user_id=args[0], groups=[])
|
|
54
|
+
if len(args) == 1 and isinstance(args[0], TargetingContext):
|
|
55
|
+
return args[0]
|
|
56
|
+
return TargetingContext()
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
async def is_enabled(self, feature_flag_id, user_id, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
Determine if the feature flag is enabled for the given context.
|
|
62
|
+
|
|
63
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
64
|
+
:param str user_id: User identifier.
|
|
65
|
+
:return: True if the feature flag is enabled for the given context.
|
|
66
|
+
:rtype: bool
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
async def is_enabled(self, feature_flag_id, *args, **kwargs):
|
|
70
|
+
"""
|
|
71
|
+
Determine if the feature flag is enabled for the given context.
|
|
72
|
+
|
|
73
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
74
|
+
:return: True if the feature flag is enabled for the given context.
|
|
75
|
+
:rtype: bool
|
|
76
|
+
"""
|
|
77
|
+
targeting_context = self._build_targeting_context(args)
|
|
78
|
+
return (await self._check_feature(feature_flag_id, targeting_context, **kwargs)).enabled
|
|
79
|
+
|
|
80
|
+
async def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
|
|
81
|
+
feature_conditions = feature_flag.conditions
|
|
82
|
+
feature_filters = feature_conditions.client_filters
|
|
83
|
+
evaluation_event = EvaluationEvent(enabled=False)
|
|
84
|
+
|
|
85
|
+
if len(feature_filters) == 0:
|
|
86
|
+
# Feature flags without any filters return evaluate
|
|
87
|
+
evaluation_event.enabled = True
|
|
88
|
+
else:
|
|
89
|
+
# The assumed value is no filters is based on the requirement type.
|
|
90
|
+
# Requirement type Any assumes false until proven true, All assumes true until proven false
|
|
91
|
+
evaluation_event.enabled = feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL
|
|
92
|
+
|
|
93
|
+
for feature_filter in feature_filters:
|
|
94
|
+
filter_name = feature_filter[FEATURE_FILTER_NAME]
|
|
95
|
+
kwargs["user"] = targeting_context.user_id
|
|
96
|
+
kwargs["groups"] = targeting_context.groups
|
|
97
|
+
if filter_name not in self._filters:
|
|
98
|
+
raise ValueError(f"Feature flag {feature_flag.name} has unknown filter {filter_name}")
|
|
99
|
+
if feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL:
|
|
100
|
+
if not await self._filters[filter_name].evaluate(feature_filter, **kwargs):
|
|
101
|
+
evaluation_event.enabled = False
|
|
102
|
+
break
|
|
103
|
+
else:
|
|
104
|
+
if await self._filters[filter_name].evaluate(feature_filter, **kwargs):
|
|
105
|
+
evaluation_event.enabled = True
|
|
106
|
+
break
|
|
107
|
+
return evaluation_event
|
|
108
|
+
|
|
109
|
+
async def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
|
|
110
|
+
"""
|
|
111
|
+
Determine if the feature flag is enabled for the given context.
|
|
112
|
+
|
|
113
|
+
:param str feature_flag_id: Name of the feature flag.
|
|
114
|
+
:return: True if the feature flag is enabled for the given context.
|
|
115
|
+
:rtype: bool
|
|
116
|
+
"""
|
|
117
|
+
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
|
|
118
|
+
self._cache = {}
|
|
119
|
+
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
|
|
120
|
+
|
|
121
|
+
if not self._cache.get(feature_flag_id):
|
|
122
|
+
feature_flag = _get_feature_flag(self._configuration, feature_flag_id)
|
|
123
|
+
self._cache[feature_flag_id] = feature_flag
|
|
124
|
+
else:
|
|
125
|
+
feature_flag = self._cache.get(feature_flag_id)
|
|
126
|
+
|
|
127
|
+
if not feature_flag:
|
|
128
|
+
logging.warning("Feature flag %s not found", feature_flag_id)
|
|
129
|
+
# Unknown feature flags are disabled by default
|
|
130
|
+
return EvaluationEvent(enabled=False)
|
|
131
|
+
|
|
132
|
+
if not feature_flag.enabled:
|
|
133
|
+
# Feature flags that are disabled are always disabled
|
|
134
|
+
return EvaluationEvent(enabled=False)
|
|
135
|
+
|
|
136
|
+
return await self._check_feature_filters(feature_flag, targeting_context, **kwargs)
|
|
137
|
+
|
|
138
|
+
def list_feature_flag_names(self):
|
|
139
|
+
"""
|
|
140
|
+
List of all feature flag names.
|
|
141
|
+
"""
|
|
142
|
+
return _list_feature_flag_names(self._configuration)
|
|
File without changes
|