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 CHANGED
@@ -18,5 +18,5 @@ from .plugins import (
18
18
  )
19
19
 
20
20
  # x-release-please-start-version
21
- __version__ = "1.4.4"
21
+ __version__ = "1.4.6"
22
22
  # x-release-please-end
@@ -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 and self._client_key:
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
- self._features = {}
754
- for key, feature in features.items():
755
- if isinstance(feature, Feature):
756
- self._features[key] = feature
757
- else:
758
- self._features[key] = Feature(
759
- rules=feature.get("rules", []),
760
- defaultValue=feature.get("defaultValue", None),
761
- )
762
- # Update the global context with the new features and saved groups
763
- self._global_ctx.features = self._features
764
- self._global_ctx.saved_groups = self._saved_groups
765
- self.refresh_sticky_buckets()
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
- return core_eval_feature(key=key,
890
- evalContext=self._get_eval_context(),
891
- callback_subscription=self._fireSubscriptions,
892
- tracking_cb=self._track
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):
@@ -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
- return core_eval_feature(key=key, evalContext=context, tracking_cb=self._track).on
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
- return core_eval_feature(key=key, evalContext=context, tracking_cb=self._track).off
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.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,,