growthbook 1.3.1__py2.py3-none-any.whl → 1.4.1__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.3.1"
21
+ __version__ = "1.4.1"
22
22
  # x-release-please-end
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
 
3
3
  import sys
4
+ from token import OP
4
5
  # Only require typing_extensions if using Python 3.7 or earlier
5
6
  if sys.version_info >= (3, 8):
6
7
  from typing import TypedDict
@@ -8,7 +9,7 @@ else:
8
9
  from typing_extensions import TypedDict
9
10
 
10
11
  from dataclasses import dataclass, field
11
- from typing import Any, Dict, List, Optional, Union, Set, Tuple
12
+ from typing import Any, Callable, Dict, List, Optional, Union, Set, Tuple
12
13
  from enum import Enum
13
14
  from abc import ABC, abstractmethod
14
15
 
@@ -400,6 +401,15 @@ class StackContext:
400
401
  class FeatureRefreshStrategy(Enum):
401
402
  STALE_WHILE_REVALIDATE = 'HTTP_REFRESH'
402
403
  SERVER_SENT_EVENTS = 'SSE'
404
+ @dataclass
405
+ class UserContext:
406
+ # user_id: Optional[str] = None
407
+ url: str = ""
408
+ attributes: Dict[str, Any] = field(default_factory=dict)
409
+ groups: Dict[str, str] = field(default_factory=dict)
410
+ forced_variations: Dict[str, Any] = field(default_factory=dict)
411
+ overrides: Dict[str, Any] = field(default_factory=dict)
412
+ sticky_bucket_assignment_docs: Dict[str, Any] = field(default_factory=dict)
403
413
 
404
414
  @dataclass
405
415
  class Options:
@@ -415,17 +425,9 @@ class Options:
415
425
  refresh_strategy: Optional[FeatureRefreshStrategy] = FeatureRefreshStrategy.STALE_WHILE_REVALIDATE
416
426
  sticky_bucket_service: Optional[AbstractStickyBucketService] = None
417
427
  sticky_bucket_identifier_attributes: Optional[List[str]] = None
418
- on_experiment_viewed=None
428
+ on_experiment_viewed: Optional[Callable[[Experiment, Result, Optional[UserContext]], None]] = None
429
+ tracking_plugins: Optional[List[Any]] = None
419
430
 
420
- @dataclass
421
- class UserContext:
422
- # user_id: Optional[str] = None
423
- url: str = ""
424
- attributes: Dict[str, Any] = field(default_factory=dict)
425
- groups: Dict[str, str] = field(default_factory=dict)
426
- forced_variations: Dict[str, Any] = field(default_factory=dict)
427
- overrides: Dict[str, Any] = field(default_factory=dict)
428
- sticky_bucket_assignment_docs: Dict[str, Any] = field(default_factory=dict)
429
431
 
430
432
  @dataclass
431
433
  class GlobalContext:
growthbook/core.py CHANGED
@@ -4,7 +4,7 @@ import json
4
4
 
5
5
  from urllib.parse import urlparse, parse_qs
6
6
  from typing import Callable, Optional, Any, Set, Tuple, List, Dict
7
- from .common_types import EvaluationContext, FeatureResult, Experiment, Filter, Result, VariationMeta
7
+ from .common_types import EvaluationContext, FeatureResult, Experiment, Filter, Result, UserContext, VariationMeta
8
8
 
9
9
 
10
10
  logger = logging.getLogger("growthbook.core")
@@ -414,7 +414,7 @@ def eval_feature(
414
414
  key: str,
415
415
  evalContext: EvaluationContext = None,
416
416
  callback_subscription: Callable[[Experiment, Result], None] = None,
417
- tracking_cb: Callable[[Experiment, Result], None] = None
417
+ tracking_cb: Callable[[Experiment, Result, UserContext], None] = None
418
418
  ) -> FeatureResult:
419
419
  """Core feature evaluation logic as a standalone function"""
420
420
 
