From 1b02f42429ce4f9a2cf7cab5f6ac2abfe11576de Mon Sep 17 00:00:00 2001 From: Madhu Chavva Date: Mon, 23 Feb 2026 14:40:28 -0800 Subject: [PATCH 1/5] add async custom cache type --- growthbook/__init__.py | 4 +++- growthbook/common_types.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/growthbook/__init__.py b/growthbook/__init__.py index eeca012..ff46447 100644 --- a/growthbook/__init__.py +++ b/growthbook/__init__.py @@ -1,10 +1,12 @@ from .growthbook import * +from .common_types import AbstractAsyncFeatureCache from .growthbook_client import ( GrowthBookClient, EnhancedFeatureRepository, FeatureCache, - BackoffStrategy + BackoffStrategy, + InMemoryAsyncFeatureCache ) # Plugin support diff --git a/growthbook/common_types.py b/growthbook/common_types.py index 916f4d3..f6455e1 100644 --- a/growthbook/common_types.py +++ b/growthbook/common_types.py @@ -414,6 +414,18 @@ class UserContext: sticky_bucket_assignment_docs: Dict[str, Any] = field(default_factory=dict) skip_all_experiments: bool = False +class AbstractAsyncFeatureCache(ABC): + @abstractmethod + async def get(self, key: str) -> Optional[Dict]: + pass + + @abstractmethod + async def set(self, key: str, value: Dict, ttl: int) -> None: + pass + + async def clear(self) -> None: + pass + @dataclass class Options: url: Optional[str] = None @@ -431,6 +443,7 @@ class Options: on_experiment_viewed: Optional[Callable[[Experiment, Result, Optional[UserContext]], None]] = None on_feature_usage: Optional[Callable[[str, 'FeatureResult', UserContext], None]] = None tracking_plugins: Optional[List[Any]] = None + cache: Optional[AbstractAsyncFeatureCache] = None @dataclass From 45e8013ce71b45e50a5b033e5d9975979df94884 Mon Sep 17 00:00:00 2001 From: Madhu Chavva Date: Mon, 23 Feb 2026 14:40:48 -0800 Subject: [PATCH 2/5] changes to support async custom cache --- growthbook/growthbook_client.py | 48 +++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/growthbook/growthbook_client.py b/growthbook/growthbook_client.py index 3a8ef1a..ad095ae 100644 --- a/growthbook/growthbook_client.py +++ b/growthbook/growthbook_client.py @@ -8,8 +8,9 @@ import asyncio import threading import traceback +import time from datetime import datetime -from growthbook import FeatureRepository, feature_repo +from growthbook import FeatureRepository, feature_repo, CacheEntry from contextlib import asynccontextmanager from .core import eval_feature as core_eval_feature, run_experiment @@ -23,7 +24,8 @@ StackContext, FeatureResult, FeatureRefreshStrategy, - Experiment + Experiment, + AbstractAsyncFeatureCache ) logger = logging.getLogger("growthbook.growthbook_client") @@ -101,8 +103,28 @@ def get_current_state(self) -> Dict[str, Any]: "savedGroups": self._cache['savedGroups'] } +class InMemoryAsyncFeatureCache(AbstractAsyncFeatureCache): + def __init__(self) -> None: + self.cache: Dict[str, CacheEntry] = {} + + async def get(self, key: str) -> Optional[Dict]: + if key in self.cache: + entry = self.cache[key] + if entry.expires >= time.time(): + return entry.value + return None + + async def set(self, key: str, value: Dict, ttl: int) -> None: + if key in self.cache: + self.cache[key].update(value) + else: + self.cache[key] = CacheEntry(value, ttl) + + async def clear(self) -> None: + self.cache.clear() + class EnhancedFeatureRepository(FeatureRepository, metaclass=SingletonMeta): - def __init__(self, api_host: str, client_key: str, decryption_key: str = "", cache_ttl: int = 60): + def __init__(self, api_host: str, client_key: str, decryption_key: str = "", cache_ttl: int = 60, cache: Optional[AbstractAsyncFeatureCache] = None): FeatureRepository.__init__(self) self._api_host = api_host self._client_key = client_key @@ -116,6 +138,7 @@ def __init__(self, api_host: str, client_key: str, decryption_key: str = "", cac self._callbacks: List[Callable[[Dict[str, Any]], Awaitable[None]]] = [] self._last_successful_refresh: Optional[datetime] = None self._refresh_in_progress = asyncio.Lock() + self.cache = cache if cache else InMemoryAsyncFeatureCache() @asynccontextmanager async def refresh_operation(self): @@ -333,7 +356,21 @@ async def load_features_async( if api_host == self._api_host and client_key == self._client_key: decryption_key = self._decryption_key ttl = self._cache_ttl - return await super().load_features_async(api_host, client_key, decryption_key, ttl) + + key = api_host + "::" + client_key + + # Try async cache first + cached = await self.cache.get(key) + if cached: + return cached + + # Fetch from network + res = await self._fetch_features_async(api_host, client_key, decryption_key) + if res is not None: + await self.cache.set(key, res, ttl) + return res + + return None class GrowthBookClient: def __init__( @@ -370,7 +407,8 @@ def __init__( self.options.api_host or "https://cdn.growthbook.io", self.options.client_key or "", self.options.decryption_key or "", - self.options.cache_ttl + self.options.cache_ttl, + self.options.cache ) if self.options.client_key else None From 1678ea445d801727c00c0a776c248afe896ab956 Mon Sep 17 00:00:00 2001 From: Madhu Chavva Date: Mon, 23 Feb 2026 14:41:07 -0800 Subject: [PATCH 3/5] test async custom cache --- tests/test_async_custom_cache.py | 73 ++++++++++++++++++++++++++++++++ tests/test_growthbook_client.py | 30 ++++++------- 2 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 tests/test_async_custom_cache.py diff --git a/tests/test_async_custom_cache.py b/tests/test_async_custom_cache.py new file mode 100644 index 0000000..a8bb06f --- /dev/null +++ b/tests/test_async_custom_cache.py @@ -0,0 +1,73 @@ +import pytest +import asyncio +from typing import Dict, Optional +from growthbook import GrowthBookClient, AbstractAsyncFeatureCache, InMemoryAsyncFeatureCache, Options, EnhancedFeatureRepository + +@pytest.fixture(autouse=True) +async def cleanup(): + EnhancedFeatureRepository._instances = {} + yield + EnhancedFeatureRepository._instances = {} + +class CustomAsyncCache(AbstractAsyncFeatureCache): + def __init__(self): + self.cache = {} + self.get_calls = 0 + self.set_calls = 0 + + async def get(self, key: str) -> Optional[Dict]: + self.get_calls += 1 + return self.cache.get(key) + + async def set(self, key: str, value: Dict, ttl: int) -> None: + self.set_calls += 1 + self.cache[key] = value + + async def clear(self) -> None: + self.cache.clear() + +@pytest.mark.asyncio +async def test_default_async_cache(): + # Test that default cache is InMemoryAsyncFeatureCache + client = GrowthBookClient(options=Options(client_key="123", api_host="http://localhost")) + # Access private repo to check cache (white-box testing) + assert isinstance(client._features_repository.cache, InMemoryAsyncFeatureCache) + + # Clean up + await client.close() + +@pytest.mark.asyncio +async def test_custom_async_cache(): + # Force cleanup manually to debug singleton issue + EnhancedFeatureRepository._instances = {} + + custom_cache = CustomAsyncCache() + options = Options( + client_key="123", + api_host="http://localhost", + cache=custom_cache + ) + client = GrowthBookClient(options=options) + + # Debug info + print(f"DEBUG: Client cache option: {client.options.cache}") + print(f"DEBUG: FeatureRepo cache: {client._features_repository.cache}") + + # Ensure options passed correctly + assert client.options.cache is custom_cache + + assert client._features_repository.cache is custom_cache + + # Simulate loading features (mocking fetch would be better, but checking instance is good step 1) + # Let's try to set something manually in the cache and see if load_features finds it + key = "http://localhost::123" + features_data = {"features": {"foo": {"defaultValue": True}}} + await custom_cache.set(key, features_data, 60) + + # This should hit the cache and not fail due to network (since localhost might not be reachable) + # load_features_async is what we want to test + loaded = await client._features_repository.load_features_async("http://localhost", "123") + assert loaded == features_data + assert custom_cache.get_calls > 0 + + await client.close() diff --git a/tests/test_growthbook_client.py b/tests/test_growthbook_client.py index 4fe3816..1e88919 100644 --- a/tests/test_growthbook_client.py +++ b/tests/test_growthbook_client.py @@ -111,7 +111,7 @@ async def test_feature_repository_load(): "savedGroups": {} } - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.FeatureRepository._fetch_features_async', new_callable=AsyncMock, return_value=features_response) as mock_load: result = await repo.load_features_async(api_host="", client_key="") assert result == features_response @@ -286,7 +286,7 @@ async def test_http_refresh(): mock_load.side_effect = [feature_updates[0], feature_updates[1], *[feature_updates[1]] * 10] try: - with patch('growthbook.FeatureRepository.load_features_async', mock_load): + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', mock_load): # Start HTTP refresh with a short interval for testing refresh_task = asyncio.create_task(repo._start_http_refresh(interval=0.1)) @@ -316,7 +316,7 @@ async def test_callback(features): callback_called = True features_received = features - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=mock_features_response) as mock_load: client = GrowthBookClient(mock_options) @@ -349,7 +349,7 @@ async def mock_sse_handler(event_data): if event_data['type'] == 'features': await client._features_repository._handle_feature_update(event_data['data']) - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value={"features": {}, "savedGroups": {}}) as mock_load: # Create options with SSE strategy @@ -407,7 +407,7 @@ async def mock_load(*args, **kwargs): return {"features": {}, "savedGroups": {}} try: - with patch('growthbook.FeatureRepository.load_features_async', side_effect=mock_load): + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', side_effect=mock_load): refresh_task = asyncio.create_task(repo._start_http_refresh(interval=0.1)) try: await asyncio.wait_for(done.wait(), timeout=5.0) @@ -457,7 +457,7 @@ async def mock_load(*args, **kwargs): shared_response["features"]["test-feature"]["defaultValue"] += 1 return shared_response - with patch('growthbook.FeatureRepository.load_features_async', side_effect=mock_load): + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', side_effect=mock_load): client = GrowthBookClient(Options( api_host="https://test.growthbook.io", client_key="test_key" @@ -529,7 +529,7 @@ async def test_eval_feature(test_eval_feature_data, base_client_setup): try: # Set up mocks for both FeatureRepository and EnhancedFeatureRepository - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=features_data), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -564,7 +564,7 @@ async def test_experiment_run(test_experiment_run_data, base_client_setup): try: # Set up mocks for both FeatureRepository and EnhancedFeatureRepository - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=features_data), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -611,7 +611,7 @@ async def test_feature_methods(): try: # Set up mocks for both FeatureRepository and EnhancedFeatureRepository - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=features_data), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -704,7 +704,7 @@ async def test_sticky_bucket(test_sticky_bucket_data, base_client_setup): try: # Set up mocks - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=features_data), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -770,7 +770,7 @@ async def test_tracking(): try: # Set up mocks for feature repository - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value={"features": {}, "savedGroups": {}}), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -831,7 +831,7 @@ async def test_handles_tracking_errors(): try: # Set up mocks - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value={"features": {}, "savedGroups": {}}), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -884,7 +884,7 @@ def feature_usage_cb(key, result, user_context): "savedGroups": {} } - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=mock_features), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -957,7 +957,7 @@ def failing_callback(key, result, user_context): "savedGroups": {} } - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=mock_features), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ @@ -1019,7 +1019,7 @@ async def test_skip_all_experiments_flag(): "savedGroups": {} } - with patch('growthbook.FeatureRepository.load_features_async', + with patch('growthbook.growthbook_client.EnhancedFeatureRepository.load_features_async', new_callable=AsyncMock, return_value=mock_features), \ patch('growthbook.growthbook_client.EnhancedFeatureRepository.start_feature_refresh', new_callable=AsyncMock), \ From 4a878f5e8fda60406a1296edcdfc5e65c3d17751 Mon Sep 17 00:00:00 2001 From: Madhu Chavva Date: Mon, 23 Feb 2026 14:41:26 -0800 Subject: [PATCH 4/5] add tests for case-insensitive ops --- tests/test_case_insensitive_operators.py | 222 +++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tests/test_case_insensitive_operators.py diff --git a/tests/test_case_insensitive_operators.py b/tests/test_case_insensitive_operators.py new file mode 100644 index 0000000..1be8794 --- /dev/null +++ b/tests/test_case_insensitive_operators.py @@ -0,0 +1,222 @@ +"""Tests for case-insensitive membership operators: $ini, $nini, $alli""" +import pytest +from growthbook.core import evalCondition + + +class TestCaseInsensitiveOperators: + """Test case-insensitive membership operators""" + + def test_ini_pass_case_insensitive_match(self): + """$ini operator should match case-insensitively""" + condition = {"name": {"$ini": ["JOHN", "JANE"]}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition) is True + + def test_ini_pass_uppercase_value_lowercase_pattern(self): + """$ini operator should match with uppercase value and lowercase pattern""" + condition = {"name": {"$ini": ["john", "jane"]}} + attributes = {"name": "JOHN"} + assert evalCondition(attributes, condition) is True + + def test_ini_pass_mixed_case(self): + """$ini operator should match with mixed case""" + condition = {"name": {"$ini": ["JoHn", "jAnE"]}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition) is True + + def test_ini_fail(self): + """$ini operator should fail when value not in list""" + condition = {"name": {"$ini": ["JOHN", "JANE"]}} + attributes = {"name": "bob"} + assert evalCondition(attributes, condition) is False + + def test_ini_array_pass_1(self): + """$ini operator should work with array attributes - intersection""" + condition = {"tags": {"$ini": ["A", "B"]}} + attributes = {"tags": ["a", "c"]} + assert evalCondition(attributes, condition) is True + + def test_ini_array_pass_2(self): + """$ini operator should work with array attributes - multiple matches""" + condition = {"tags": {"$ini": ["A", "B"]}} + attributes = {"tags": ["a", "b"]} + assert evalCondition(attributes, condition) is True + + def test_ini_array_fail(self): + """$ini operator should fail when no intersection""" + condition = {"tags": {"$ini": ["A", "B"]}} + attributes = {"tags": ["c", "d"]} + assert evalCondition(attributes, condition) is False + + def test_ini_not_array(self): + """$ini operator should fail when condition value is not an array""" + condition = {"name": {"$ini": "JOHN"}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition) is False + + def test_nini_pass(self): + """$nini operator should pass when value not in list""" + condition = {"name": {"$nini": ["JOHN", "JANE"]}} + attributes = {"name": "bob"} + assert evalCondition(attributes, condition) is True + + def test_nini_fail_case_insensitive_match(self): + """$nini operator should fail on case-insensitive match""" + condition = {"name": {"$nini": ["JOHN", "JANE"]}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition) is False + + def test_nini_array_pass(self): + """$nini operator should pass with array when no intersection""" + condition = {"tags": {"$nini": ["A", "B"]}} + attributes = {"tags": ["c", "d"]} + assert evalCondition(attributes, condition) is True + + def test_nini_array_fail(self): + """$nini operator should fail with array when there's intersection""" + condition = {"tags": {"$nini": ["A", "B"]}} + attributes = {"tags": ["a", "c"]} + assert evalCondition(attributes, condition) is False + + def test_alli_pass(self): + """$alli operator should pass when all values match case-insensitively""" + condition = {"tags": {"$alli": ["A", "B"]}} + attributes = {"tags": ["a", "b", "c"]} + assert evalCondition(attributes, condition) is True + + def test_alli_pass_exact_match(self): + """$alli operator should pass with exact case-insensitive match""" + condition = {"tags": {"$alli": ["A", "B"]}} + attributes = {"tags": ["A", "B"]} + assert evalCondition(attributes, condition) is True + + def test_alli_fail_missing_value(self): + """$alli operator should fail when not all values present""" + condition = {"tags": {"$alli": ["A", "B", "C"]}} + attributes = {"tags": ["a", "b"]} + assert evalCondition(attributes, condition) is False + + def test_alli_fail_not_array(self): + """$alli operator should fail when attribute is not an array""" + condition = {"tags": {"$alli": ["A", "B"]}} + attributes = {"tags": "a"} + assert evalCondition(attributes, condition) is False + + def test_alli_pass_with_numbers(self): + """$alli operator should work with non-string values""" + condition = {"ids": {"$alli": [1, 2]}} + attributes = {"ids": [1, 2, 3]} + assert evalCondition(attributes, condition) is True + + def test_ini_pass_with_numbers(self): + """$ini operator should work with non-string values""" + condition = {"id": {"$ini": [1, 2, 3]}} + attributes = {"id": 2} + assert evalCondition(attributes, condition) is True + + def test_nini_pass_with_numbers(self): + """$nini operator should work with non-string values""" + condition = {"id": {"$nini": [1, 2, 3]}} + attributes = {"id": 4} + assert evalCondition(attributes, condition) is True + + def test_case_sensitive_in_vs_case_insensitive_ini(self): + """$in should be case-sensitive while $ini is case-insensitive""" + # $in should be case-sensitive (fail) + condition_in = {"name": {"$in": ["JOHN", "JANE"]}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition_in) is False + + # $ini should be case-insensitive (pass) + condition_ini = {"name": {"$ini": ["JOHN", "JANE"]}} + assert evalCondition(attributes, condition_ini) is True + + def test_case_sensitive_nin_vs_case_insensitive_nini(self): + """$nin should be case-sensitive while $nini is case-insensitive""" + # $nin should be case-sensitive (pass because "john" != "JOHN") + condition_nin = {"name": {"$nin": ["JOHN", "JANE"]}} + attributes = {"name": "john"} + assert evalCondition(attributes, condition_nin) is True + + # $nini should be case-insensitive (fail because "john" matches "JOHN") + condition_nini = {"name": {"$nini": ["JOHN", "JANE"]}} + assert evalCondition(attributes, condition_nini) is False + + def test_case_sensitive_all_vs_case_insensitive_alli(self): + """$all should be case-sensitive while $alli is case-insensitive""" + # $all should be case-sensitive (fail) + condition_all = {"tags": {"$all": ["A", "B"]}} + attributes = {"tags": ["a", "b", "c"]} + assert evalCondition(attributes, condition_all) is False + + # $alli should be case-insensitive (pass) + condition_alli = {"tags": {"$alli": ["A", "B"]}} + assert evalCondition(attributes, condition_alli) is True + + def test_complex_condition_with_ini(self): + """Complex condition with $ini and $or operators""" + condition = { + "$or": [ + {"country": {"$ini": ["USA", "CANADA"]}}, + {"language": {"$ini": ["EN", "FR"]}} + ] + } + + # Should match on country (case-insensitive) + attributes1 = {"country": "usa", "language": "de"} + assert evalCondition(attributes1, condition) is True + + # Should match on language (case-insensitive) + attributes2 = {"country": "germany", "language": "en"} + assert evalCondition(attributes2, condition) is True + + # Should not match + attributes3 = {"country": "germany", "language": "de"} + assert evalCondition(attributes3, condition) is False + + def test_complex_condition_with_alli(self): + """Complex condition with $alli and $and operators""" + condition = { + "$and": [ + {"tags": {"$alli": ["PREMIUM", "ACTIVE"]}}, + {"role": {"$ini": ["ADMIN", "MODERATOR"]}} + ] + } + + # Should match + attributes1 = {"tags": ["premium", "active", "verified"], "role": "admin"} + assert evalCondition(attributes1, condition) is True + + # Should not match (missing tag) + attributes2 = {"tags": ["premium"], "role": "admin"} + assert evalCondition(attributes2, condition) is False + + # Should not match (wrong role) + attributes3 = {"tags": ["premium", "active"], "role": "user"} + assert evalCondition(attributes3, condition) is False + + def test_empty_array_conditions(self): + """Test behavior with empty arrays""" + # Empty condition array should pass (vacuous truth) + condition_ini_empty = {"tags": {"$ini": []}} + attributes = {"tags": ["a", "b"]} + assert evalCondition(attributes, condition_ini_empty) is False + + # Empty attribute array should fail for $alli + condition_alli = {"tags": {"$alli": ["A", "B"]}} + attributes_empty = {"tags": []} + assert evalCondition(attributes_empty, condition_alli) is False + + def test_unicode_case_insensitive(self): + """Test case-insensitive matching with unicode characters""" + condition = {"name": {"$ini": ["CAFÉ", "RÉSUMÉ"]}} + attributes = {"name": "café"} + assert evalCondition(attributes, condition) is True + + condition2 = {"name": {"$ini": ["café", "résumé"]}} + attributes2 = {"name": "CAFÉ"} + assert evalCondition(attributes2, condition2) is True + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 47769ef749231e21ad28d4b762c657feda7c1035 Mon Sep 17 00:00:00 2001 From: Madhu Chavva Date: Tue, 10 Mar 2026 12:36:14 -0700 Subject: [PATCH 5/5] fix imports --- growthbook/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/growthbook/__init__.py b/growthbook/__init__.py index 8d32a16..31f215d 100644 --- a/growthbook/__init__.py +++ b/growthbook/__init__.py @@ -1,13 +1,13 @@ from .growthbook import * -from .common_types import AbstractAsyncFeatureCache - from .growthbook_client import ( GrowthBookClient, EnhancedFeatureRepository, FeatureCache, BackoffStrategy, - InMemoryAsyncFeatureCache + InMemoryAsyncFeatureCache, + RedisAsyncFeatureCache ) +from .common_types import AbstractAsyncFeatureCache # Plugin support from .plugins import (