@@ -165,36 +165,36 @@ def isIn(conditionValue, attributeValue) -> bool:
165
165
return attributeValue in conditionValue
166
166
167
167
168
- def evalCondition (attributes : dict , condition : dict ) -> bool :
168
+ def evalCondition (attributes : dict , condition : dict , savedGroups : dict = None ) -> bool :
169
169
if "$or" in condition :
170
- return evalOr (attributes , condition ["$or" ])
170
+ return evalOr (attributes , condition ["$or" ], savedGroups )
171
171
if "$nor" in condition :
172
- return not evalOr (attributes , condition ["$nor" ])
172
+ return not evalOr (attributes , condition ["$nor" ], savedGroups )
173
173
if "$and" in condition :
174
- return evalAnd (attributes , condition ["$and" ])
174
+ return evalAnd (attributes , condition ["$and" ], savedGroups )
175
175
if "$not" in condition :
176
- return not evalCondition (attributes , condition ["$not" ])
176
+ return not evalCondition (attributes , condition ["$not" ], savedGroups )
177
177
178
178
for key , value in condition .items ():
179
- if not evalConditionValue (value , getPath (attributes , key )):
179
+ if not evalConditionValue (value , getPath (attributes , key ), savedGroups ):
180
180
return False
181
181
182
182
return True
183
183
184
184
185
- def evalOr (attributes , conditions ) -> bool :
185
+ def evalOr (attributes , conditions , savedGroups ) -> bool :
186
186
if len (conditions ) == 0 :
187
187
return True
188
188
189
189
for condition in conditions :
190
- if evalCondition (attributes , condition ):
190
+ if evalCondition (attributes , condition , savedGroups ):
191
191
return True
192
192
return False
193
193
194
194
195
- def evalAnd (attributes , conditions ) -> bool :
195
+ def evalAnd (attributes , conditions , savedGroups ) -> bool :
196
196
for condition in conditions :
197
- if not evalCondition (attributes , condition ):
197
+ if not evalCondition (attributes , condition , savedGroups ):
198
198
return False
199
199
return True
200
200
@@ -234,25 +234,25 @@ def getPath(attributes, path):
234
234
return current
235
235
236
236
237
- def evalConditionValue (conditionValue , attributeValue ) -> bool :
237
+ def evalConditionValue (conditionValue , attributeValue , savedGroups ) -> bool :
238
238
if type (conditionValue ) is dict and isOperatorObject (conditionValue ):
239
239
for key , value in conditionValue .items ():
240
- if not evalOperatorCondition (key , attributeValue , value ):
240
+ if not evalOperatorCondition (key , attributeValue , value , savedGroups ):
241
241
return False
242
242
return True
243
243
return conditionValue == attributeValue
244
244
245
245
246
- def elemMatch (condition , attributeValue ) -> bool :
246
+ def elemMatch (condition , attributeValue , savedGroups ) -> bool :
247
247
if not type (attributeValue ) is list :
248
248
return False
249
249
250
250
for item in attributeValue :
251
251
if isOperatorObject (condition ):
252
- if evalConditionValue (condition , item ):
252
+ if evalConditionValue (condition , item , savedGroups ):
253
253
return True
254
254
else :
255
- if evalCondition (item , condition ):
255
+ if evalCondition (item , condition , savedGroups ):
256
256
return True
257
257
258
258
return False
@@ -278,7 +278,7 @@ def compare(val1, val2) -> int:
278
278
return 0
279
279
280
280
281
- def evalOperatorCondition (operator , attributeValue , conditionValue ) -> bool :
281
+ def evalOperatorCondition (operator , attributeValue , conditionValue , savedGroups ) -> bool :
282
282
if operator == "$eq" :
283
283
try :
284
284
return compare (attributeValue , conditionValue ) == 0
@@ -321,6 +321,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
321
321
return paddedVersionString (attributeValue ) > paddedVersionString (conditionValue )
322
322
elif operator == "$vgte" :
323
323
return paddedVersionString (attributeValue ) >= paddedVersionString (conditionValue )
324
+ elif operator == "$inGroup" :
325
+ if not type (conditionValue ) is str :
326
+ return False
327
+ if not conditionValue in savedGroups :
328
+ return False
329
+ return isIn (savedGroups [conditionValue ] or [], attributeValue )
330
+ elif operator == "$notInGroup" :
331
+ if not type (conditionValue ) is str :
332
+ return False
333
+ if not conditionValue in savedGroups :
334
+ return True
335
+ return not isIn (savedGroups [conditionValue ] or [], attributeValue )
324
336
elif operator == "$regex" :
325
337
try :
326
338
r = re .compile (conditionValue )
@@ -336,18 +348,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
336
348
return False
337
349
return not isIn (conditionValue , attributeValue )
338
350
elif operator == "$elemMatch" :
339
- return elemMatch (conditionValue , attributeValue )
351
+ return elemMatch (conditionValue , attributeValue , savedGroups )
340
352
elif operator == "$size" :
341
353
if not (type (attributeValue ) is list ):
342
354
return False
343
- return evalConditionValue (conditionValue , len (attributeValue ))
355
+ return evalConditionValue (conditionValue , len (attributeValue ), savedGroups )
344
356
elif operator == "$all" :
345
357
if not (type (attributeValue ) is list ):
346
358
return False
347
359
for cond in conditionValue :
348
360
passing = False
349
361
for attr in attributeValue :
350
- if evalConditionValue (cond , attr ):
362
+ if evalConditionValue (cond , attr , savedGroups ):
351
363
passing = True
352
364
if not passing :
353
365
return False
@@ -359,7 +371,7 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool:
359
371
elif operator == "$type" :
360
372
return getType (attributeValue ) == conditionValue
361
373
elif operator == "$not" :
362
- return not evalConditionValue (conditionValue , attributeValue )
374
+ return not evalConditionValue (conditionValue , attributeValue , savedGroups )
363
375
return False
364
376
365
377
@@ -947,6 +959,9 @@ def set_cache(self, cache: AbstractFeatureCache) -> None:
947
959
def clear_cache (self ):
948
960
self .cache .clear ()
949
961
962
+ def save_in_cache (self , key : str , res , ttl : int = 60 ):
963
+ self .cache .set (key , res , ttl )
964
+
950
965
# Loads features with an in-memory cache in front
951
966
def load_features (
952
967
self , api_host : str , client_key : str , decryption_key : str = "" , ttl : int = 60
@@ -1011,31 +1026,49 @@ async def _fetch_and_decode_async(self, api_host: str, client_key: str) -> Optio
1011
1026
except Exception as e :
1012
1027
logger .warning ("Failed to decode feature JSON from GrowthBook API: %s" , e )
1013
1028
return None
1014
-
1015
- # Fetch features from the GrowthBook API
1016
- def _fetch_features (
1017
- self , api_host : str , client_key : str , decryption_key : str = ""
1018
- ) -> Optional [Dict ]:
1019
- decoded = self ._fetch_and_decode (api_host , client_key )
1020
- if not decoded :
1021
- return None
1022
-
1023
- if "encryptedFeatures" in decoded :
1029
+
1030
+ def decrypt_response (self , data , decryption_key : str ):
1031
+ if "encryptedFeatures" in data :
1024
1032
if not decryption_key :
1025
1033
raise ValueError ("Must specify decryption_key" )
1026
1034
try :
1027
- decrypted = decrypt (decoded ["encryptedFeatures" ], decryption_key )
1028
- return json .loads (decrypted )
1035
+ decryptedFeatures = decrypt (data ["encryptedFeatures" ], decryption_key )
1036
+ data ['features' ] = json .loads (decryptedFeatures )
1037
+ del data ['encryptedFeatures' ]
1029
1038
except Exception :
1030
1039
logger .warning (
1031
1040
"Failed to decrypt features from GrowthBook API response"
1032
1041
)
1033
1042
return None
1034
- elif "features" in decoded :
1035
- return decoded ["features" ]
1036
- else :
1043
+ elif "features" not in data :
1037
1044
logger .warning ("GrowthBook API response missing features" )
1045
+
1046
+ if "encryptedSavedGroups" in data :
1047
+ if not decryption_key :
1048
+ raise ValueError ("Must specify decryption_key" )
1049
+ try :
1050
+ decryptedFeatures = decrypt (data ["encryptedSavedGroups" ], decryption_key )
1051
+ data ['savedGroups' ] = json .loads (decryptedFeatures )
1052
+ del data ['encryptedSavedGroups' ]
1053
+ return data
1054
+ except Exception :
1055
+ logger .warning (
1056
+ "Failed to decrypt saved groups from GrowthBook API response"
1057
+ )
1058
+
1059
+ return data
1060
+
1061
+ # Fetch features from the GrowthBook API
1062
+ def _fetch_features (
1063
+ self , api_host : str , client_key : str , decryption_key : str = ""
1064
+ ) -> Optional [Dict ]:
1065
+ decoded = self ._fetch_and_decode (api_host , client_key )
1066
+ if not decoded :
1038
1067
return None
1068
+
1069
+ data = self .decrypt_response (decoded , decryption_key )
1070
+
1071
+ return data
1039
1072
1040
1073
async def _fetch_features_async (
1041
1074
self , api_host : str , client_key : str , decryption_key : str = ""
@@ -1044,22 +1077,9 @@ async def _fetch_features_async(
1044
1077
if not decoded :
1045
1078
return None
1046
1079
1047
- if "encryptedFeatures" in decoded :
1048
- if not decryption_key :
1049
- raise ValueError ("Must specify decryption_key" )
1050
- try :
1051
- decrypted = decrypt (decoded ["encryptedFeatures" ], decryption_key )
1052
- return json .loads (decrypted )
1053
- except Exception :
1054
- logger .warning (
1055
- "Failed to decrypt features from GrowthBook API response"
1056
- )
1057
- return None
1058
- elif "features" in decoded :
1059
- return decoded ["features" ]
1060
- else :
1061
- logger .warning ("GrowthBook API response missing features" )
1062
- return None
1080
+ data = self .decrypt_response (decoded , decryption_key )
1081
+
1082
+ return data
1063
1083
1064
1084
1065
1085
def startAutoRefresh (self , api_host , client_key , cb ):
@@ -1094,6 +1114,7 @@ def __init__(
1094
1114
forced_variations : dict = {},
1095
1115
sticky_bucket_service : AbstractStickyBucketService = None ,
1096
1116
sticky_bucket_identifier_attributes : List [str ] = None ,
1117
+ savedGroups : dict = {},
1097
1118
streaming : bool = False ,
1098
1119
# Deprecated args
1099
1120
trackingCallback = None ,
@@ -1107,6 +1128,7 @@ def __init__(
1107
1128
self ._attributes = attributes
1108
1129
self ._url = url
1109
1130
self ._features : Dict [str , Feature ] = {}
1131
+ self ._saved_groups = savedGroups
1110
1132
self ._api_host = api_host
1111
1133
self ._client_key = client_key
1112
1134
self ._decryption_key = decryption_key
@@ -1143,11 +1165,14 @@ def load_features(self) -> None:
1143
1165
if not self ._client_key :
1144
1166
raise ValueError ("Must specify `client_key` to refresh features" )
1145
1167
1146
- features = feature_repo .load_features (
1168
+ response = feature_repo .load_features (
1147
1169
self ._api_host , self ._client_key , self ._decryption_key , self ._cache_ttl
1148
1170
)
1149
- if features is not None :
1150
- self .setFeatures (features )
1171
+ if response is not None and "features" in response .keys ():
1172
+ self .setFeatures (response ["features" ])
1173
+
1174
+ if response is not None and "savedGroups" in response :
1175
+ self ._saved_groups = response ["savedGroups" ]
1151
1176
1152
1177
async def load_features_async (self ) -> None :
1153
1178
if not self ._client_key :
@@ -1156,37 +1181,35 @@ async def load_features_async(self) -> None:
1156
1181
features = await feature_repo .load_features_async (
1157
1182
self ._api_host , self ._client_key , self ._decryption_key , self ._cache_ttl
1158
1183
)
1184
+
1159
1185
if features is not None :
1160
- self .setFeatures (features )
1186
+ if "features" in features :
1187
+ self .setFeatures (features ["features" ])
1188
+ if "savedGroups" in features :
1189
+ self ._saved_groups = features ["savedGroups" ]
1190
+ feature_repo .save_in_cache (self ._client_key , features , self ._cache_ttl )
1161
1191
1162
- def features_event_handler (self , features ):
1192
+ def _features_event_handler (self , features ):
1163
1193
decoded = json .loads (features )
1164
1194
if not decoded :
1165
1195
return None
1166
-
1167
- if "encryptedFeatures" in decoded :
1168
- if not self ._decryption_key :
1169
- raise ValueError ("Must specify decryption_key" )
1170
- try :
1171
- decrypted = decrypt (decoded ["encryptedFeatures" ], self ._decryption_key )
1172
- return json .loads (decrypted )
1173
- except Exception :
1174
- logger .warning (
1175
- "Failed to decrypt features from GrowthBook API response"
1176
- )
1177
- return None
1178
- elif "features" in decoded :
1179
- self .set_features (decoded ["features" ])
1180
- else :
1181
- logger .warning ("GrowthBook API response missing features" )
1196
+
1197
+ data = feature_repo .decrypt_response (decoded , self ._decryption_key )
1198
+
1199
+ if data is not None :
1200
+ if "features" in data :
1201
+ self .setFeatures (data ["features" ])
1202
+ if "savedGroups" in data :
1203
+ self ._saved_groups = data ["savedGroups" ]
1204
+ feature_repo .save_in_cache (self ._client_key , features , self ._cache_ttl )
1182
1205
1183
- def dispatch_sse_event (self , event_data ):
1206
+ def _dispatch_sse_event (self , event_data ):
1184
1207
event_type = event_data ['type' ]
1185
1208
data = event_data ['data' ]
1186
1209
if event_type == 'features-updated' :
1187
1210
self .load_features ()
1188
1211
elif event_type == 'features' :
1189
- self .features_event_handler (data )
1212
+ self ._features_event_handler (data )
1190
1213
1191
1214
1192
1215
def startAutoRefresh (self ):
@@ -1196,7 +1219,7 @@ def startAutoRefresh(self):
1196
1219
feature_repo .startAutoRefresh (
1197
1220
api_host = self ._api_host ,
1198
1221
client_key = self ._client_key ,
1199
- cb = self .dispatch_sse_event
1222
+ cb = self ._dispatch_sse_event
1200
1223
)
1201
1224
1202
1225
def stopAutoRefresh (self ):
@@ -1284,7 +1307,7 @@ def eval_prereqs(self, parentConditions: List[dict], stack: Set[str]) -> str:
1284
1307
if parentRes .source == "cyclicPrerequisite" :
1285
1308
return "cyclic"
1286
1309
1287
- if not evalCondition ({'value' : parentRes .value }, parentCondition .get ("condition" , None )):
1310
+ if not evalCondition ({'value' : parentRes .value }, parentCondition .get ("condition" , None ), self . _saved_groups ):
1288
1311
if parentCondition .get ("gate" , False ):
1289
1312
return "gate"
1290
1313
return "fail"
@@ -1320,7 +1343,7 @@ def _eval_feature(self, key: str, stack: Set[str]) -> FeatureResult:
1320
1343
continue
1321
1344
1322
1345
if rule .condition :
1323
- if not evalCondition (self ._attributes , rule .condition ):
1346
+ if not evalCondition (self ._attributes , rule .condition , self . _saved_groups ):
1324
1347
logger .debug (
1325
1348
"Skip rule because of failed condition, feature %s" , key
1326
1349
)
@@ -1600,7 +1623,7 @@ def _run(self, experiment: Experiment, featureId: Optional[str] = None) -> Resul
1600
1623
1601
1624
# 8. Exclude if condition is false
1602
1625
if experiment .condition and not evalCondition (
1603
- self ._attributes , experiment .condition
1626
+ self ._attributes , experiment .condition , self . _saved_groups
1604
1627
):
1605
1628
logger .debug (
1606
1629
"Skip experiment %s because user failed the condition" , experiment .key
0 commit comments