growthbook 1.4.5__tar.gz → 1.4.6__tar.gz

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.
Files changed (26) hide show
  1. {growthbook-1.4.5/growthbook.egg-info → growthbook-1.4.6}/PKG-INFO +1 -2
  2. {growthbook-1.4.5 → growthbook-1.4.6}/README.md +0 -1
  3. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/__init__.py +1 -1
  4. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/common_types.py +1 -1
  5. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/growthbook.py +29 -15
  6. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/growthbook_client.py +4 -4
  7. {growthbook-1.4.5 → growthbook-1.4.6/growthbook.egg-info}/PKG-INFO +1 -2
  8. {growthbook-1.4.5 → growthbook-1.4.6}/setup.cfg +1 -1
  9. {growthbook-1.4.5 → growthbook-1.4.6}/tests/test_growthbook.py +7 -3
  10. {growthbook-1.4.5 → growthbook-1.4.6}/tests/test_growthbook_client.py +7 -3
  11. {growthbook-1.4.5 → growthbook-1.4.6}/LICENSE +0 -0
  12. {growthbook-1.4.5 → growthbook-1.4.6}/MANIFEST.in +0 -0
  13. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/core.py +0 -0
  14. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/__init__.py +0 -0
  15. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/base.py +0 -0
  16. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/growthbook_tracking.py +0 -0
  17. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/request_context.py +0 -0
  18. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/py.typed +0 -0
  19. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/SOURCES.txt +0 -0
  20. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/dependency_links.txt +0 -0
  21. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/requires.txt +0 -0
  22. {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/top_level.txt +0 -0
  23. {growthbook-1.4.5 → growthbook-1.4.6}/pyproject.toml +0 -0
  24. {growthbook-1.4.5 → growthbook-1.4.6}/setup.py +0 -0
  25. {growthbook-1.4.5 → growthbook-1.4.6}/tests/conftest.py +0 -0
  26. {growthbook-1.4.5 → growthbook-1.4.6}/tests/test_plugins.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 1.4.5
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
  )
@@ -464,7 +464,6 @@ class MyStickyBucketService(AbstractStickyBucketService):
464
464
  })
465
465
 
466
466
  # Pass in an instance of this service to your GrowthBook constructor
467
-
468
467
  gb = GrowthBook(
469
468
  sticky_bucket_service = MyStickyBucketService()
470
469
  )
@@ -18,5 +18,5 @@ from .plugins import (
18
18
  )
19
19
 
20
20
  # x-release-please-start-version
21
- __version__ = "1.4.5"
21
+ __version__ = "1.4.6"
22
22
  # x-release-please-end
@@ -426,7 +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'], None]] = None
429
+ on_feature_usage: Optional[Callable[[str, 'FeatureResult', UserContext], None]] = None
430
430
  tracking_plugins: Optional[List[Any]] = None
431
431
 
432
432
 
@@ -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
@@ -620,6 +624,7 @@ class GrowthBook(object):
620
624
  self._tracked: Dict[str, Any] = {}
621
625
  self._assigned: Dict[str, Any] = {}
622
626
  self._subscriptions: Set[Any] = set()
627
+ self._is_updating_features = False
623
628
 
624
629
  # support plugins
625
630
  self._plugins: List = plugins or []
@@ -662,7 +667,7 @@ class GrowthBook(object):
662
667
  if self._streaming:
663
668
  self.load_features()
664
669
  self.startAutoRefresh()
665
- elif self._stale_while_revalidate and self._client_key:
670
+ elif self._stale_while_revalidate:
666
671
  # Start background refresh task for stale-while-revalidate
667
672
  self.load_features() # Initial load
