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.
- {growthbook-1.4.5/growthbook.egg-info → growthbook-1.4.6}/PKG-INFO +1 -2
- {growthbook-1.4.5 → growthbook-1.4.6}/README.md +0 -1
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/__init__.py +1 -1
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/common_types.py +1 -1
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/growthbook.py +29 -15
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/growthbook_client.py +4 -4
- {growthbook-1.4.5 → growthbook-1.4.6/growthbook.egg-info}/PKG-INFO +1 -2
- {growthbook-1.4.5 → growthbook-1.4.6}/setup.cfg +1 -1
- {growthbook-1.4.5 → growthbook-1.4.6}/tests/test_growthbook.py +7 -3
- {growthbook-1.4.5 → growthbook-1.4.6}/tests/test_growthbook_client.py +7 -3
- {growthbook-1.4.5 → growthbook-1.4.6}/LICENSE +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/MANIFEST.in +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/core.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/__init__.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/base.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/growthbook_tracking.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/plugins/request_context.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook/py.typed +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/SOURCES.txt +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/dependency_links.txt +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/requires.txt +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/growthbook.egg-info/top_level.txt +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/pyproject.toml +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/setup.py +0 -0
- {growthbook-1.4.5 → growthbook-1.4.6}/tests/conftest.py +0 -0
- {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.
|
|
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
|
)
|
|
@@ -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
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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.
|
|
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
|
)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|