Skip to content

Commit cd1c4f8

Browse files
authored
Merge pull request #19 from growthbook/feature/load-features-async
Feature/load features async
2 parents 73adb6b + 9222789 commit cd1c4f8

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

growthbook/growthbook.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from urllib.parse import urlparse, parse_qs
2323
from base64 import b64decode
2424
from time import time
25+
import aiohttp
26+
2527
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
2628
from cryptography.hazmat.primitives import padding
2729
from urllib3 import PoolManager
@@ -828,12 +830,26 @@ def load_features(
828830
logger.debug("Fetched features from API, stored in cache")
829831
return res
830832
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
831847

832848
# Perform the GET request (separate method for easy mocking)
833849
def _get(self, url: str):
834850
self.http = self.http or PoolManager()
835851
return self.http.request("GET", url)
836-
852+
837853
def _fetch_and_decode(self, api_host: str, client_key: str) -> Optional[Dict]:
838854
try:
839855
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]:
847863
except Exception:
848864
logger.warning("Failed to decode feature JSON from GrowthBook API")
849865
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
850883

851884
# Fetch features from the GrowthBook API
852885
def _fetch_features(
@@ -872,6 +905,30 @@ def _fetch_features(
872905
else:
873906
logger.warning("GrowthBook API response missing features")
874907
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
875932

876933
@staticmethod
877934
def _get_features_url(api_host: str, client_key: str) -> str:
@@ -947,6 +1004,16 @@ def load_features(self) -> None:
9471004
if features is not None:
9481005
self.setFeatures(features)
9491006

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+
9501017
# @deprecated, use set_features
9511018
def setFeatures(self, features: dict) -> None:
9521019
return self.set_features(features)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
cryptography>=35.0.0
22
typing_extensions>=3.8.0
3-
urllib3>=1.26.0
3+
urllib3>=1.26.0
4+
aiohttp>=3.8.6

0 commit comments

Comments
 (0)