growthbook 2.1.1__tar.gz → 2.1.2__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 (27) hide show
  1. {growthbook-2.1.1/growthbook.egg-info → growthbook-2.1.2}/PKG-INFO +1 -1
  2. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/__init__.py +1 -1
  3. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/growthbook_client.py +2 -2
  4. {growthbook-2.1.1 → growthbook-2.1.2/growthbook.egg-info}/PKG-INFO +1 -1
  5. {growthbook-2.1.1 → growthbook-2.1.2}/setup.cfg +1 -1
  6. {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_growthbook_client.py +79 -5
  7. {growthbook-2.1.1 → growthbook-2.1.2}/LICENSE +0 -0
  8. {growthbook-2.1.1 → growthbook-2.1.2}/MANIFEST.in +0 -0
  9. {growthbook-2.1.1 → growthbook-2.1.2}/README.md +0 -0
  10. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/common_types.py +0 -0
  11. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/core.py +0 -0
  12. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/growthbook.py +0 -0
  13. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/__init__.py +0 -0
  14. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/base.py +0 -0
  15. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/growthbook_tracking.py +0 -0
  16. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/request_context.py +0 -0
  17. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/py.typed +0 -0
  18. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/SOURCES.txt +0 -0
  19. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/dependency_links.txt +0 -0
  20. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/requires.txt +0 -0
  21. {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/top_level.txt +0 -0
  22. {growthbook-2.1.1 → growthbook-2.1.2}/pyproject.toml +0 -0
  23. {growthbook-2.1.1 → growthbook-2.1.2}/setup.py +0 -0
  24. {growthbook-2.1.1 → growthbook-2.1.2}/tests/conftest.py +0 -0
  25. {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_etag.py +0 -0
  26. {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_growthbook.py +0 -0
  27. {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_plugins.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 2.1.1
3
+ Version: 2.1.2
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
@@ -18,5 +18,5 @@ from .plugins import (
18
18
  )
19
19
 
20
20
  # x-release-please-start-version
21
- __version__ = "2.1.1"
21
+ __version__ = "2.1.2"
22
22
  # x-release-please-end
@@ -90,8 +90,8 @@ class FeatureCache:
90
90
  def update(self, features: Dict[str, Any], saved_groups: Dict[str, Any]) -> None:
91
91
  """Simple thread-safe update of cache with new API data"""
92
92
  with self._lock:
93
- self._cache['features'].update(features)
94
- self._cache['savedGroups'].update(saved_groups)
93
+ self._cache['features'] = dict(features)
94
+ self._cache['savedGroups'] = dict(saved_groups)
95
95
 
96
96
  def get_current_state(self) -> Dict[str, Any]:
97
97
  """Get current cache state"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 2.1.1
3
+ Version: 2.1.2
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
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.1.1
2
+ current_version = 2.1.2
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -158,19 +158,93 @@ async def test_concurrent_feature_updates():
158
158
  client_key="test_key"
159
159
  )
160
160
  features = {f"feature-{i}": {"defaultValue": i} for i in range(10)}
161
-
161
+
162
162
  async def update_features(feature_subset):
163
163
  await repo._handle_feature_update({"features": feature_subset, "savedGroups": {}})
164
-
164
+
165
165
  await asyncio.gather(*[
166
- update_features({k: features[k]})
166
+ update_features({k: features[k]})
167
167
  for k in features
168
168
  ])
169
-
169
+
170
170
  cache_state = repo._feature_cache.get_current_state()
171
171
  # Verify all features were properly stored
172
- assert cache_state["features"] == features
172
+ assert len(cache_state["features"]) == 1
173
173
  assert cache_state["savedGroups"] == {}
174
+ feature_key = list(cache_state["features"].keys())[0]
175
+ assert feature_key in features
176
+ assert cache_state["features"][feature_key] == features[feature_key]
177
+
178
+
179
+ @pytest.mark.asyncio
180
+ async def test_feature_cache_thread_safety(cache):
181
+ """Verify FeatureCache is thread-safe during concurrent updates"""
182
+ repo = EnhancedFeatureRepository(
183
+ api_host="https://test.growthbook.io",
184
+ client_key="test_key"
185
+ )
186
+
187
+ feature_sets = [
188
+ {f"set-{i}-feature-{j}": {"value": j} for j in range(3)}
189
+ for i in range(5)
190
+ ]
191
+
192
+ async def update_full_set(feature_set):
193
+ await repo._handle_feature_update({
194
+ "features": feature_set,
195
+ "savedGroups": {}
196
+ })
197
+
198
+ # Concurrent updates
199
+ await asyncio.gather(*[update_full_set(fs) for fs in feature_sets])
200
+
201
+ cache = repo._feature_cache.get_current_state()
202
+
203
+ # One complete set should be in cache (race condition winner)
204
+ assert len(cache["features"]) == 3
205
+ cache_keys = set(cache["features"].keys())
206
+ assert any(cache_keys == set(fs.keys()) for fs in feature_sets)
207
+
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_disabled_features_removed_from_cache(cache):
211
+ """
212
+ Regression test: disabled features must be removed from cache.
213
+
214
+ Previously, FeatureCache.update() used dict.update() which only
215
+ adds/modifies entries but never removes them. This caused disabled
216
+ features to persist in the cache indefinitely.
217
+ """
218
+ repo = EnhancedFeatureRepository(
219
+ api_host="https://test.growthbook.io",
220
+ client_key="test_key"
221
+ )
222
+
223
+ # Initial state: 2 features enabled
224
+ await repo._handle_feature_update({
225
+ "features": {
226
+ "feature-a": {"defaultValue": True},
227
+ "feature-b": {"defaultValue": False}
228
+ },
229
+ "savedGroups": {}
230
+ })
231
+
232
+ cache = repo._feature_cache.get_current_state()
233
+ assert "feature-a" in cache["features"]
234
+ assert "feature-b" in cache["features"]
235
+
236
+ # User disables feature-b in Growthbook UI
237
+ # API now returns only active features
238
+ await repo._handle_feature_update({
239
+ "features": {
240
+ "feature-a": {"defaultValue": True}
241
+ },
242
+ "savedGroups": {}
243
+ })
244
+
245
+ cache = repo._feature_cache.get_current_state()
246
+ assert "feature-a" in cache["features"]
247
+ assert "feature-b" not in cache["features"] # Must be removed!
174
248
 
175
249
  @pytest.mark.asyncio
176
250
  async def test_callback_thread_safety():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes