growthbook 1.4.10__py2.py3-none-any.whl → 2.0.0__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.10"
21
+ __version__ = "2.0.0"
22
22
  # x-release-please-end
growthbook/growthbook.py CHANGED
@@ -782,7 +782,7 @@ class GrowthBook(object):
782
782
  )
783
783
 
784
784
  if features:
785
- self.setFeatures(features)
785
+ self.set_features(features)
786
786
 
787
787
  # Register for automatic feature updates when cache expires
788
788
  if self._client_key:
@@ -813,7 +813,7 @@ class GrowthBook(object):
813
813
  self._api_host, self._client_key, self._decryption_key, self._cache_ttl
814
814
  )
815
815
  if response is not None and "features" in response.keys():
816
- self.setFeatures(response["features"])
816
+ self.set_features(response["features"])
817
817
 
818
818
  if response is not None and "savedGroups" in response:
819
819
  self._saved_groups = response["savedGroups"]
@@ -828,7 +828,7 @@ class GrowthBook(object):
828
828
 
829
829
  if features is not None:
830
830
  if "features" in features:
831
- self.setFeatures(features["features"])
831
+ self.set_features(features["features"])
832
832
  if "savedGroups" in features:
833
833
  self._saved_groups = features["savedGroups"]
834
834
  feature_repo.save_in_cache(self._client_key, features, self._cache_ttl)
@@ -842,7 +842,7 @@ class GrowthBook(object):
842
842
 
843
843
  if data is not None:
844
844
  if "features" in data:
845
- self.setFeatures(data["features"])
845
+ self.set_features(data["features"])
846
846
  if "savedGroups" in data:
847
847
  self._saved_groups = data["savedGroups"]
848
848
  feature_repo.save_in_cache(self._client_key, features, self._cache_ttl)
@@ -169,40 +169,78 @@ class EnhancedFeatureRepository(FeatureRepository, metaclass=SingletonMeta):
169
169
  if callback in self._callbacks:
170
170
  self._callbacks.remove(callback)
171
171
 
172
+ """
173
+ _start_sse_refresh flow mimics a bridge pattern to connect a blocking, synchronous background thread
174
+ (the SSEClient) with your non-blocking, async main loop.
175
+
176
+ Bridge - _maintain_sse_connection - runs on the main async loop, calls `startAutoRefresh` (which in turn spawns a thread)
177
+ and waits indefinitely. (Awaiting a Future suspends the coroutine, costing zero CPU)
178
+
179
+ The SSEClient runs in a separate thread, makes a blocking HTTP request, and invokes `on_event` synchronously.
180
+
181
+ The Hand off - when the event arrives (we're still on the background thread), sse_handler uses `asyncio.run_coroutine_threadsafe`
182
+ to schedule the async processing `_handle_sse_event` onto the main event loop.
183
+ """
172
184
  async def _start_sse_refresh(self) -> None:
173
185
  """Start SSE-based feature refresh"""
174
186
  with self._refresh_lock:
175
187
  if self._refresh_task is not None: # Already running
176
188
  return
177
189
 
178
- async def sse_handler(event_data: Dict[str, Any]) -> None:
190
+ # SSEClient invokes `on_event` synchronously from a background thread.
191
+ async def _handle_sse_event(event_data: Dict[str, Any]) -> None:
179
192
  try:
180
- if event_data['type'] == 'features-updated':
193
+ event_type = event_data.get("type")
194
+ if event_type == "features-updated":
181
195
  response = await self.load_features_async(
182
196
  self._api_host, self._client_key, self._decryption_key, self._cache_ttl
183
197
  )
184
198
  if response is not None:
185
199
  await self._handle_feature_update(response)
186
- elif event_data['type'] == 'features':
187
- await self._handle_feature_update(event_data['data'])
200
+ elif event_type == "features":
201
+ await self._handle_feature_update(event_data.get("data", {}))
188
202
  except Exception:
189
- traceback.print_exc()
203
+ logger.exception("Error handling SSE event")
190
204
 
191
- # Start the SSE connection task
192
- self._refresh_task = asyncio.create_task(
193
- self._maintain_sse_connection(sse_handler)
194
- )
205
+ main_loop = asyncio.get_running_loop()
195
206
 
196
- async def _maintain_sse_connection(self, handler: Callable) -> None:
197
- """Maintain SSE connection with automatic reconnection"""
198
- while not self._stop_event.is_set():
199
- try:
200
- await self.startAutoRefresh(self._api_host, self._client_key, handler)
201
- except Exception as e:
202
- if not self._stop_event.is_set():
203
- delay = self._backoff.next_delay()
204
- logger.error(f"SSE connection lost, reconnecting in {delay:.2f}s: {str(e)}")
205
- await asyncio.sleep(delay)
207
+ # We must not pass an `async def` callback here (it would never be awaited).
208
+ def sse_handler(event_data: Dict[str, Any]) -> None:
209
+ # Schedule async processing onto the main event loop.
210
+ try:
211
+ asyncio.run_coroutine_threadsafe(_handle_sse_event(event_data), main_loop)
212
+ except Exception:
213
+ logger.exception("Failed to schedule SSE event handler")
214
+
215
+ async def _maintain_sse_connection() -> None:
216
+ """
217
+ Start SSE streaming and keep the task alive until cancelled.
218
+ """
219
+ try:
220
+ # NOTE: `startAutoRefresh` is synchronous and starts a background thread.
221
+ self.startAutoRefresh(self._api_host, self._client_key, sse_handler)
222
+
223
+ # Wait indefinitely until the task is cancelled - basically saying "Keep this service 'active' until someone cancels me."
224
+ # reconnection logic is handled inside SSEClient's thread
225
+ await asyncio.Future()
226
+ except asyncio.CancelledError:
227
+ # Normal shutdown flow
228
+ raise
229
+ except Exception:
230
+ logger.exception("Unexpected error in SSE lifecycle task")
231
+ finally:
232
+ try:
233
+ # stopAutoRefresh blocks joining a thread, so it needs to be run in executor
234
+ # to avoid blocking the async event loop
235
+ await main_loop.run_in_executor(
236
+ None,
237
+ lambda: self.stopAutoRefresh(timeout=10)
238
+ )
239
+ except Exception:
240
+ logger.exception("Failed to stop SSE auto-refresh")
241
+
242
+ # Start a task that owns the SSE lifecycle and cleanup.
243
+ self._refresh_task = asyncio.create_task(_maintain_sse_connection())
206
244
 
