@@ -162,36 +162,36 @@ def isIn(conditionValue, attributeValue) -> bool:
162
162
return attributeValue in conditionValue
163
163
164
164
165
- def evalCondition (attributes : dict , condition : dict ) -> bool :
165
+ def evalCondition (attributes : dict , condition : dict , savedGroups : dict = None ) -> bool :
166
166
if "$or" in condition :
167
- return evalOr (attributes , condition ["$or" ])
167
+ return evalOr (attributes , condition ["$or" ], savedGroups )
168
168
if "$nor" in condition :
169
- return not evalOr (attributes , condition ["$nor" ])
169
+ return not evalOr (attributes , condition ["$nor" ], savedGroups )
170
170
if "$and" in condition :
171
- return evalAnd (attributes , condition ["$and" ])
171
+ return evalAnd (attributes , condition ["$and" ], savedGroups )
172
172
if "$not" in condition :
173
- return not evalCondition (attributes , condition ["$not" ])
173
+ return not evalCondition (attributes , condition ["$not" ], savedGroups )
174
174
175
175
for key , value in condition .items ():
176
- if not evalConditionValue (value , getPath (attributes , key )):
176
+ if not evalConditionValue (value , getPath (attributes , key ), savedGroups ):
177
177
return False
178
178
179
179
return True
180
180
181
181
182
- def evalOr (attributes , conditions ) -> bool :
182
+ def evalOr (attributes , conditions , savedGroups ) -> bool :
183
183
if len (conditions ) == 0 :
184
184
return True
185
185
186
186
for condition in conditions :
187
- if evalCondition (attributes , condition ):
187
+ if evalCondition (attributes , condition , savedGroups ):
188
188
return True
189
189
return False
190
190
191
191
192
- def evalAnd (attributes , conditions ) -> bool :
192
+ def evalAnd (attributes , conditions , savedGroups ) -> bool :
193
193
for condition in conditions :
194
- if not evalCondition (attributes , condition ):
194
+ if not evalCondition (attributes , condition , savedGroups ):
195
195
return False
196
196
return True
197
197
@@ -231,25 +231,25 @@ def getPath(attributes, path):
231
231
return current
232
232
233
233
234
- def evalConditionValue (conditionValue , attributeValue ) -> bool :
234
+ def evalConditionValue (conditionValue , attributeValue , savedGroups ) -> bool :
235
235
if type (conditionValue ) is dict and isOperatorObject (conditionValue ):
236
236
for key , value in conditionValue .items ():
237
- if not evalOperatorCondition (key , attributeValue , value ):
237
+ if not evalOperatorCondition (key , attributeValue , value , savedGroups ):
238
238
return False
239
239
return True
240
240
return conditionValue == attributeValue
241
241
242
242
243
- def elemMatch (condition , attributeValue ) -> bool :
243
+ def elemMatch (condition , attributeValue , savedGroups ) -> bool :
244
244
if not type (attributeValue ) is list :
245
245
return False
246
246
247
247
for item in attributeValue :
248
248
if isOperatorObject (condition ):
249
- if evalConditionValue (condition , item ):
249
+ if evalConditionValue (condition , item , savedGroups ):
250
250
return True
251
251
else :
252
- if evalCondition (item , condition ):
252
+ if evalCondition (item , condition , savedGroups ):
253
253
return True
254
254
255
255
return False
@@ -275,7 +275,7 @@ def compare(val1, val2) -> int:
275
275
return 0
276
276
277
277
278
- def evalOperatorCondition (operator , attributeValue , conditionValue ) -> bool :
278
+ def evalOperatorCondition (operator , attributeValue , conditionValue , savedGroups ) -> bool :
279
279
if operator == "$eq" :
280
280
try :
281
281
return compare (attributeValue , conditionValue ) == 0
@@ -318,6 +318,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
318
318
return paddedVersionString (attributeValue ) > paddedVersionString (conditionValue )
319
319
elif operator == "$vgte" :
320
320
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 )
321
333
elif operator == "$regex" :
322
334
try :
323
335
r = re .compile (conditionValue )
@@ -333,18 +345,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
333
345
return False
334
346
return not isIn (conditionValue , attributeValue )
335
347
elif operator == "$elemMatch" :
336
- return elemMatch (conditionValue , attributeValue )
348
+ return elemMatch (conditionValue , attributeValue , savedGroups )
337
349
elif operator == "$size" :
338
350
if not (type (attributeValue ) is list ):
339
351
return False
340
- return evalConditionValue (conditionValue , len (attributeValue ))
352
+ return evalConditionValue (conditionValue , len (attributeValue ), savedGroups )
341
353
elif operator == "$all" :
342
354
if not (type (attributeValue ) is list ):
343
355
return False
344
356
for cond in conditionValue :
345
357
passing = False
346
358
for attr in attributeValue :
347
- if evalConditionValue (cond , attr ):
359
+ if evalConditionValue (cond , attr , savedGroups ):
348
360
passing = True
349
361
if not passing :
350
362
return False
@@ -356,7 +368,7 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
356
368
elif operator == "$type" :
357
369
return getType (attributeValue ) == conditionValue
358
370
elif operator == "$not" :
359
- return not evalConditionValue (conditionValue , attributeValue )
371
+ return not evalConditionValue (conditionValue , attributeValue , savedGroups )
360
372
return False
361
373
362
374
@@ -893,15 +905,15 @@ def _fetch_features(
893
905
if not decryption_key :
894
906
raise ValueError ("Must specify decryption_key" )
895
907
try :
896
- decrypted = decrypt (decoded [" encryptedFeatures" ], decryption_key )
908
+ decrypted = decrypt (decoded [' encryptedFeatures' ], decryption_key )
897
909
return json .loads (decrypted )
898
910
except Exception :
899
911
logger .warning (
900
912
"Failed to decrypt features from GrowthBook API response"
901
913
)
902
914
return None
903
915
elif "features" in decoded :
904
- return decoded [ "features" ]
916
+ return decoded
905
917
else :
906
918
logger .warning ("GrowthBook API response missing features" )
907
919
return None
@@ -917,15 +929,17 @@ async def _fetch_features_async(
917
929
if not decryption_key :
918
930
raise ValueError ("Must specify decryption_key" )
919
931
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
922
936
except Exception :
923
937
logger .warning (
924
938
"Failed to decrypt features from GrowthBook API response"
925
939
)
926
940
return None
927
941
elif "features" in decoded :
928
- return decoded [ "features" ]
942
+ return decoded
929
943
else :
930
944
logger .warning ("GrowthBook API response missing features" )
931
945
return None
@@ -956,6 +970,7 @@ def __init__(
956
970
forced_variations : dict = {},
957
971
sticky_bucket_service : AbstractStickyBucketService = None ,
958
972
sticky_bucket_identifier_attributes : List [str ] = None ,
973
+ savedGroups : dict = {},
959
974
# Deprecated args
960
975
trackingCallback = None ,
961
976
qaMode : bool = False ,
@@ -968,6 +983,7 @@ def __init__(
968
983
self ._attributes = attributes
969
984
self ._url = url
970
985
self ._features : Dict [str , Feature ] = {}
986
+ self ._saved_groups = savedGroups
971
987
self ._api_host = api_host
972
988
self ._client_key = client_key
973
989
self ._decryption_key = decryption_key
@@ -998,11 +1014,14 @@ def load_features(self) -> None:
998
1014
if not self ._client_key :
999
1015
raise ValueError ("Must specify `client_key` to refresh features" )
1000
1016
1001
- features = feature_repo .load_features (
1017
+ response = feature_repo .load_features (
1002
1018
self ._api_host , self ._client_key , self ._decryption_key , self ._cache_ttl
1003
1019
)
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" ]
1006
1025
1007
1026
async def load_features_async (self ) -> None :
1008
1027
if not self ._client_key :
@@ -1011,8 +1030,11 @@ async def load_features_async(self) -> None:
1011
1030
features = await feature_repo .load_features_async (
1012
1031
self ._api_host , self ._client_key , self ._decryption_key , self ._cache_ttl
1013
1032
)
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" ]
1016
1038
1017
1039
# @deprecated, use set_features
1018
1040
def setFeatures (self , features : dict ) -> None :
@@ -1096,7 +1118,7 @@ def eval_prereqs(self, parentConditions: List[dict], stack: Set[str]) -> str:
1096
1118
if parentRes .source == "cyclicPrerequisite" :
1097
1119
return "cyclic"
1098
1120
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 ):
1100
1122
if parentCondition .get ("gate" , False ):
1101
1123
return "gate"
1102
1124
return "fail"
@@ -1132,7 +1154,7 @@ def _eval_feature(self, key: str, stack: Set[str]) -> FeatureResult:
1132
1154
continue
1133
1155
1134
1156
if rule .condition :
1135
- if not evalCondition (self ._attributes , rule .condition ):
1157
+ if not evalCondition (self ._attributes , rule .condition , self . _saved_groups ):
1136
1158
logger .debug (
1137
1159
"Skip rule because of failed condition, feature %s" , key
1138
1160
)
@@ -1412,7 +1434,7 @@ def _run(self, experiment: Experiment, featureId: Optional[str] = None) -> Resul
1412
1434
1413
1435
# 8. Exclude if condition is false
1414
1436
if experiment .condition and not evalCondition (
1415
- self ._attributes , experiment .condition
1437
+ self ._attributes , experiment .condition , self . _saved_groups
1416
1438
):
1417
1439
logger .debug (
1418
1440
"Skip experiment %s because user failed the condition" , experiment .key
0 commit comments