growthbook 1.4.6__tar.gz → 1.4.8__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.
- {growthbook-1.4.6/growthbook.egg-info → growthbook-1.4.8}/PKG-INFO +20 -1
- {growthbook-1.4.6 → growthbook-1.4.8}/README.md +18 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/__init__.py +1 -1
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/common_types.py +58 -55
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/core.py +34 -29
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/growthbook.py +159 -58
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/growthbook_client.py +2 -2
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/plugins/growthbook_tracking.py +1 -1
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/plugins/request_context.py +2 -1
- {growthbook-1.4.6 → growthbook-1.4.8/growthbook.egg-info}/PKG-INFO +20 -1
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook.egg-info/SOURCES.txt +1 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/pyproject.toml +28 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/setup.cfg +1 -1
- {growthbook-1.4.6 → growthbook-1.4.8}/setup.py +1 -0
- growthbook-1.4.8/tests/test_etag.py +345 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/tests/test_growthbook.py +83 -5
- {growthbook-1.4.6 → growthbook-1.4.8}/tests/test_growthbook_client.py +73 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/LICENSE +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/MANIFEST.in +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/plugins/__init__.py +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/plugins/base.py +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook/py.typed +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook.egg-info/dependency_links.txt +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook.egg-info/requires.txt +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/growthbook.egg-info/top_level.txt +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/tests/conftest.py +0 -0
- {growthbook-1.4.6 → growthbook-1.4.8}/tests/test_plugins.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: growthbook
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.8
|
|
4
4
|
Summary: Powerful Feature flagging and A/B testing for Python apps
|
|
5
5
|
Home-page: https://github.com/growthbook/growthbook-python
|
|
6
6
|
Author: GrowthBook
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Typing :: Typed
|
|
21
22
|
Requires-Python: >=3.7
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
License-File: LICENSE
|
|
@@ -56,6 +57,24 @@ Powerful Feature flagging and A/B testing for Python apps.
|
|
|
56
57
|
|
|
57
58
|
`pip install growthbook` (recommended) or copy `growthbook.py` into your project
|
|
58
59
|
|
|
60
|
+
## Type Checking Support
|
|
61
|
+
|
|
62
|
+
The GrowthBook Python SDK is fully typed and includes inline type hints for all public APIs. This enables:
|
|
63
|
+
|
|
64
|
+
- **Better IDE support** with autocomplete and inline documentation
|
|
65
|
+
- **Type safety** - catch bugs at development time with mypy or other type checkers
|
|
66
|
+
- **Better code documentation** - types serve as inline documentation
|
|
67
|
+
- **Safer refactoring** - type checkers will catch breaking changes
|
|
68
|
+
|
|
69
|
+
To use type checking with mypy:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install mypy
|
|
73
|
+
mypy your_code.py
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The SDK includes a `py.typed` marker file and is compliant with [PEP 561](https://www.python.org/dev/peps/pep-0561/).
|
|
77
|
+
|
|
59
78
|
## Quick Usage
|
|
60
79
|
|
|
61
80
|
```python
|
|
@@ -21,6 +21,24 @@ Powerful Feature flagging and A/B testing for Python apps.
|
|
|
21
21
|
|
|
22
22
|
`pip install growthbook` (recommended) or copy `growthbook.py` into your project
|
|
23
23
|
|
|
24
|
+
## Type Checking Support
|
|
25
|
+
|
|
26
|
+
The GrowthBook Python SDK is fully typed and includes inline type hints for all public APIs. This enables:
|
|
27
|
+
|
|
28
|
+
- **Better IDE support** with autocomplete and inline documentation
|
|
29
|
+
- **Type safety** - catch bugs at development time with mypy or other type checkers
|
|
30
|
+
- **Better code documentation** - types serve as inline documentation
|
|
31
|
+
- **Safer refactoring** - type checkers will catch breaking changes
|
|
32
|
+
|
|
33
|
+
To use type checking with mypy:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install mypy
|
|
37
|
+
mypy your_code.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The SDK includes a `py.typed` marker file and is compliant with [PEP 561](https://www.python.org/dev/peps/pep-0561/).
|
|
41
|
+
|
|
24
42
|
## Quick Usage
|
|
25
43
|
|
|
26
44
|
```python
|
|
@@ -29,30 +29,30 @@ class Experiment(object):
|
|
|
29
29
|
def __init__(
|
|
30
30
|
self,
|
|
31
31
|
key: str,
|
|
32
|
-
variations:
|
|
33
|
-
weights: List[float] = None,
|
|
32
|
+
variations: List[Any],
|
|
33
|
+
weights: Optional[List[float]] = None,
|
|
34
34
|
active: bool = True,
|
|
35
35
|
status: str = "running",
|
|
36
|
-
coverage:
|
|
37
|
-
condition:
|
|
38
|
-
namespace: Tuple[str, float, float] = None,
|
|
36
|
+
coverage: Optional[float] = None,
|
|
37
|
+
condition: Optional[Dict[str, Any]] = None,
|
|
38
|
+
namespace: Optional[Tuple[str, float, float]] = None,
|
|
39
39
|
url: str = "",
|
|
40
|
-
include=None,
|
|
41
|
-
groups:
|
|
42
|
-
force: int = None,
|
|
40
|
+
include: Optional[Any] = None,
|
|
41
|
+
groups: Optional[List[Any]] = None,
|
|
42
|
+
force: Optional[int] = None,
|
|
43
43
|
hashAttribute: str = "id",
|
|
44
|
-
fallbackAttribute: str = None,
|
|
45
|
-
hashVersion: int = None,
|
|
46
|
-
ranges: List[Tuple[float, float]] = None,
|
|
47
|
-
meta: List[VariationMeta] = None,
|
|
48
|
-
filters: List[Filter] = None,
|
|
49
|
-
seed: str = None,
|
|
50
|
-
name: str = None,
|
|
51
|
-
phase: str = None,
|
|
44
|
+
fallbackAttribute: Optional[str] = None,
|
|
45
|
+
hashVersion: Optional[int] = None,
|
|
46
|
+
ranges: Optional[List[Tuple[float, float]]] = None,
|
|
47
|
+
meta: Optional[List[VariationMeta]] = None,
|
|
48
|
+
filters: Optional[List[Filter]] = None,
|
|
49
|
+
seed: Optional[str] = None,
|
|
50
|
+
name: Optional[str] = None,
|
|
51
|
+
phase: Optional[str] = None,
|
|
52
52
|
disableStickyBucketing: bool = False,
|
|
53
|
-
bucketVersion: int = None,
|
|
54
|
-
minBucketVersion: int = None,
|
|
55
|
-
parentConditions: List[
|
|
53
|
+
bucketVersion: Optional[int] = None,
|
|
54
|
+
minBucketVersion: Optional[int] = None,
|
|
55
|
+
parentConditions: Optional[List[Dict[str, Any]]] = None,
|
|
56
56
|
) -> None:
|
|
57
57
|
self.key = key
|
|
58
58
|
self.variations = variations
|
|
@@ -85,8 +85,8 @@ class Experiment(object):
|
|
|
85
85
|
self.include = include
|
|
86
86
|
self.groups = groups
|
|
87
87
|
|
|
88
|
-
def to_dict(self):
|
|
89
|
-
obj = {
|
|
88
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
89
|
+
obj: Dict[str, Any] = {
|
|
90
90
|
"key": self.key,
|
|
91
91
|
"variations": self.variations,
|
|
92
92
|
"weights": self.weights,
|
|
@@ -118,7 +118,7 @@ class Experiment(object):
|
|
|
118
118
|
|
|
119
119
|
return obj
|
|
120
120
|
|
|
121
|
-
def update(self, data:
|
|
121
|
+
def update(self, data: Dict[str, Any]) -> None:
|
|
122
122
|
weights = data.get("weights", None)
|
|
123
123
|
status = data.get("status", None)
|
|
124
124
|
coverage = data.get("coverage", None)
|
|
@@ -145,13 +145,13 @@ class Result(object):
|
|
|
145
145
|
self,
|
|
146
146
|
variationId: int,
|
|
147
147
|
inExperiment: bool,
|
|
148
|
-
value,
|
|
148
|
+
value: Any,
|
|
149
149
|
hashUsed: bool,
|
|
150
150
|
hashAttribute: str,
|
|
151
151
|
hashValue: str,
|
|
152
152
|
featureId: Optional[str],
|
|
153
|
-
meta: VariationMeta = None,
|
|
154
|
-
bucket: float = None,
|
|
153
|
+
meta: Optional[VariationMeta] = None,
|
|
154
|
+
bucket: Optional[float] = None,
|
|
155
155
|
stickyBucketUsed: bool = False,
|
|
156
156
|
) -> None:
|
|
157
157
|
self.variationId = variationId
|
|
@@ -176,8 +176,8 @@ class Result(object):
|
|
|
176
176
|
if "passthrough" in meta:
|
|
177
177
|
self.passthrough = meta["passthrough"]
|
|
178
178
|
|
|
179
|
-
def to_dict(self) ->
|
|
180
|
-
obj = {
|
|
179
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
180
|
+
obj: Dict[str, Any] = {
|
|
181
181
|
"featureId": self.featureId,
|
|
182
182
|
"variationId": self.variationId,
|
|
183
183
|
"inExperiment": self.inExperiment,
|
|
@@ -201,11 +201,11 @@ class Result(object):
|
|
|
201
201
|
class FeatureResult(object):
|
|
202
202
|
def __init__(
|
|
203
203
|
self,
|
|
204
|
-
value,
|
|
204
|
+
value: Any,
|
|
205
205
|
source: str,
|
|
206
|
-
experiment: Experiment = None,
|
|
207
|
-
experimentResult: Result = None,
|
|
208
|
-
ruleId: str = None,
|
|
206
|
+
experiment: Optional[Experiment] = None,
|
|
207
|
+
experimentResult: Optional[Result] = None,
|
|
208
|
+
ruleId: Optional[str] = None,
|
|
209
209
|
) -> None:
|
|
210
210
|
self.value = value
|
|
211
211
|
self.source = source
|
|
@@ -215,8 +215,8 @@ class FeatureResult(object):
|
|
|
215
215
|
self.on = bool(value)
|
|
216
216
|
self.off = not bool(value)
|
|
217
217
|
|
|
218
|
-
def to_dict(self) ->
|
|
219
|
-
data = {
|
|
218
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
219
|
+
data: Dict[str, Any] = {
|
|
220
220
|
"value": self.value,
|
|
221
221
|
"source": self.source,
|
|
222
222
|
"on": self.on,
|
|
@@ -231,7 +231,9 @@ class FeatureResult(object):
|
|
|
231
231
|
return data
|
|
232
232
|
|
|
233
233
|
class Feature(object):
|
|
234
|
-
def __init__(self, defaultValue=None, rules:
|
|
234
|
+
def __init__(self, defaultValue: Any = None, rules: Optional[List[Any]] = None) -> None:
|
|
235
|
+
if rules is None:
|
|
236
|
+
rules = []
|
|
235
237
|
self.defaultValue = defaultValue
|
|
236
238
|
self.rules: List[FeatureRule] = []
|
|
237
239
|
for rule in rules:
|
|
@@ -263,7 +265,7 @@ class Feature(object):
|
|
|
263
265
|
parentConditions=rule.get("parentConditions", None),
|
|
264
266
|
))
|
|
265
267
|
|
|
266
|
-
def to_dict(self) ->
|
|
268
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
267
269
|
return {
|
|
268
270
|
"defaultValue": self.defaultValue,
|
|
269
271
|
"rules": [rule.to_dict() for rule in self.rules],
|
|
@@ -272,28 +274,28 @@ class Feature(object):
|
|
|
272
274
|
class FeatureRule(object):
|
|
273
275
|
def __init__(
|
|
274
276
|
self,
|
|
275
|
-
id: str = None,
|
|
277
|
+
id: Optional[str] = None,
|
|
276
278
|
key: str = "",
|
|
277
|
-
variations:
|
|
278
|
-
weights: List[float] = None,
|
|
279
|
-
coverage:
|
|
280
|
-
condition:
|
|
281
|
-
namespace: Tuple[str, float, float] = None,
|
|
282
|
-
force=None,
|
|
279
|
+
variations: Optional[List[Any]] = None,
|
|
280
|
+
weights: Optional[List[float]] = None,
|
|
281
|
+
coverage: Optional[float] = None,
|
|
282
|
+
condition: Optional[Dict[str, Any]] = None,
|
|
283
|
+
namespace: Optional[Tuple[str, float, float]] = None,
|
|
284
|
+
force: Optional[Any] = None,
|
|
283
285
|
hashAttribute: str = "id",
|
|
284
|
-
fallbackAttribute: str = None,
|
|
285
|
-
hashVersion: int = None,
|
|
286
|
-
range: Tuple[float, float] = None,
|
|
287
|
-
ranges: List[Tuple[float, float]] = None,
|
|
288
|
-
meta: List[VariationMeta] = None,
|
|
289
|
-
filters: List[Filter] = None,
|
|
290
|
-
seed: str = None,
|
|
291
|
-
name: str = None,
|
|
292
|
-
phase: str = None,
|
|
286
|
+
fallbackAttribute: Optional[str] = None,
|
|
287
|
+
hashVersion: Optional[int] = None,
|
|
288
|
+
range: Optional[Tuple[float, float]] = None,
|
|
289
|
+
ranges: Optional[List[Tuple[float, float]]] = None,
|
|
290
|
+
meta: Optional[List[VariationMeta]] = None,
|
|
291
|
+
filters: Optional[List[Filter]] = None,
|
|
292
|
+
seed: Optional[str] = None,
|
|
293
|
+
name: Optional[str] = None,
|
|
294
|
+
phase: Optional[str] = None,
|
|
293
295
|
disableStickyBucketing: bool = False,
|
|
294
|
-
bucketVersion: int = None,
|
|
295
|
-
minBucketVersion: int = None,
|
|
296
|
-
parentConditions: List[
|
|
296
|
+
bucketVersion: Optional[int] = None,
|
|
297
|
+
minBucketVersion: Optional[int] = None,
|
|
298
|
+
parentConditions: Optional[List[Dict[str, Any]]] = None,
|
|
297
299
|
) -> None:
|
|
298
300
|
|
|
299
301
|
if disableStickyBucketing:
|
|
@@ -322,7 +324,7 @@ class FeatureRule(object):
|
|
|
322
324
|
self.minBucketVersion = minBucketVersion or 0
|
|
323
325
|
self.parentConditions = parentConditions
|
|
324
326
|
|
|
325
|
-
def to_dict(self) ->
|
|
327
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
326
328
|
data: Dict[str, Any] = {}
|
|
327
329
|
if self.id:
|
|
328
330
|
data["id"] = self.id
|
|
@@ -410,6 +412,7 @@ class UserContext:
|
|
|
410
412
|
forced_variations: Dict[str, Any] = field(default_factory=dict)
|
|
411
413
|
overrides: Dict[str, Any] = field(default_factory=dict)
|
|
412
414
|
sticky_bucket_assignment_docs: Dict[str, Any] = field(default_factory=dict)
|
|
415
|
+
skip_all_experiments: bool = False
|
|
413
416
|
|
|
414
417
|
@dataclass
|
|
415
418
|
class Options:
|
|
@@ -9,7 +9,7 @@ from .common_types import EvaluationContext, FeatureResult, Experiment, Filter,
|
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger("growthbook.core")
|
|
11
11
|
|
|
12
|
-
def evalCondition(attributes:
|
|
12
|
+
def evalCondition(attributes: Dict[str, Any], condition: Dict[str, Any], savedGroups: Optional[Dict[str, Any]] = None) -> bool:
|
|
13
13
|
for key, value in condition.items():
|
|
14
14
|
if key == "$or":
|
|
15
15
|
if not evalOr(attributes, value, savedGroups):
|
|
@@ -28,7 +28,7 @@ def evalCondition(attributes: dict, condition: dict, savedGroups: dict = None) -
|
|
|
28
28
|
|
|
29
29
|
return True
|
|
30
30
|
|
|
31
|
-
def evalOr(attributes, conditions, savedGroups) -> bool:
|
|
31
|
+
def evalOr(attributes: Dict[str, Any], conditions: List[Any], savedGroups: Optional[Dict[str, Any]]) -> bool:
|
|
32
32
|
if len(conditions) == 0:
|
|
33
33
|
return True
|
|
34
34
|
|
|
@@ -38,13 +38,13 @@ def evalOr(attributes, conditions, savedGroups) -> bool:
|
|
|
38
38
|
return False
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def evalAnd(attributes, conditions, savedGroups) -> bool:
|
|
41
|
+
def evalAnd(attributes: Dict[str, Any], conditions: List[Any], savedGroups: Optional[Dict[str, Any]]) -> bool:
|
|
42
42
|
for condition in conditions:
|
|
43
43
|
if not evalCondition(attributes, condition, savedGroups):
|
|
44
44
|
return False
|
|
45
45
|
return True
|
|
46
46
|
|
|
47
|
-
def isOperatorObject(obj) -> bool:
|
|
47
|
+
def isOperatorObject(obj: Any) -> bool:
|
|
48
48
|
for key in obj.keys():
|
|
49
49
|
if key[0] != "$":
|
|
50
50
|
return False
|
|
@@ -82,7 +82,7 @@ def evalConditionValue(conditionValue, attributeValue, savedGroups) -> bool:
|
|
|
82
82
|
if not evalOperatorCondition(key, attributeValue, value, savedGroups):
|
|
83
83
|
return False
|
|
84
84
|
return True
|
|
85
|
-
return conditionValue == attributeValue
|
|
85
|
+
return bool(conditionValue == attributeValue)
|
|
86
86
|
|
|
87
87
|
def elemMatch(condition, attributeValue, savedGroups) -> bool:
|
|
88
88
|
if not type(attributeValue) is list:
|
|
@@ -208,7 +208,7 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
|
|
|
208
208
|
return attributeValue is None
|
|
209
209
|
return attributeValue is not None
|
|
210
210
|
elif operator == "$type":
|
|
211
|
-
return getType(attributeValue) == conditionValue
|
|
211
|
+
return bool(getType(attributeValue) == conditionValue)
|
|
212
212
|
elif operator == "$not":
|
|
213
213
|
return not evalConditionValue(conditionValue, attributeValue, savedGroups)
|
|
214
214
|
return False
|
|
@@ -263,18 +263,18 @@ def _getOrigHashValue(
|
|
|
263
263
|
|
|
264
264
|
return (actual_attr, val)
|
|
265
265
|
|
|
266
|
-
def _getHashValue(eval_context: EvaluationContext, attr: str = None, fallbackAttr: str = None) -> Tuple[str, str]:
|
|
266
|
+
def _getHashValue(eval_context: EvaluationContext, attr: Optional[str] = None, fallbackAttr: Optional[str] = None) -> Tuple[str, str]:
|
|
267
267
|
(attr, val) = _getOrigHashValue(attr=attr, fallbackAttr=fallbackAttr, eval_context=eval_context)
|
|
268
268
|
return (attr, str(val))
|
|
269
269
|
|
|
270
270
|
def _isIncludedInRollout(
|
|
271
271
|
seed: str,
|
|
272
272
|
eval_context: EvaluationContext,
|
|
273
|
-
hashAttribute: str = None,
|
|
274
|
-
fallbackAttribute: str = None,
|
|
275
|
-
range: Tuple[float, float] = None,
|
|
276
|
-
coverage: float = None,
|
|
277
|
-
hashVersion: int = None
|
|
273
|
+
hashAttribute: Optional[str] = None,
|
|
274
|
+
fallbackAttribute: Optional[str] = None,
|
|
275
|
+
range: Optional[Tuple[float, float]] = None,
|
|
276
|
+
coverage: Optional[float] = None,
|
|
277
|
+
hashVersion: Optional[int] = None
|
|
278
278
|
) -> bool:
|
|
279
279
|
if coverage is None and range is None:
|
|
280
280
|
return True
|
|
@@ -388,7 +388,7 @@ def getEqualWeights(numVariations: int) -> List[float]:
|
|
|
388
388
|
|
|
389
389
|
|
|
390
390
|
def getBucketRanges(
|
|
391
|
-
numVariations: int, coverage: float = 1, weights: List[float] = None
|
|
391
|
+
numVariations: int, coverage: float = 1, weights: Optional[List[float]] = None
|
|
392
392
|
) -> List[Tuple[float, float]]:
|
|
393
393
|
if coverage < 0:
|
|
394
394
|
coverage = 0
|
|
@@ -412,9 +412,9 @@ def getBucketRanges(
|
|
|
412
412
|
|
|
413
413
|
def eval_feature(
|
|
414
414
|
key: str,
|
|
415
|
-
evalContext: EvaluationContext = None,
|
|
416
|
-
callback_subscription: Callable[[Experiment, Result], None] = None,
|
|
417
|
-
tracking_cb: Callable[[Experiment, Result, UserContext], None] = None
|
|
415
|
+
evalContext: Optional[EvaluationContext] = None,
|
|
416
|
+
callback_subscription: Optional[Callable[[Experiment, Result], None]] = None,
|
|
417
|
+
tracking_cb: Optional[Callable[[Experiment, Result, UserContext], None]] = None
|
|
418
418
|
) -> FeatureResult:
|
|
419
419
|
"""Core feature evaluation logic as a standalone function"""
|
|
420
420
|
|
|
@@ -560,8 +560,8 @@ def _get_sticky_bucket_experiment_key(experiment_key: str, bucket_version: int =
|
|
|
560
560
|
return experiment_key + "__" + str(bucket_version)
|
|
561
561
|
|
|
562
562
|
def _get_sticky_bucket_assignments(evalContext: EvaluationContext,
|
|
563
|
-
attr: str = None,
|
|
564
|
-
fallback: str = None) -> Dict[str, str]:
|
|
563
|
+
attr: Optional[str] = None,
|
|
564
|
+
fallback: Optional[str] = None) -> Dict[str, str]:
|
|
565
565
|
merged: Dict[str, str] = {}
|
|
566
566
|
|
|
567
567
|
# Search for docs stored for attribute(id)
|
|
@@ -597,12 +597,12 @@ def _is_blocked(
|
|
|
597
597
|
def _get_sticky_bucket_variation(
|
|
598
598
|
experiment_key: str,
|
|
599
599
|
evalContext: EvaluationContext,
|
|
600
|
-
bucket_version: int = None,
|
|
601
|
-
min_bucket_version: int = None,
|
|
602
|
-
meta: List[VariationMeta] = None,
|
|
603
|
-
hash_attribute: str = None,
|
|
604
|
-
fallback_attribute: str = None,
|
|
605
|
-
) ->
|
|
600
|
+
bucket_version: Optional[int] = None,
|
|
601
|
+
min_bucket_version: Optional[int] = None,
|
|
602
|
+
meta: Optional[List[VariationMeta]] = None,
|
|
603
|
+
hash_attribute: Optional[str] = None,
|
|
604
|
+
fallback_attribute: Optional[str] = None,
|
|
605
|
+
) -> Dict[str, Any]:
|
|
606
606
|
bucket_version = bucket_version or 0
|
|
607
607
|
min_bucket_version = min_bucket_version or 0
|
|
608
608
|
meta = meta or []
|
|
@@ -633,8 +633,8 @@ def _get_sticky_bucket_variation(
|
|
|
633
633
|
|
|
634
634
|
def run_experiment(experiment: Experiment,
|
|
635
635
|
featureId: Optional[str] = None,
|
|
636
|
-
evalContext: EvaluationContext = None,
|
|
637
|
-
tracking_cb: Callable[[Experiment, Result, UserContext], None] = None
|
|
636
|
+
evalContext: Optional[EvaluationContext] = None,
|
|
637
|
+
tracking_cb: Optional[Callable[[Experiment, Result, UserContext], None]] = None
|
|
638
638
|
) -> Result:
|
|
639
639
|
if evalContext is None:
|
|
640
640
|
raise ValueError("evalContext is required - run_experiment")
|
|
@@ -821,11 +821,16 @@ def run_experiment(experiment: Experiment,
|
|
|
821
821
|
experiment=experiment, variationId=experiment.force, featureId=featureId, evalContext=evalContext
|
|
822
822
|
)
|
|
823
823
|
|
|
824
|
-
# 12. Exclude if in QA mode
|
|
824
|
+
# 12. Exclude if in QA mode (global)
|
|
825
825
|
if evalContext.global_ctx.options.qa_mode:
|
|
826
826
|
logger.debug("Skip experiment %s because of QA Mode", experiment.key)
|
|
827
827
|
return _getExperimentResult(experiment=experiment, featureId=featureId, evalContext=evalContext)
|
|
828
828
|
|
|
829
|
+
# 12.1. Exclude if user has skip_all_experiments flag set
|
|
830
|
+
if evalContext.user.skip_all_experiments:
|
|
831
|
+
logger.debug("Skip experiment %s because user has skip_all_experiments flag set", experiment.key)
|
|
832
|
+
return _getExperimentResult(experiment=experiment, featureId=featureId, evalContext=evalContext)
|
|
833
|
+
|
|
829
834
|
# 12.5. If experiment is stopped, return immediately
|
|
830
835
|
if experiment.status == "stopped":
|
|
831
836
|
logger.debug("Skip experiment %s because it is stopped", experiment.key)
|
|
@@ -891,8 +896,8 @@ def _getExperimentResult(
|
|
|
891
896
|
evalContext: EvaluationContext,
|
|
892
897
|
variationId: int = -1,
|
|
893
898
|
hashUsed: bool = False,
|
|
894
|
-
featureId: str = None,
|
|
895
|
-
bucket: float = None,
|
|
899
|
+
featureId: Optional[str] = None,
|
|
900
|
+
bucket: Optional[float] = None,
|
|
896
901
|
stickyBucketUsed: bool = False
|
|
897
902
|
) -> Result:
|
|
898
903
|
inExperiment = True
|