growthbook 1.4.3__py2.py3-none-any.whl → 1.4.4__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- growthbook/__init__.py +1 -1
- growthbook/growthbook.py +74 -3
- {growthbook-1.4.3.dist-info → growthbook-1.4.4.dist-info}/METADATA +1 -1
- {growthbook-1.4.3.dist-info → growthbook-1.4.4.dist-info}/RECORD +7 -7
- {growthbook-1.4.3.dist-info → growthbook-1.4.4.dist-info}/WHEEL +0 -0
- {growthbook-1.4.3.dist-info → growthbook-1.4.4.dist-info}/licenses/LICENSE +0 -0
- {growthbook-1.4.3.dist-info → growthbook-1.4.4.dist-info}/top_level.txt +0 -0
growthbook/__init__.py
CHANGED
growthbook/growthbook.py
CHANGED
|
@@ -100,7 +100,8 @@ class InMemoryFeatureCache(AbstractFeatureCache):
|
|
|
100
100
|
def set(self, key: str, value: Dict, ttl: int) -> None:
|
|
101
101
|
if key in self.cache:
|
|
102
102
|
self.cache[key].update(value)
|
|
103
|
-
|
|
103
|
+
else:
|
|
104
|
+
self.cache[key] = CacheEntry(value, ttl)
|
|
104
105
|
|
|
105
106
|
def clear(self) -> None:
|
|
106
107
|
self.cache.clear()
|
|
@@ -327,6 +328,11 @@ class FeatureRepository(object):
|
|
|
327
328
|
self.http: Optional[PoolManager] = None
|
|
328
329
|
self.sse_client: Optional[SSEClient] = None
|
|
329
330
|
self._feature_update_callbacks: List[Callable[[Dict], None]] = []
|
|
331
|
+
|
|
332
|
+
# Background refresh support
|
|
333
|
+
self._refresh_thread: Optional[threading.Thread] = None
|
|
334
|
+
self._refresh_stop_event = threading.Event()
|
|
335
|
+
self._refresh_lock = threading.Lock()
|
|
330
336
|
|
|
331
337
|
def set_cache(self, cache: AbstractFeatureCache) -> None:
|
|
332
338
|
self.cache = cache
|
|
@@ -375,6 +381,7 @@ class FeatureRepository(object):
|
|
|
375
381
|
return res
|
|
376
382
|
return cached
|
|
377
383
|
|
|
384
|
+
|
|
378
385
|
async def load_features_async(
|
|
379
386
|
self, api_host: str, client_key: str, decryption_key: str = "", ttl: int = 600
|
|
380
387
|
) -> Optional[Dict]:
|
|
@@ -493,6 +500,52 @@ class FeatureRepository(object):
|
|
|
493
500
|
if self.sse_client:
|
|
494
501
|
self.sse_client.disconnect(timeout=timeout)
|
|
495
502
|
self.sse_client = None
|
|
503
|
+
|
|
504
|
+
def start_background_refresh(self, api_host: str, client_key: str, decryption_key: str, ttl: int = 600, refresh_interval: int = 300) -> None:
|
|
505
|
+
"""Start periodic background refresh task"""
|
|
506
|
+
with self._refresh_lock:
|
|
507
|
+
if self._refresh_thread is not None:
|
|
508
|
+
return # Already running
|
|
509
|
+
|
|
510
|
+
self._refresh_stop_event.clear()
|
|
511
|
+
self._refresh_thread = threading.Thread(
|
|
512
|
+
target=self._background_refresh_worker,
|
|
513
|
+
args=(api_host, client_key, decryption_key, ttl, refresh_interval),
|
|
514
|
+
daemon=True
|
|
515
|
+
)
|
|
516
|
+
self._refresh_thread.start()
|
|
517
|
+
logger.debug("Started background refresh task")
|
|
518
|
+
|
|
519
|
+
def _background_refresh_worker(self, api_host: str, client_key: str, decryption_key: str, ttl: int, refresh_interval: int) -> None:
|
|
520
|
+
"""Worker method for periodic background refresh"""
|
|
521
|
+
while not self._refresh_stop_event.is_set():
|
|
522
|
+
try:
|
|
523
|
+
# Wait for the refresh interval or stop event
|
|
524
|
+
if self._refresh_stop_event.wait(refresh_interval):
|
|
525
|
+
break # Stop event was set
|
|
526
|
+
|
|
527
|
+
logger.debug("Background refresh for Features - started")
|
|
528
|
+
res = self._fetch_features(api_host, client_key, decryption_key)
|
|
529
|
+
if res is not None:
|
|
530
|
+
cache_key = api_host + "::" + client_key
|
|
531
|
+
self.cache.set(cache_key, res, ttl)
|
|
532
|
+
logger.debug("Background refresh completed")
|
|
533
|
+
# Notify callbacks about fresh features
|
|
534
|
+
self._notify_feature_update_callbacks(res)
|
|
535
|
+
else:
|
|
536
|
+
logger.warning("Background refresh failed")
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.warning(f"Background refresh error: {e}")
|
|
539
|
+
|
|
540
|
+
def stop_background_refresh(self) -> None:
|
|
541
|
+
"""Stop background refresh task"""
|
|
542
|
+
self._refresh_stop_event.set()
|
|
543
|
+
|
|
544
|
+
with self._refresh_lock:
|
|
545
|
+
if self._refresh_thread is not None:
|
|
546
|
+
self._refresh_thread.join(timeout=1.0) # Wait up to 1 second
|
|
547
|
+
self._refresh_thread = None
|
|
548
|
+
logger.debug("Stopped background refresh task")
|
|
496
549
|
|
|
497
550
|
@staticmethod
|
|
498
551
|
def _get_features_url(api_host: str, client_key: str) -> str:
|
|
@@ -522,6 +575,8 @@ class GrowthBook(object):
|
|
|
522
575
|
savedGroups: dict = {},
|
|
523
576
|
streaming: bool = False,
|
|
524
577
|
streaming_connection_timeout: int = 30,
|
|
578
|
+
stale_while_revalidate: bool = False,
|
|
579
|
+
stale_ttl: int = 300, # 5 minutes default
|
|
525
580
|
plugins: List = None,
|
|
526
581
|
# Deprecated args
|
|
527
582
|
trackingCallback=None,
|
|
@@ -551,6 +606,8 @@ class GrowthBook(object):
|
|
|
551
606
|
|
|
552
607
|
self._streaming = streaming
|
|
553
608
|
self._streaming_timeout = streaming_connection_timeout
|
|
609
|
+
self._stale_while_revalidate = stale_while_revalidate
|
|
610
|
+
self._stale_ttl = stale_ttl
|
|
554
611
|
|
|
555
612
|
# Deprecated args
|
|
556
613
|
self._user = user
|
|
@@ -603,6 +660,13 @@ class GrowthBook(object):
|
|
|
603
660
|
if self._streaming:
|
|
604
661
|
self.load_features()
|
|
605
662
|
self.startAutoRefresh()
|
|
663
|
+
elif self._stale_while_revalidate and self._client_key:
|
|
664
|
+
# Start background refresh task for stale-while-revalidate
|
|
665
|
+
self.load_features() # Initial load
|
|
666
|
+
feature_repo.start_background_refresh(
|
|
667
|
+
self._api_host, self._client_key, self._decryption_key,
|
|
668
|
+
self._cache_ttl, self._stale_ttl
|
|
669
|
+
)
|
|
606
670
|
|
|
607
671
|
def _on_feature_update(self, features_data: Dict) -> None:
|
|
608
672
|
"""Callback to handle automatic feature updates from FeatureRepository"""
|
|
@@ -739,6 +803,13 @@ class GrowthBook(object):
|
|
|
739
803
|
except Exception as e:
|
|
740
804
|
logger.warning(f"Error stopping auto refresh during destroy: {e}")
|
|
741
805
|
|
|
806
|
+
try:
|
|
807
|
+
# Stop background refresh operations
|
|
808
|
+
if self._stale_while_revalidate and self._client_key:
|
|
809
|
+
feature_repo.stop_background_refresh()
|
|
810
|
+
except Exception as e:
|
|
811
|
+
logger.warning(f"Error stopping background refresh during destroy: {e}")
|
|
812
|
+
|
|
742
813
|
try:
|
|
743
814
|
# Clean up feature update callback
|
|
744
815
|
if self._client_key:
|
|
@@ -790,8 +861,8 @@ class GrowthBook(object):
|
|
|
790
861
|
def _ensure_fresh_features(self) -> None:
|
|
791
862
|
"""Lazy refresh: Check cache expiry and refresh if needed, but only if client_key is provided"""
|
|
792
863
|
|
|
793
|
-
if self._streaming or not self._client_key:
|
|
794
|
-
return # Skip cache checks - SSE
|
|
864
|
+
if self._streaming or self._stale_while_revalidate or not self._client_key:
|
|
865
|
+
return # Skip cache checks - SSE or background refresh handles freshness
|
|
795
866
|
|
|
796
867
|
try:
|
|
797
868
|
self.load_features()
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
growthbook/__init__.py,sha256=
|
|
1
|
+
growthbook/__init__.py,sha256=sEc27BZrhkkInXWijTKKmrZ4kfeyR3z8V7SFG7ykvM0,444
|
|
2
2
|
growthbook/common_types.py,sha256=OUGkqoUuYetWz1cyA1eWz5DM3awYw_ExcNAjFqJuGAc,14881
|
|
3
3
|
growthbook/core.py,sha256=n9nwna26iZTY48LIvQqu5N_RrE35X0wlRBhq0-Qdb-s,35241
|
|
4
|
-
growthbook/growthbook.py,sha256=
|
|
4
|
+
growthbook/growthbook.py,sha256=E4CrSOtrbnYKfuC-_7NAsZwqtRnRy7h8j9MsRestXMI,39417
|
|
5
5
|
growthbook/growthbook_client.py,sha256=1bDIuJoxlKUR_bKe_gD6V7JlUPt53uGgix9DhgSkPPc,23360
|
|
6
6
|
growthbook/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
growthbook/plugins/__init__.py,sha256=y2eAV1sA041XWcftBVTDH0t-ggy9r2C5oKRYRF6XR6s,602
|
|
8
8
|
growthbook/plugins/base.py,sha256=PWBXUBj62hi25Y5Eif9WmEWagWdkwGXHi2dMtn44bo8,3637
|
|
9
9
|
growthbook/plugins/growthbook_tracking.py,sha256=FvPFOuKF_xKjmTX8x_hzMlHrrL-68Y2ZPw1Hfl2_ilQ,11333
|
|
10
10
|
growthbook/plugins/request_context.py,sha256=O5FJDrjJR5u0rx3ENGO9cOsKMHd9e0l0Nvdb1PHfmm8,12951
|
|
11
|
-
growthbook-1.4.
|
|
12
|
-
growthbook-1.4.
|
|
13
|
-
growthbook-1.4.
|
|
14
|
-
growthbook-1.4.
|
|
15
|
-
growthbook-1.4.
|
|
11
|
+
growthbook-1.4.4.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
|
|
12
|
+
growthbook-1.4.4.dist-info/METADATA,sha256=961Hf5woy1DJDd1hKGl7maQLAGUv_A2pvEM32l1GKZs,22074
|
|
13
|
+
growthbook-1.4.4.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
14
|
+
growthbook-1.4.4.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
|
|
15
|
+
growthbook-1.4.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|