668
673
  feature_repo.start_background_refresh(
@@ -752,19 +757,24 @@ class GrowthBook(object):
752
757
  return self.set_features(features)
753
758
 
754
759
  def set_features(self, features: dict) -> None:
755
- self._features = {}
756
- for key, feature in features.items():
757
- if isinstance(feature, Feature):
758
- self._features[key] = feature
759
- else:
760
- self._features[key] = Feature(
761
- rules=feature.get("rules", []),
762
- defaultValue=feature.get("defaultValue", None),
763
- )
764
- # Update the global context with the new features and saved groups
765
- self._global_ctx.features = self._features
766
- self._global_ctx.saved_groups = self._saved_groups
767
- 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
768
778
 
769
779
  # @deprecated, use get_features
770
780
  def getFeatures(self) -> Dict[str, Feature]:
@@ -864,6 +874,10 @@ class GrowthBook(object):
864
874
  def _ensure_fresh_features(self) -> None:
865
875
  """Lazy refresh: Check cache expiry and refresh if needed, but only if client_key is provided"""
866
876
 
877
+ # Prevent infinite recursion when updating features (e.g., during sticky bucket refresh)
878
+ if self._is_updating_features:
879
+ return
880
+
867
881
  if self._streaming or self._stale_while_revalidate or not self._client_key:
868
882
  return # Skip cache checks - SSE or background refresh handles freshness
869
883
 
@@ -897,7 +911,7 @@ class GrowthBook(object):
897
911
  # Call feature usage callback if provided
898
912
  if self._featureUsageCallback:
899
913
  try:
900
- self._featureUsageCallback(key, result)
914
+ self._featureUsageCallback(key, result, self._user_ctx)
901
915
  except Exception:
902
916
  pass
903
917
  return result
@@ -503,7 +503,7 @@ class GrowthBookClient:
503
503
  # Call feature usage callback if provided
504
504
  if self.options.on_feature_usage:
505
505
  try:
506
- self.options.on_feature_usage(key, result)
506
+ self.options.on_feature_usage(key, result, user_context)
507
507
  except Exception:
508
508
  logger.exception("Error in feature usage callback")
509
509
  return result
@@ -516,7 +516,7 @@ class GrowthBookClient:
516
516
  # Call feature usage callback if provided
517
517
  if self.options.on_feature_usage:
518
518
  try:
519
- self.options.on_feature_usage(key, result)
519
+ self.options.on_feature_usage(key, result, user_context)
520
520
  except Exception:
521
521
  logger.exception("Error in feature usage callback")
522
522
  return result.on
@@ -529,7 +529,7 @@ class GrowthBookClient:
529
529
  # Call feature usage callback if provided
530
530
  if self.options.on_feature_usage:
531
531
  try:
532
- self.options.on_feature_usage(key, result)
532
+ self.options.on_feature_usage(key, result, user_context)
533
533
  except Exception:
534
534
  logger.exception("Error in feature usage callback")
535
535
  return result.off
@@ -541,7 +541,7 @@ class GrowthBookClient:
541
541
  # Call feature usage callback if provided
542
542
  if self.options.on_feature_usage:
543
543
  try:
544
- self.options.on_feature_usage(key, result)
544
+ self.options.on_feature_usage(key, result, user_context)
545
545
  except Exception:
546
546
  logger.exception("Error in feature usage callback")
547
547
  return result.value if result.value is not None else fallback
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 1.4.5
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
  )
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.4.5
2
+ current_version = 1.4.6
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -219,8 +219,8 @@ def test_feature_usage_callback():
219
219
  """Test that feature usage callback is called correctly"""
220
220
  calls = []
221
221
 
222
- def feature_usage_cb(key, result):
223
- calls.append([key, result])
222
+ def feature_usage_cb(key, result, user_context):
223
+ calls.append([key, result, user_context])
224
224
 
