Skip to content

Commit bf531bf

Browse files
committed
Added saved groups, added $inGroup $notInGroup operators, removed versionCompare test cases
1 parent cd1c4f8 commit bf531bf

File tree

3 files changed

+3756
-764
lines changed

3 files changed

+3756
-764
lines changed

growthbook/growthbook.py

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -162,36 +162,36 @@ def isIn(conditionValue, attributeValue) -> bool:
162162
return attributeValue in conditionValue
163163

164164

165-
def evalCondition(attributes: dict, condition: dict) -> bool:
165+
def evalCondition(attributes: dict, condition: dict, savedGroups: dict = None) -> bool:
166166
if "$or" in condition:
167-
return evalOr(attributes, condition["$or"])
167+
return evalOr(attributes, condition["$or"], savedGroups)
168168
if "$nor" in condition:
169-
return not evalOr(attributes, condition["$nor"])
169+
return not evalOr(attributes, condition["$nor"], savedGroups)
170170
if "$and" in condition:
171-
return evalAnd(attributes, condition["$and"])
171+
return evalAnd(attributes, condition["$and"], savedGroups)
172172
if "$not" in condition:
173-
return not evalCondition(attributes, condition["$not"])
173+
return not evalCondition(attributes, condition["$not"], savedGroups)
174174

175175
for key, value in condition.items():
176-
if not evalConditionValue(value, getPath(attributes, key)):
176+
if not evalConditionValue(value, getPath(attributes, key), savedGroups):
177177
return False
178178

179179
return True
180180

181181

182-
def evalOr(attributes, conditions) -> bool:
182+
def evalOr(attributes, conditions, savedGroups) -> bool:
183183
if len(conditions) == 0:
184184
return True
185185

186186
for condition in conditions:
187-
if evalCondition(attributes, condition):
187+
if evalCondition(attributes, condition, savedGroups):
188188
return True
189189
return False
190190

191191

192-
def evalAnd(attributes, conditions) -> bool:
192+
def evalAnd(attributes, conditions, savedGroups) -> bool:
193193
for condition in conditions:
194-
if not evalCondition(attributes, condition):
194+
if not evalCondition(attributes, condition, savedGroups):
195195
return False
196196
return True
197197

@@ -231,25 +231,25 @@ def getPath(attributes, path):
231231
return current
232232

233233

234-
def evalConditionValue(conditionValue, attributeValue) -> bool:
234+
def evalConditionValue(conditionValue, attributeValue, savedGroups) -> bool:
235235
if type(conditionValue) is dict and isOperatorObject(conditionValue):
236236
for key, value in conditionValue.items():
237-
if not evalOperatorCondition(key, attributeValue, value):
237+
if not evalOperatorCondition(key, attributeValue, value, savedGroups):
238238
return False
239239
return True
240240
return conditionValue == attributeValue
241241

242242

243-
def elemMatch(condition, attributeValue) -> bool:
243+
def elemMatch(condition, attributeValue, savedGroups) -> bool:
244244
if not type(attributeValue) is list:
245245
return False
246246

247247
for item in attributeValue:
248248
if isOperatorObject(condition):
249-
if evalConditionValue(condition, item):
249+
if evalConditionValue(condition, item, savedGroups):
250250
return True
251251
else:
252-
if evalCondition(item, condition):
252+
if evalCondition(item, condition, savedGroups):
253253
return True
254254

255255
return False
@@ -275,7 +275,7 @@ def compare(val1, val2) -> int:
275275
return 0
276276

277277

278-
def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
278+
def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups) -> bool:
279279
if operator == "$eq":
280280
try:
281281
return compare(attributeValue, conditionValue) == 0
@@ -318,6 +318,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
318318
return paddedVersionString(attributeValue) > paddedVersionString(conditionValue)
319319
elif operator == "$vgte":
320320
return paddedVersionString(attributeValue) >= paddedVersionString(conditionValue)
321+
elif operator == "$inGroup":
322+
if not type(conditionValue) is str:
323+
return False
324+
if not conditionValue in savedGroups:
325+
return False
326+
return isIn(savedGroups[conditionValue] or [], attributeValue)
327+
elif operator == "$notInGroup":
328+
if not type(conditionValue) is str:
329+
return False
330+
if not conditionValue in savedGroups:
331+
return True
332+
return not isIn(savedGroups[conditionValue] or [], attributeValue)
321333
elif operator == "$regex":
322334
try:
323335
r = re.compile(conditionValue)
@@ -333,18 +345,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
333345
return False
334346
return not isIn(conditionValue, attributeValue)
335347
elif operator == "$elemMatch":
336-
return elemMatch(conditionValue, attributeValue)
348+
return elemMatch(conditionValue, attributeValue, savedGroups)
337349
elif operator == "$size":
338350
if not (type(attributeValue) is list):
339351
return False
340-
return evalConditionValue(conditionValue, len(attributeValue))
352+
return evalConditionValue(conditionValue, len(attributeValue), savedGroups)
341353
elif operator == "$all":
342354
if not (type(attributeValue) is list):
343355
return False
344356
for cond in conditionValue:
345357
passing = False
346358
for attr in attributeValue:
347-
if evalConditionValue(cond, attr):
359+
if evalConditionValue(cond, attr, savedGroups):
348360
passing = True
349361
if not passing:
350362
return False
@@ -356,7 +368,7 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
356368
elif operator == "$type":
357369
return getType(attributeValue) == conditionValue
358370
elif operator == "$not":
359-
return not evalConditionValue(conditionValue, attributeValue)
371+
return not evalConditionValue(conditionValue, attributeValue, savedGroups)
360372
return False
361373

