growthbook 1.4.4__py2.py3-none-any.whl → 1.4.6__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/common_types.py +1 -0
- growthbook/growthbook.py +43 -19
- growthbook/growthbook_client.py +28 -2
- {growthbook-1.4.4.dist-info → growthbook-1.4.6.dist-info}/METADATA +1 -2
- growthbook-1.4.6.dist-info/RECORD +15 -0
- growthbook-1.4.4.dist-info/RECORD +0 -15
- {growthbook-1.4.4.dist-info → growthbook-1.4.6.dist-info}/WHEEL +0 -0
- {growthbook-1.4.4.dist-info → growthbook-1.4.6.dist-info}/licenses/LICENSE +0 -0
- {growthbook-1.4.4.dist-info → growthbook-1.4.6.dist-info}/top_level.txt +0 -0
growthbook/__init__.py
CHANGED
growthbook/common_types.py
CHANGED
|
@@ -426,6 +426,7 @@ class Options:
|
|
|
426
426
|
sticky_bucket_service: Optional[AbstractStickyBucketService] = None
|
|
427
427
|
sticky_bucket_identifier_attributes: Optional[List[str]] = None
|
|
428
428
|
on_experiment_viewed: Optional[Callable[[Experiment, Result, Optional[UserContext]], None]] = None
|
|
429
|
+
on_feature_usage: Optional[Callable[[str, 'FeatureResult', UserContext], None]] = None
|
|
429
430
|
tracking_plugins: Optional[List[Any]] = None
|
|
430
431
|
|
|
431
432
|
|
growthbook/growthbook.py
CHANGED
|
@@ -503,6 +503,10 @@ class FeatureRepository(object):
|
|
|
503
503
|
|
|
504
504
|
def start_background_refresh(self, api_host: str, client_key: str, decryption_key: str, ttl: int = 600, refresh_interval: int = 300) -> None:
|
|
505
505
|
"""Start periodic background refresh task"""
|
|
506
|
+
|
|
507
|
+
if not client_key:
|
|
508
|
+
raise ValueError("Must specify `client_key` to refresh features")
|
|
509
|
+
|
|
506
510
|
with self._refresh_lock:
|
|
507
511
|
if self._refresh_thread is not None:
|
|
508
512
|
return # Already running
|
|
@@ -565,6 +569,7 @@ class GrowthBook(object):
|
|
|
565
569
|
features: dict = {},
|
|
566
570
|
qa_mode: bool = False,
|
|
567
571
|
on_experiment_viewed=None,
|
|
572
|
+
on_feature_usage=None,
|
|
568
573
|
api_host: str = "",
|
|
569
574
|
client_key: str = "",
|
|
570
575
|
decryption_key: str = "",
|
|
@@ -603,6 +608,7 @@ class GrowthBook(object):
|
|
|
603
608
|
|
|
604
609
|
self._qaMode = qa_mode or qaMode
|
|
605
610
|
self._trackingCallback = on_experiment_viewed or trackingCallback
|
|
611
|
+
self._featureUsageCallback = on_feature_usage
|
|
606
612
|
|
|
607
613
|
self._streaming = streaming
|
|
608
614
|
self._streaming_timeout = streaming_connection_timeout
|
|
@@ -618,6 +624,7 @@ class GrowthBook(object):
|
|
|
618
624
|
self._tracked: Dict[str, Any] = {}
|
|
619
625
|
self._assigned: Dict[str, Any] = {}
|
|
620
626
|
self._subscriptions: Set[Any] = set()
|
|
627
|
+
self._is_updating_features = False
|
|
621
628
|
|
|
622
629
|
# support plugins
|
|
623
630
|
self._plugins: List = plugins or []
|
|
@@ -660,7 +667,7 @@ class GrowthBook(object):
|
|
|
660
667
|
if self._streaming:
|
|
661
668
|
self.load_features()
|
|
662
669
|
self.startAutoRefresh()
|
|
663
|
-
elif self._stale_while_revalidate
|
|
670
|
+
elif self._stale_while_revalidate:
|
|
664
671
|
# Start background refresh task for stale-while-revalidate
|
|
665
672
|
self.load_features() # Initial load
|
|
666
673
|
feature_repo.start_background_refresh(
|
|
@@ -750,19 +757,24 @@ class GrowthBook(object):
|
|
|
750
757
|
return self.set_features(features)
|
|
751
758
|
|
|
752
759
|
def set_features(self, features: dict) -> None:
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
760
|
+
# Prevent infinite recursion during feature updates
|
|
761
|
+
self._is_updating_features = True
|
|
762
|
+
try:
|
|
763
|
+
self._features = {}
|
|
764
|
+
for key, feature in features.items():
|
|
765
|
+
if isinstance(feature, Feature):
|
|
766
|
+
self._features[key] = feature
|
|
767
|
+
else:
|
|
768
|
+
self._features[key] = Feature(
|
|
769
|
+
rules=feature.get("rules", []),
|
|
770
|
+
defaultValue=feature.get("defaultValue", None),
|
|
771
|
+
)
|
|
772
|
+
# Update the global context with the new features and saved groups
|
|
773
|
+
self._global_ctx.features = self._features
|
|
774
|
+
self._global_ctx.saved_groups = self._saved_groups
|
|
775
|
+
self.refresh_sticky_buckets()
|
|
776
|
+
finally:
|
|
777
|
+
self._is_updating_features = False
|
|
766
778
|
|
|
767
779
|
# @deprecated, use get_features
|
|
768
780
|
def getFeatures(self) -> Dict[str, Feature]:
|
|
@@ -823,6 +835,7 @@ class GrowthBook(object):
|
|
|
823
835
|
self._tracked.clear()
|
|
824
836
|
self._assigned.clear()
|
|
825
837
|
self._trackingCallback = None
|
|
838
|
+
self._featureUsageCallback = None
|
|
826
839
|
self._forcedVariations.clear()
|
|
827
840
|
self._overrides.clear()
|
|
828
841
|
self._groups.clear()
|
|
@@ -861,6 +874,10 @@ class GrowthBook(object):
|
|
|
861
874
|
def _ensure_fresh_features(self) -> None:
|
|
862
875
|
"""Lazy refresh: Check cache expiry and refresh if needed, but only if client_key is provided"""
|
|
863
876
|
|
|
877
|
+
# Prevent infinite recursion when updating features (e.g., during sticky bucket refresh)
|
|
878
|
+
if self._is_updating_features:
|
|
879
|
+
return
|
|
880
|
+
|
|
864
881
|
if self._streaming or self._stale_while_revalidate or not self._client_key:
|
|
865
882
|
return # Skip cache checks - SSE or background refresh handles freshness
|
|
866
883
|
|
|
@@ -886,11 +903,18 @@ class GrowthBook(object):
|
|
|
886
903
|
)
|
|
887
904
|
|
|
888
905
|
def eval_feature(self, key: str) -> FeatureResult:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
906
|
+
result = core_eval_feature(key=key,
|
|
907
|
+
evalContext=self._get_eval_context(),
|
|
908
|
+
callback_subscription=self._fireSubscriptions,
|
|
909
|
+
tracking_cb=self._track
|
|
910
|
+
)
|
|
911
|
+
# Call feature usage callback if provided
|
|
912
|
+
if self._featureUsageCallback:
|
|
913
|
+
try:
|
|
914
|
+
self._featureUsageCallback(key, result, self._user_ctx)
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
917
|
+
return result
|
|
894
918
|
|
|
895
919
|
# @deprecated, use get_all_results
|
|
896
920
|
def getAllResults(self):
|
growthbook/growthbook_client.py
CHANGED
|
@@ -500,24 +500,50 @@ class GrowthBookClient:
|
|
|
500
500
|
async with self._context_lock:
|
|
501
501
|
context = await self.create_evaluation_context(user_context)
|
|
502
502
|
result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
|
|
503
|
+
# Call feature usage callback if provided
|
|
504
|
+
if self.options.on_feature_usage:
|
|
505
|
+
try:
|
|
506
|
+
self.options.on_feature_usage(key, result, user_context)
|
|
507
|
+
except Exception:
|
|
508
|
+
logger.exception("Error in feature usage callback")
|
|
503
509
|
return result
|
|
504
510
|
|
|
505
511
|
async def is_on(self, key: str, user_context: UserContext) -> bool:
|
|
506
512
|
"""Check if a feature is enabled with proper async context management"""
|
|
507
513
|
async with self._context_lock:
|
|
508
514
|
context = await self.create_evaluation_context(user_context)
|
|
509
|
-
|
|
515
|
+
result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
|
|
516
|
+
# Call feature usage callback if provided
|
|
517
|
+
if self.options.on_feature_usage:
|
|
518
|
+
try:
|
|
519
|
+
self.options.on_feature_usage(key, result, user_context)
|
|
520
|
+
except Exception:
|
|
521
|
+
logger.exception("Error in feature usage callback")
|
|
522
|
+
return result.on
|
|
510
523
|
|
|
511
524
|
async def is_off(self, key: str, user_context: UserContext) -> bool:
|
|
512
525
|
"""Check if a feature is set to off with proper async context management"""
|
|
513
526
|
async with self._context_lock:
|
|
514
527
|
context = await self.create_evaluation_context(user_context)
|
|
515
|
-
|
|
528
|
+
result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
|
|
529
|
+
# Call feature usage callback if provided
|
|
530
|
+
if self.options.on_feature_usage:
|
|
531
|
+
try:
|
|
532
|
+
self.options.on_feature_usage(key, result, user_context)
|
|
533
|
+
except Exception:
|
|
534
|
+
logger.exception("Error in feature usage callback")
|
|
535
|
+
return result.off
|
|
516
536
|
|
|
517
537
|
async def get_feature_value(self, key: str, fallback: Any, user_context: UserContext) -> Any:
|
|
518
538
|
async with self._context_lock:
|
|
519
539
|
context = await self.create_evaluation_context(user_context)
|
|
520
540
|
result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
|
|
541
|
+
# Call feature usage callback if provided
|
|
542
|
+
if self.options.on_feature_usage:
|
|
543
|
+
try:
|
|
544
|
+
self.options.on_feature_usage(key, result, user_context)
|
|
545
|
+
except Exception:
|
|
546
|
+
logger.exception("Error in feature usage callback")
|
|
521
547
|
return result.value if result.value is not None else fallback
|
|
522
548
|
|
|
523
549
|
async def run(self, experiment: Experiment, user_context: UserContext) -> Result:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: growthbook
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.6
|
|
4
4
|
Summary: Powerful Feature flagging and A/B testing for Python apps
|
|
5
5
|
Home-page: https://github.com/growthbook/growthbook-python
|
|
6
6
|
Author: GrowthBook
|
|
@@ -499,7 +499,6 @@ class MyStickyBucketService(AbstractStickyBucketService):
|
|
|
499
499
|
})
|
|
500
500
|
|
|
501
501
|
# Pass in an instance of this service to your GrowthBook constructor
|
|
502
|
-
|
|
503
502
|
gb = GrowthBook(
|
|
504
503
|
sticky_bucket_service = MyStickyBucketService()
|
|
505
504
|
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
growthbook/__init__.py,sha256=1B2U7uOmsNvvEQ3DQ2hLuvALy6pqnJAduUUIXFbFbso,444
|
|
2
|
+
growthbook/common_types.py,sha256=OMfssahxLjvCGuGsWa75G6JMmu5xH1hVrK0pCL1ArMU,14972
|
|
3
|
+
growthbook/core.py,sha256=n9nwna26iZTY48LIvQqu5N_RrE35X0wlRBhq0-Qdb-s,35241
|
|
4
|
+
growthbook/growthbook.py,sha256=8U6UmxHAdLDCsUi3CBFTvK4nBchs9-ukXwd_bZAiXsY,40333
|
|
5
|
+
growthbook/growthbook_client.py,sha256=igD6T9MP9rfYzS1Dk0wBJ3fgfNzJxDQZ_bm5SS8XO7I,24632
|
|
6
|
+
growthbook/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
growthbook/plugins/__init__.py,sha256=y2eAV1sA041XWcftBVTDH0t-ggy9r2C5oKRYRF6XR6s,602
|
|
8
|
+
growthbook/plugins/base.py,sha256=PWBXUBj62hi25Y5Eif9WmEWagWdkwGXHi2dMtn44bo8,3637
|
|
9
|
+
growthbook/plugins/growthbook_tracking.py,sha256=FvPFOuKF_xKjmTX8x_hzMlHrrL-68Y2ZPw1Hfl2_ilQ,11333
|
|
10
|
+
growthbook/plugins/request_context.py,sha256=O5FJDrjJR5u0rx3ENGO9cOsKMHd9e0l0Nvdb1PHfmm8,12951
|
|
11
|
+
growthbook-1.4.6.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
|
|
12
|
+
growthbook-1.4.6.dist-info/METADATA,sha256=q04J95e-Kgf9yssVQCfG_JJZHGHWsTDndDwZm1c74eg,22073
|
|
13
|
+
growthbook-1.4.6.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
14
|
+
growthbook-1.4.6.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
|
|
15
|
+
growthbook-1.4.6.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
growthbook/__init__.py,sha256=sEc27BZrhkkInXWijTKKmrZ4kfeyR3z8V7SFG7ykvM0,444
|
|
2
|
-
growthbook/common_types.py,sha256=OUGkqoUuYetWz1cyA1eWz5DM3awYw_ExcNAjFqJuGAc,14881
|
|
3
|
-
growthbook/core.py,sha256=n9nwna26iZTY48LIvQqu5N_RrE35X0wlRBhq0-Qdb-s,35241
|
|
4
|
-
growthbook/growthbook.py,sha256=E4CrSOtrbnYKfuC-_7NAsZwqtRnRy7h8j9MsRestXMI,39417
|
|
5
|
-
growthbook/growthbook_client.py,sha256=1bDIuJoxlKUR_bKe_gD6V7JlUPt53uGgix9DhgSkPPc,23360
|
|
6
|
-
growthbook/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
growthbook/plugins/__init__.py,sha256=y2eAV1sA041XWcftBVTDH0t-ggy9r2C5oKRYRF6XR6s,602
|
|
8
|
-
growthbook/plugins/base.py,sha256=PWBXUBj62hi25Y5Eif9WmEWagWdkwGXHi2dMtn44bo8,3637
|
|
9
|
-
growthbook/plugins/growthbook_tracking.py,sha256=FvPFOuKF_xKjmTX8x_hzMlHrrL-68Y2ZPw1Hfl2_ilQ,11333
|
|
10
|
-
growthbook/plugins/request_context.py,sha256=O5FJDrjJR5u0rx3ENGO9cOsKMHd9e0l0Nvdb1PHfmm8,12951
|
|
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
|