225
225
  gb = GrowthBook(
226
226
  attributes={"id": "1"},
@@ -243,12 +243,14 @@ def test_feature_usage_callback():
243
243
  assert calls[0][0] == "feature-1"
244
244
  assert calls[0][1].value is True
245
245
  assert calls[0][1].source == "defaultValue"
246
+ assert calls[0][2].attributes == {"id": "1"}
246
247
 
247
248
  # Test is_on
248
249
  gb.is_on("feature-2")
249
250
  assert len(calls) == 2
250
251
  assert calls[1][0] == "feature-2"
251
252
  assert calls[1][1].value is False
253
+ assert calls[1][2].attributes == {"id": "1"}
252
254
 
253
255
  # Test get_feature_value
254
256
  value = gb.get_feature_value("feature-3", "blue")
@@ -256,11 +258,13 @@ def test_feature_usage_callback():
256
258
  assert calls[2][0] == "feature-3"
257
259
  assert calls[2][1].value == "red"
258
260
  assert value == "red"
261
+ assert calls[2][2].attributes == {"id": "1"}
259
262
 
260
263
  # Test is_off
261
264
  gb.is_off("feature-1")
262
265
  assert len(calls) == 4
263
266
  assert calls[3][0] == "feature-1"
267
+ assert calls[3][2].attributes == {"id": "1"}
264
268
 
265
269
  # Calling same feature multiple times should trigger callback each time
266
270
  gb.eval_feature("feature-1")
@@ -273,7 +277,7 @@ def test_feature_usage_callback():
273
277
  def test_feature_usage_callback_error_handling():
274
278
  """Test that feature usage callback errors are handled gracefully"""
275
279
 
276
- def failing_callback(key, result):
280
+ def failing_callback(key, result, user_context):
277
281
  raise Exception("Callback error")
278
282
 
279
283
  gb = GrowthBook(
@@ -776,8 +776,8 @@ async def test_feature_usage_callback():
776
776
  """Test that feature usage callback is called correctly"""
777
777
  calls = []
778
778
 
779
- def feature_usage_cb(key, result):
780
- calls.append([key, result])
779
+ def feature_usage_cb(key, result, user_context):
780
+ calls.append([key, result, user_context])
781
781
 
782
782
  client = GrowthBookClient(Options(
783
783
  api_host="https://localhost.growthbook.io",
@@ -820,12 +820,14 @@ async def test_feature_usage_callback():
820
820
  assert calls[0][0] == "feature-1"
821
821
  assert calls[0][1].value is True
822
822
  assert calls[0][1].source == "defaultValue"
823
+ assert calls[0][2].attributes == {"id": "1"}
823
824
 
824
825
  # Test is_on
825
826
  await client.is_on("feature-2", user_context)
826
827
  assert len(calls) == 2
827
828
  assert calls[1][0] == "feature-2"
828
829
  assert calls[1][1].value is False
830
+ assert calls[1][2].attributes == {"id": "1"}
829
831
 
830
832
  # Test get_feature_value
831
833
  value = await client.get_feature_value("feature-3", "blue", user_context)
@@ -833,11 +835,13 @@ async def test_feature_usage_callback():
833
835
  assert calls[2][0] == "feature-3"
834
836
  assert calls[2][1].value == "red"
835
837
  assert value == "red"
838
+ assert calls[2][2].attributes == {"id": "1"}
836
839
 
837
840
  # Test is_off
838
841
  await client.is_off("feature-1", user_context)
839
842
  assert len(calls) == 4
840
843
  assert calls[3][0] == "feature-1"
844
+ assert calls[3][2].attributes == {"id": "1"}
841
845
 
842
846
  # Calling same feature multiple times should trigger callback each time
843
847
  await client.eval_feature("feature-1", user_context)
@@ -852,7 +856,7 @@ async def test_feature_usage_callback():
852
856
  async def test_feature_usage_callback_error_handling():
853
857
  """Test that feature usage callback errors are handled gracefully"""
854
858
 
855
- def failing_callback(key, result):
859
+ def failing_callback(key, result, user_context):
856
860
  raise Exception("Callback error")
857
861
 
858
862
  client = GrowthBookClient(Options(
File without changes
File without changes
File without changes
File without changes
File without changes