@@ -634,7 +634,7 @@ def _get_sticky_bucket_variation(
634
634
  def run_experiment(experiment: Experiment,
635
635
  featureId: Optional[str] = None,
636
636
  evalContext: EvaluationContext = None,
637
- tracking_cb: Callable[[Experiment, Result], None] = None
637
+ tracking_cb: Callable[[Experiment, Result, UserContext], None] = None
638
638
  ) -> Result:
639
639
  if evalContext is None:
640
640
  raise ValueError("evalContext is required - run_experiment")
@@ -859,7 +859,7 @@ def run_experiment(experiment: Experiment,
859
859
 
860
860
  # 14. Fire the tracking callback if set
861
861
  if tracking_cb:
862
- tracking_cb(experiment, result)
862
+ tracking_cb(experiment, result, evalContext.user)
863
863
 
864
864
  # 15. Return the result
865
865
  logger.debug("Assigned variation %d in experiment %s", assigned, experiment.key)
growthbook/growthbook.py CHANGED
@@ -120,12 +120,13 @@ class InMemoryStickyBucketService(AbstractStickyBucketService):
120
120
 
121
121
 
122
122
  class SSEClient:
123
- def __init__(self, api_host, client_key, on_event, reconnect_delay=5, headers=None):
123
+ def __init__(self, api_host, client_key, on_event, reconnect_delay=5, headers=None, timeout = 30):
124
124
  self.api_host = api_host
125
125
  self.client_key = client_key
126
126
 
127
127
  self.on_event = on_event
128
128
  self.reconnect_delay = reconnect_delay
129
+ self.timeout = timeout
129
130
 
130
131
  self._sse_session = None
131
132
  self._sse_thread = None
@@ -173,7 +174,8 @@ class SSEClient:
173
174
 
174
175
  while self.is_running:
175
176
  try:
176
- async with aiohttp.ClientSession(headers=self.headers) as session:
177
+ async with aiohttp.ClientSession(headers=self.headers,
178
+ timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
177
179
  self._sse_session = session
178
180
 
179
181
  async with session.get(url) as response:
@@ -407,10 +409,10 @@ class FeatureRepository(object):
407
409
  return data
408
410
 
409
411
 
410
- def startAutoRefresh(self, api_host, client_key, cb):
412
+ def startAutoRefresh(self, api_host, client_key, cb, streaming_timeout=30):
411
413
  if not client_key:
412
414
  raise ValueError("Must specify `client_key` to start features streaming")
413
- self.sse_client = self.sse_client or SSEClient(api_host=api_host, client_key=client_key, on_event=cb)
415
+ self.sse_client = self.sse_client or SSEClient(api_host=api_host, client_key=client_key, on_event=cb, timeout=streaming_timeout)
414
416
  self.sse_client.connect()
415
417
 
416
418
  def stopAutoRefresh(self):
@@ -443,6 +445,7 @@ class GrowthBook(object):
443
445
  sticky_bucket_identifier_attributes: List[str] = None,
444
446
  savedGroups: dict = {},
445
447
  streaming: bool = False,
448
+ streaming_timeout: int = 30,
446
449
  plugins: List = None,
447
450
  # Deprecated args
448
451
  trackingCallback=None,
@@ -471,6 +474,7 @@ class GrowthBook(object):
471
474
  self._trackingCallback = on_experiment_viewed or trackingCallback
472
475
 
473
476
  self._streaming = streaming
477
+ self._streaming_timeout = streaming_timeout
474
478
 
475
479
  # Deprecated args
476
480
  self._user = user
@@ -587,7 +591,8 @@ class GrowthBook(object):
587
591
  feature_repo.startAutoRefresh(
588
592
  api_host=self._api_host,
589
593
  client_key=self._client_key,
590
- cb=self._dispatch_sse_event
594
+ cb=self._dispatch_sse_event,
595
+ streaming_timeout=self._streaming_timeout
591
596
  )
592
597
 
593
598
  def stopAutoRefresh(self):
@@ -671,7 +676,7 @@ class GrowthBook(object):
671
676
  return self.get_feature_value(key, fallback)
672
677
 
673
678
  def get_feature_value(self, key: str, fallback):
674
- res = self.evalFeature(key)
679
+ res = self.eval_feature(key)
675
680
  return res.value if res.value is not None else fallback
676
681
 
677
682
  # @deprecated, use eval_feature
@@ -753,7 +758,7 @@ class GrowthBook(object):
753
758
  self._subscriptions.add(callback)
754
759
  return lambda: self._subscriptions.remove(callback)
755
760
 
756
- def _track(self, experiment: Experiment, result: Result) -> None:
761
+ def _track(self, experiment: Experiment, result: Result, user_context: UserContext) -> None:
757
762
  if not self._trackingCallback:
758
763
  return None
759
764
  key = (
@@ -764,7 +769,7 @@ class GrowthBook(object):
764
769
  )
765
770
  if not self._tracked.get(key):
766
771
  try:
767
- self._trackingCallback(experiment=experiment, result=result)
772
+ self._trackingCallback(experiment=experiment, result=result, user_context=user_context)
768
773
  self._tracked[key] = True
769
774
  except Exception:
770
775
  pass
@@ -317,6 +317,10 @@ class GrowthBookClient:
317
317
  }
318
318
  self._sticky_bucket_cache_lock = False
319
319
 
320
+ # Plugin support
321
+ self._tracking_plugins: List[Any] = self.options.tracking_plugins or []
322
+ self._initialized_plugins: List[Any] = []
323
+
320
324
  self._features_repository = (
321
325
  EnhancedFeatureRepository(
322
326
  self.options.api_host or "https://cdn.growthbook.io",
@@ -330,9 +334,12 @@ class GrowthBookClient:
330
334
 
331
335
  self._global_context: Optional[GlobalContext] = None
332
336
  self._context_lock = asyncio.Lock()
337
+
338
+ # Initialize plugins
339
+ self._initialize_plugins()
333
340
 
334
341
 
335
- def _track(self, experiment: Experiment, result: Result) -> None:
342
+ def _track(self, experiment: Experiment, result: Result, user_context: UserContext) -> None:
336
343
  """Thread-safe tracking implementation"""
337
344
  if not self.options.on_experiment_viewed:
338
345
  return
@@ -348,7 +355,7 @@ class GrowthBookClient:
348
355
  with self._tracked_lock:
349
356
  if not self._tracked.get(key):
350
357
  try:
351
- self.options.on_experiment_viewed(experiment=experiment, result=result)
358
+ self.options.on_experiment_viewed(experiment, result, user_context)
352
359
  self._tracked[key] = True
353
360
  except Exception:
354
361
  logger.exception("Error in tracking callback")
@@ -492,25 +499,25 @@ class GrowthBookClient:
492
499
  """Evaluate a feature with proper async context management"""
493
500
  async with self._context_lock:
494
501
  context = await self.create_evaluation_context(user_context)
495
- result = core_eval_feature(key=key, evalContext=context)
502
+ result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
496
503
  return result
497
504
 
498
505
  async def is_on(self, key: str, user_context: UserContext) -> bool:
499
506
  """Check if a feature is enabled with proper async context management"""
500
507
  async with self._context_lock:
501
508
  context = await self.create_evaluation_context(user_context)
502
- return core_eval_feature(key=key, evalContext=context).on
509
+ return core_eval_feature(key=key, evalContext=context, tracking_cb=self._track).on
503
510
 
504
511
  async def is_off(self, key: str, user_context: UserContext) -> bool:
505
512
  """Check if a feature is set to off with proper async context management"""
506
513
  async with self._context_lock:
507
514
  context = await self.create_evaluation_context(user_context)
508
- return core_eval_feature(key=key, evalContext=context).off
515
+ return core_eval_feature(key=key, evalContext=context, tracking_cb=self._track).off
509
516
 
510
517
  async def get_feature_value(self, key: str, fallback: Any, user_context: UserContext) -> Any:
511
518
  async with self._context_lock:
512
519
  context = await self.create_evaluation_context(user_context)
513
- result = core_eval_feature(key=key, evalContext=context)
520
+ result = core_eval_feature(key=key, evalContext=context, tracking_cb=self._track)
514
521
  return result.value if result.value is not None else fallback
515
522
 
516
523
  async def run(self, experiment: Experiment, user_context: UserContext) -> Result:
@@ -539,4 +546,37 @@ class GrowthBookClient:
539
546
 
540
547
  # Clear context
541
548
  async with self._context_lock:
542
- self._global_context = None
549
+ self._global_context = None
550
+
551
+ # Cleanup plugins
552
+ self._cleanup_plugins()
553
+
554
+ def _initialize_plugins(self) -> None:
555
+ """Initialize all tracking plugins with this GrowthBookClient instance."""
556
+ for plugin in self._tracking_plugins:
557
+ try:
558
+ if hasattr(plugin, 'initialize'):
559
+ # Plugin is a class instance with initialize method
560
+ plugin.initialize(self)
561
+ self._initialized_plugins.append(plugin)
562
+ logger.debug(f"Initialized plugin: {plugin.__class__.__name__}")
563
+ elif callable(plugin):
564
+ # Plugin is a callable function
565
+ plugin(self)
566
+ self._initialized_plugins.append(plugin)
567
+ logger.debug(f"Initialized callable plugin: {plugin.__name__}")
568
+ else:
569
+ logger.warning(f"Plugin {plugin} is neither callable nor has initialize method")
570
+ except Exception as e:
571
+ logger.error(f"Failed to initialize plugin {plugin}: {e}")
572
+
573
+ def _cleanup_plugins(self) -> None:
574
+ """Cleanup all initialized plugins."""
575
+ for plugin in self._initialized_plugins:
576
+ try:
577
+ if hasattr(plugin, 'cleanup'):
578
+ plugin.cleanup()
579
+ logger.debug(f"Cleaned up plugin: {plugin.__class__.__name__}")
580
+ except Exception as e:
581
+ logger.error(f"Error cleaning up plugin {plugin}: {e}")
582
+ self._initialized_plugins.clear()
@@ -2,14 +2,16 @@ import json
2
2
  import logging
3
3
  import threading
4
4
  import time
5
- from typing import Dict, Any, Optional, List, Callable
5
+ from typing import Dict, Any, Optional, List, Callable, TYPE_CHECKING
6
6
  from .base import GrowthBookPlugin
7
7
 
8
- requests: Any = None
9
- try:
10
- import requests
11
- except ImportError:
12
- pass
8
+ if TYPE_CHECKING:
9
+ import requests # type: ignore
10
+ else:
11
+ try:
12
+ import requests # type: ignore
13
+ except ImportError:
14
+ requests = None
13
15
 
14
16
  logger = logging.getLogger("growthbook.plugins.growthbook_tracking")
15
17
 
@@ -88,22 +90,43 @@ class GrowthBookTrackingPlugin(GrowthBookPlugin):
88
90
  super().cleanup()
89
91
 
90
92
  def _setup_experiment_tracking(self, gb_instance) -> None:
91
- """Setup experiment tracking."""
92
- original_callback = getattr(gb_instance, '_trackingCallback', None)
93
+ """Setup experiment tracking for both legacy and async clients."""
93
94
 
94
- def tracking_wrapper(experiment, result):
95
+ def tracking_wrapper(experiment, result, user_context=None):
95
96
  # Track to ingestor
96
97
  self._track_experiment_viewed(experiment, result)
97
98
 
98
99
  # Call additional callback
99
100
  if self.additional_callback:
100
- self._safe_execute(self.additional_callback, experiment, result)
101
+ self._safe_execute(self.additional_callback, experiment, result, user_context)
102
+
103
+ # Check if it's the legacy GrowthBook client (has _trackingCallback)
104
+ if hasattr(gb_instance, '_trackingCallback'):
105
+ # Legacy GrowthBook client
106
+ original_callback = getattr(gb_instance, '_trackingCallback', None)
107
+
108
+ def legacy_wrapper(experiment, result, user_context=None):
109
+ tracking_wrapper(experiment, result, user_context)
110
+ # Call original callback
111
+ if original_callback:
112
+ self._safe_execute(original_callback, experiment, result, user_context)
113
+
114
+ gb_instance._trackingCallback = legacy_wrapper
115
+
116
+ elif hasattr(gb_instance, 'options') and hasattr(gb_instance.options, 'on_experiment_viewed'):
117
+ # New GrowthBookClient (async)
118
+ original_callback = gb_instance.options.on_experiment_viewed
119
+
120
+ def async_wrapper(experiment, result, user_context):
121
+ tracking_wrapper(experiment, result, user_context)
122
+ # Call original callback
123
+ if original_callback:
124
+ self._safe_execute(original_callback, experiment, result, user_context)
101
125
 
102
- # Call original callback
103
- if original_callback:
104
- self._safe_execute(original_callback, experiment, result)
126
+ gb_instance.options.on_experiment_viewed = async_wrapper
105
127
 
106
- gb_instance._trackingCallback = tracking_wrapper
128
+ else:
129
+ self.logger.warning("_trackingCallback or on_experiment_viewed properties not found - tracking may not work properly")
107
130
 
108
131
  def _setup_feature_tracking(self, gb_instance):
109
132
  """Setup feature evaluation tracking."""
@@ -77,6 +77,7 @@ class RequestContextPlugin(GrowthBookPlugin):
77
77
  self.client_side_attributes = client_side_attributes
78
78
  self.extract_utm = extract_utm
79
79
  self.extract_user_agent = extract_user_agent
80
+ self._extracted_attributes: Dict[str, Any] = {}
80
81
 
81
82
  def initialize(self, gb_instance) -> None:
82
83
  """Initialize plugin - extract attributes from request context."""
@@ -87,18 +88,33 @@ class RequestContextPlugin(GrowthBookPlugin):
87
88
  request_attributes = self._extract_all_attributes()
88
89
 
89
90
  if request_attributes:
90
- # Merge with existing attributes (existing take precedence)
91
- current_attributes = gb_instance.get_attributes()
92
- merged_attributes = {**request_attributes, **current_attributes}
93
- gb_instance.set_attributes(merged_attributes)
94
-
95
- self.logger.info(f"Extracted {len(request_attributes)} request attributes")
91
+ # Check client type and merge attributes accordingly
92
+ if hasattr(gb_instance, 'get_attributes') and hasattr(gb_instance, 'set_attributes'):
93
+ # Legacy GrowthBook client
94
+ current_attributes = gb_instance.get_attributes()
95
+ merged_attributes = {**request_attributes, **current_attributes}
96
+ gb_instance.set_attributes(merged_attributes)
97
+ self.logger.info(f"Extracted {len(request_attributes)} request attributes for legacy client")
98
+
99
+ elif hasattr(gb_instance, 'options'):
100
+ # New GrowthBookClient - store attributes for future use
101
+ # Note: GrowthBookClient doesn't have get/set_attributes, but we can store
102
+ # the extracted attributes for potential future use or logging
103
+ self._extracted_attributes = request_attributes
104
+ self.logger.info(f"Extracted {len(request_attributes)} request attributes for async client (stored for reference)")
105
+
106
+ else:
107
+ self.logger.warning("Unknown client type - cannot set attributes")
96
108
  else:
97
109
  self.logger.debug("No request context available")
98
110
 
99
111
  except Exception as e:
100
112
  self.logger.error(f"Failed to extract request attributes: {e}")
101
113
 
114
+ def get_extracted_attributes(self) -> Dict[str, Any]:
115
+ """Get the attributes extracted from request context."""
116
+ return self._extracted_attributes.copy()
117
+
102
118
  def _extract_all_attributes(self) -> Dict[str, Any]:
103
119
  """Extract all available attributes from request context."""
104
120
  attributes = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: growthbook
3
- Version: 1.3.1
3
+ Version: 1.4.1
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=wx8DI5CN6we3wD2KfxR9CgroKfiiF7LQSQuqdrzQyjw,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=oxjXeGkYOU164miEcZzTFPCgMJrcCB_XHCB7IOOabFM,31037
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.1.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
12
+ growthbook-1.4.1.dist-info/METADATA,sha256=G1jZbYb7vuJL1FIo2gdBimQVFwuoGI-JLvd4dJGZxLQ,22073
13
+ growthbook-1.4.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
14
+ growthbook-1.4.1.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
15
+ growthbook-1.4.1.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- growthbook/__init__.py,sha256=2pw6uyYKI4IQ59LZbCUsNroy4LLFZ_5XyPmscPUyAwI,444
2
- growthbook/common_types.py,sha256=1jDWwS_9L0z_AZb5AJq1arEqi6QU66N3S4vYzkcDFDY,14728
3
- growthbook/core.py,sha256=LCz5f5rchUL056kgJxgH566NoZWr2nr4hhL7MRlyNiM,35184
4
- growthbook/growthbook.py,sha256=IJmFKTx87zAwQ6nO_Gepm5-S_Mtwf-lnt2RFBviB6Lk,30672
5
- growthbook/growthbook_client.py,sha256=zLgoBSBCSfCb8OE0b-XQiDAEB4djTiMSrbNpqBui-M0,21454
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=aL_dgsPt51IZNxRVZVA5U3QEiGW-e-l5tC7Owl6CZO0,10032
10
- growthbook/plugins/request_context.py,sha256=7_9LOfS5nXbxXE7KJ-zNT4Ug4-_698A4c3t0hjr6l4c,11895
11
- growthbook-1.3.1.dist-info/licenses/LICENSE,sha256=D-TcBckB0dTPUlNJ8jBiTIJIj1ekHLB1CY7HJtJKhMY,1069
12
- growthbook-1.3.1.dist-info/METADATA,sha256=FwTzARVgHtfSH2ZnCWVq0G9k5ISiPyQDIeYjabR3vCQ,22073
13
- growthbook-1.3.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
14
- growthbook-1.3.1.dist-info/top_level.txt,sha256=dzfRQFGYejCIUstRSrrRVTMlxf7pBqASTI5S8gGRlXw,11
15
- growthbook-1.3.1.dist-info/RECORD,,