207
245
  async def _start_http_refresh(self, interval: int = 60) -> None:
208
246
  """Enhanced HTTP polling with backoff"""
@@ -261,6 +299,12 @@ class EnhancedFeatureRepository(FeatureRepository, metaclass=SingletonMeta):
261
299
  async def stop_refresh(self) -> None:
262
300
  """Clean shutdown of refresh tasks"""
263
301
  self._stop_event.set()
302
+ # Ensure any SSE background thread is stopped as well.
303
+ try:
304
+ self.stopAutoRefresh(timeout=10)
305
+ except Exception:
306
+ # Best-effort cleanup; task cancellation below will proceed.
307
+ logger.exception("Error stopping SSE auto-refresh")
264
308
  if self._refresh_task:
265
309
  # Cancel the task
266
310
  self._refresh_task.cancel()
@@ -132,8 +132,8 @@ class GrowthBookTrackingPlugin(GrowthBookPlugin):
132
132
  """Setup feature evaluation tracking."""
133
133
  original_eval_feature = gb_instance.eval_feature
134
134
 
135
- def eval_feature_wrapper(key: str):
136
- result = original_eval_feature(key)
135
+ def eval_feature_wrapper(key: str, *args, **kwargs):
136
+ result = original_eval_feature(key, *args, **kwargs)
137
137
  self._track_feature_evaluated(key, result, gb_instance)
138
138
  return result
139
139
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 1.4.10
3
+ Version: 2.0.0
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
@@ -0,0 +1,15 @@
1
+ growthbook/__init__.py,sha256=tksQ8wdB4p_iIIPCa1e5NsUie_NM-b1173zp5wkCGLQ,444
2
+ growthbook/common_types.py,sha256=YKUmmYfzgrzLQ7kp2IPLc8QBA-B0QbnbF5viekNiTpw,15703
3
+ growthbook/core.py,sha256=C1Nes_AiEuu6ypPghKuIeM2F22XUsLK1KrLW0xDwLYU,35963
4
+ growthbook/growthbook.py,sha256=Ee6-jWPtvlgRvxRbsX-ZhrMV-8_F2T78Q7P30DVMsQM,47833
5
+ growthbook/growthbook_client.py,sha256=YnbKGbO2taWZXtlutuTf_ZSkL_WmhYLhr39fB6BFIcw,27578
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=yN2xOHtRNsJuxkm16wY0YBQFxjEXDKnKcup7C9bQwe4,11351
10
+ growthbook/plugins/request_context.py,sha256=WzoGxalxPfrsN3RzfkvVYaUGat1A3N4AErnaS9IZ48Y,13005
11
+ growthbook-2.0.0.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
12
+ growthbook-2.0.0.dist-info/METADATA,sha256=yCwwaIc8N5XnwmAMNVcTtqN0ZPaFsbCtcgmgG-dD3wg,22726
13
+ growthbook-2.0.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
14
+ growthbook-2.0.0.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
15
+ growthbook-2.0.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- growthbook/__init__.py,sha256=UisD9HLBgfbN9HN7bKSmt9-KIulTfmU1XojdFC-lOZw,445
2
- growthbook/common_types.py,sha256=YKUmmYfzgrzLQ7kp2IPLc8QBA-B0QbnbF5viekNiTpw,15703
3
- growthbook/core.py,sha256=C1Nes_AiEuu6ypPghKuIeM2F22XUsLK1KrLW0xDwLYU,35963
4
- growthbook/growthbook.py,sha256=xHrc_wgrfl9oTbAxIDSpFet7Wb8diW4Fetv02Cpc4aA,47829
5
- growthbook/growthbook_client.py,sha256=dN7BSWJ2RDNIWLnHh8tYKedOc6FzdecvkG88YzHESto,25082
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=lWO9ErUSrnqhcpWLp03XIrh45-BdBssdmLDVvaGvulY,11317
10
- growthbook/plugins/request_context.py,sha256=WzoGxalxPfrsN3RzfkvVYaUGat1A3N4AErnaS9IZ48Y,13005
11
- growthbook-1.4.10.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
12
- growthbook-1.4.10.dist-info/METADATA,sha256=TzLKYNF9RfuJDh-iOFKOF-e95s9U9l3N5cSo4Otmghg,22727
13
- growthbook-1.4.10.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
14
- growthbook-1.4.10.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
15
- growthbook-1.4.10.dist-info/RECORD,,