362374

@@ -893,15 +905,15 @@ def _fetch_features(
893905
if not decryption_key:
894906
raise ValueError("Must specify decryption_key")
895907
try:
896-
decrypted = decrypt(decoded["encryptedFeatures"], decryption_key)
908+
decrypted = decrypt(decoded['encryptedFeatures'], decryption_key)
897909
return json.loads(decrypted)
898910
except Exception:
899911
logger.warning(
900912
"Failed to decrypt features from GrowthBook API response"
901913
)
902914
return None
903915
elif "features" in decoded:
904-
return decoded["features"]
916+
return decoded
905917
else:
906918
logger.warning("GrowthBook API response missing features")
907919
return None
@@ -917,15 +929,17 @@ async def _fetch_features_async(
917929
if not decryption_key:
918930
raise ValueError("Must specify decryption_key")
919931
try:
920-
decrypted = decrypt(decoded["encryptedFeatures"], decryption_key)
921-
return json.loads(decrypted)
932+
decryptedFeatures = decrypt(decoded["encryptedFeatures"], decryption_key)
933+
decoded['features'] = json.loads(decryptedFeatures)
934+
del decoded['encryptedFeatures']
935+
return decoded
922936
except Exception:
923937
logger.warning(
924938
"Failed to decrypt features from GrowthBook API response"
925939
)
926940
return None
927941
elif "features" in decoded:
928-
return decoded["features"]
942+
return decoded
929943
else:
930944
logger.warning("GrowthBook API response missing features")
931945
return None
@@ -956,6 +970,7 @@ def __init__(
956970
forced_variations: dict = {},
957971
sticky_bucket_service: AbstractStickyBucketService = None,
958972
sticky_bucket_identifier_attributes: List[str] = None,
973+
savedGroups: dict = {},
959974
# Deprecated args
960975
trackingCallback=None,
961976
qaMode: bool = False,
@@ -968,6 +983,7 @@ def __init__(
968983
self._attributes = attributes
969984
self._url = url
970985
self._features: Dict[str, Feature] = {}
986+
self._saved_groups = savedGroups
971987
self._api_host = api_host
972988
self._client_key = client_key
973989
self._decryption_key = decryption_key
@@ -998,11 +1014,14 @@ def load_features(self) -> None:
9981014
if not self._client_key:
9991015
raise ValueError("Must specify `client_key` to refresh features")
10001016

1001-
features = feature_repo.load_features(
1017+
response = feature_repo.load_features(
10021018
self._api_host, self._client_key, self._decryption_key, self._cache_ttl
10031019
)
1004-
if features is not None:
1005-
self.setFeatures(features)
1020+
if response is not None and "features" in response.keys():
1021+
self.setFeatures(response["features"])
1022+
1023+
if response is not None and "savedGroups" in response:
1024+
self._saved_groups = response["savedGroups"]
10061025

10071026
async def load_features_async(self) -> None:
10081027
if not self._client_key:
@@ -1011,8 +1030,11 @@ async def load_features_async(self) -> None:
10111030
features = await feature_repo.load_features_async(
10121031
self._api_host, self._client_key, self._decryption_key, self._cache_ttl
10131032
)
1014-
if features is not None:
1015-
self.setFeatures(features)
1033+
if features is not None and "features" in features:
1034+
self.setFeatures(features["features"])
1035+
1036+
if features is not None and "savedGroups" in features:
1037+
self._saved_groups = features["savedGroups"]
10161038

10171039
# @deprecated, use set_features
10181040
def setFeatures(self, features: dict) -> None:
@@ -1096,7 +1118,7 @@ def eval_prereqs(self, parentConditions: List[dict], stack: Set[str]) -> str:
10961118
if parentRes.source == "cyclicPrerequisite":
10971119
return "cyclic"
10981120

1099-
if not evalCondition({'value': parentRes.value}, parentCondition.get("condition", None)):
1121+
if not evalCondition({'value': parentRes.value}, parentCondition.get("condition", None), self._saved_groups):
11001122
if parentCondition.get("gate", False):
11011123
return "gate"
11021124
return "fail"
@@ -1132,7 +1154,7 @@ def _eval_feature(self, key: str, stack: Set[str]) -> FeatureResult:
11321154
continue
11331155

11341156
if rule.condition:
1135-
if not evalCondition(self._attributes, rule.condition):
1157+
if not evalCondition(self._attributes, rule.condition, self._saved_groups):
11361158
logger.debug(
11371159
"Skip rule because of failed condition, feature %s", key
11381160
)
@@ -1412,7 +1434,7 @@ def _run(self, experiment: Experiment, featureId: Optional[str] = None) -> Resul
14121434

14131435
# 8. Exclude if condition is false
14141436
if experiment.condition and not evalCondition(
1415-
self._attributes, experiment.condition
1437+
self._attributes, experiment.condition, self._saved_groups
14161438
):
14171439
logger.debug(
14181440
"Skip experiment %s because user failed the condition", experiment.key

0 commit comments

Comments
 (0)