22
22
from urllib .parse import urlparse , parse_qs
23
23
from base64 import b64decode
24
24
from time import time
25
+ import aiohttp
26
+
25
27
from cryptography .hazmat .primitives .ciphers import Cipher , algorithms , modes
26
28
from cryptography .hazmat .primitives import padding
27
29
from urllib3 import PoolManager
@@ -828,12 +830,26 @@ def load_features(
828
830
logger .debug ("Fetched features from API, stored in cache" )
829
831
return res
830
832
return cached
833
+
834
+ async def load_features_async (
835
+ self , api_host : str , client_key : str , decryption_key : str = "" , ttl : int = 60
836
+ ) -> Optional [Dict ]:
837
+ key = api_host + "::" + client_key
838
+
839
+ cached = self .cache .get (key )
840
+ if not cached :
841
+ res = await self ._fetch_features_async (api_host , client_key , decryption_key )
842
+ if res is not None :
843
+ self .cache .set (key , res , ttl )
844
+ logger .debug ("Fetched features from API, stored in cache" )
845
+ return res
846
+ return cached
831
847
832
848
# Perform the GET request (separate method for easy mocking)
833
849
def _get (self , url : str ):
834
850
self .http = self .http or PoolManager ()
835
851
return self .http .request ("GET" , url )
836
-
852
+
837
853
def _fetch_and_decode (self , api_host : str , client_key : str ) -> Optional [Dict ]:
838
854
try :
839
855
r = self ._get (self ._get_features_url (api_host , client_key ))
@@ -847,6 +863,23 @@ def _fetch_and_decode(self, api_host: str, client_key: str) -> Optional[Dict]:
847
863
except Exception :
848
864
logger .warning ("Failed to decode feature JSON from GrowthBook API" )
849
865
return None
866
+
867
+ async def _fetch_and_decode_async (self , api_host : str , client_key : str ) -> Optional [Dict ]:
868
+ try :
869
+ url = self ._get_features_url (api_host , client_key )
870
+ async with aiohttp .ClientSession () as session :
871
+ async with session .get (url ) as response :
872
+ if response .status >= 400 :
873
+ logger .warning ("Failed to fetch features, received status code %d" , response .status )
874
+ return None
875
+ decoded = await response .json ()
876
+ return decoded
877
+ except aiohttp .ClientError as e :
878
+ logger .warning (f"HTTP request failed: { e } " )
879
+ return None
880
+ except Exception as e :
881
+ logger .warning ("Failed to decode feature JSON from GrowthBook API: %s" , e )
882
+ return None
850
883
851
884
# Fetch features from the GrowthBook API
852
885
def _fetch_features (
@@ -872,6 +905,30 @@ def _fetch_features(
872
905
else :
873
906
logger .warning ("GrowthBook API response missing features" )
874
907
return None
908
+
909
+ async def _fetch_features_async (
910
+ self , api_host : str , client_key : str , decryption_key : str = ""
911
+ ) -> Optional [Dict ]:
912
+ decoded = await self ._fetch_and_decode_async (api_host , client_key )
913
+ if not decoded :
914
+ return None
915
+
916
+ if "encryptedFeatures" in decoded :
917
+ if not decryption_key :
918
+ raise ValueError ("Must specify decryption_key" )
919
+ try :
920
+ decrypted = decrypt (decoded ["encryptedFeatures" ], decryption_key )
921
+ return json .loads (decrypted )
922
+ except Exception :
923
+ logger .warning (
924
+ "Failed to decrypt features from GrowthBook API response"
925
+ )
926
+ return None
927
+ elif "features" in decoded :
928
+ return decoded ["features" ]
929
+ else :
930
+ logger .warning ("GrowthBook API response missing features" )
931
+ return None
875
932
876
933
@staticmethod
877
934
def _get_features_url (api_host : str , client_key : str ) -> str :
@@ -947,6 +1004,16 @@ def load_features(self) -> None:
947
1004
if features is not None :
948
1005
self .setFeatures (features )
949
1006
1007
+ async def load_features_async (self ) -> None :
1008
+ if not self ._client_key :
1009
+ raise ValueError ("Must specify `client_key` to refresh features" )
1010
+
1011
+ features = await feature_repo .load_features_async (
1012
+ self ._api_host , self ._client_key , self ._decryption_key , self ._cache_ttl
1013
+ )
1014
+ if features is not None :
1015
+ self .setFeatures (features )
1016
+
950
1017
# @deprecated, use set_features
951
1018
def setFeatures (self , features : dict ) -> None :
952
1019
return self .set_features (features )
0 commit comments