growthbook 2.1.2__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 ADDED
@@ -0,0 +1,22 @@
1
+ from .growthbook import *
2
+
3
+ from .growthbook_client import (
4
+ GrowthBookClient,
5
+ EnhancedFeatureRepository,
6
+ FeatureCache,
7
+ BackoffStrategy
8
+ )
9
+
10
+ # Plugin support
11
+ from .plugins import (
12
+ GrowthBookTrackingPlugin,
13
+ growthbook_tracking_plugin,
14
+ RequestContextPlugin,
15
+ ClientSideAttributes,
16
+ request_context_plugin,
17
+ client_side_attributes
18
+ )
19
+
20
+ # x-release-please-start-version
21
+ __version__ = "2.1.2"
22
+ # x-release-please-end
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+ from token import OP
5
+ # Only require typing_extensions if using Python 3.7 or earlier
6
+ if sys.version_info >= (3, 8):
7
+ from typing import TypedDict
8
+ else:
9
+ from typing_extensions import TypedDict
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Callable, Dict, List, Optional, Union, Set, Tuple
13
+ from enum import Enum
14
+ from abc import ABC, abstractmethod
15
+
16
+ class VariationMeta(TypedDict):
17
+ key: str
18
+ name: str
19
+ passthrough: bool
20
+
21
+
22
+ class Filter(TypedDict):
23
+ seed: str
24
+ ranges: List[Tuple[float, float]]
25
+ hashVersion: int
26
+ attribute: str
27
+
28
+ class Experiment(object):
29
+ def __init__(
30
+ self,
31
+ key: str,
32
+ variations: List[Any],
33
+ weights: Optional[List[float]] = None,
34
+ active: bool = True,
35
+ status: str = "running",
36
+ coverage: Optional[float] = None,
37
+ condition: Optional[Dict[str, Any]] = None,
38
+ namespace: Optional[Tuple[str, float, float]] = None,
39
+ url: str = "",
40
+ include: Optional[Any] = None,
41
+ groups: Optional[List[Any]] = None,
42
+ force: Optional[int] = None,
43
+ hashAttribute: str = "id",
44
+ fallbackAttribute: Optional[str] = None,
45
+ hashVersion: Optional[int] = None,
46
+ ranges: Optional[List[Tuple[float, float]]] = None,
47
+ meta: Optional[List[VariationMeta]] = None,
48
+ filters: Optional[List[Filter]] = None,
49
+ seed: Optional[str] = None,
50
+ name: Optional[str] = None,
51
+ phase: Optional[str] = None,
52
+ disableStickyBucketing: bool = False,
53
+ bucketVersion: Optional[int] = None,
54
+ minBucketVersion: Optional[int] = None,
55
+ parentConditions: Optional[List[Dict[str, Any]]] = None,
56
+ ) -> None:
57
+ self.key = key
58
+ self.variations = variations
59
+ self.weights = weights
60
+ self.active = active
61
+ self.coverage = coverage
62
+ self.condition = condition
63
+ self.namespace = namespace
64
+ self.force = force
65
+ self.hashAttribute = hashAttribute
66
+ self.hashVersion = hashVersion or 1
67
+ self.ranges = ranges
68
+ self.meta = meta
69
+ self.filters = filters
70
+ self.seed = seed
71
+ self.name = name
72
+ self.phase = phase
73
+ self.disableStickyBucketing = disableStickyBucketing
74
+ self.bucketVersion = bucketVersion or 0
75
+ self.minBucketVersion = minBucketVersion or 0
76
+ self.parentConditions = parentConditions
77
+
78
+ self.fallbackAttribute = None
79
+ if not self.disableStickyBucketing:
80
+ self.fallbackAttribute = fallbackAttribute
81
+
82
+ # Deprecated properties
83
+ self.status = status
84
+ self.url = url
85
+ self.include = include
86
+ self.groups = groups
87
+
88
+ def to_dict(self) -> Dict[str, Any]:
89
+ obj: Dict[str, Any] = {
90
+ "key": self.key,
91
+ "variations": self.variations,
92
+ "weights": self.weights,
93
+ "active": self.active,
94
+ "coverage": self.coverage or 1,
95
+ "condition": self.condition,
96
+ "namespace": self.namespace,
97
+ "force": self.force,
98
+ "hashAttribute": self.hashAttribute,
99
+ "hashVersion": self.hashVersion,
100
+ "ranges": self.ranges,
101
+ "meta": self.meta,
102
+ "filters": self.filters,
103
+ "seed": self.seed,
104
+ "name": self.name,
105
+ "phase": self.phase,
106
+ }
107
+
108
+ if self.fallbackAttribute:
109
+ obj["fallbackAttribute"] = self.fallbackAttribute
110
+ if self.disableStickyBucketing:
111
+ obj["disableStickyBucketing"] = True
112
+ if self.bucketVersion:
113
+ obj["bucketVersion"] = self.bucketVersion
114
+ if self.minBucketVersion:
115
+ obj["minBucketVersion"] = self.minBucketVersion
116
+ if self.parentConditions:
117
+ obj["parentConditions"] = self.parentConditions
118
+
119
+ return obj
120
+
121
+ def update(self, data: Dict[str, Any]) -> None:
122
+ weights = data.get("weights", None)
123
+ status = data.get("status", None)
124
+ coverage = data.get("coverage", None)
125
+ url = data.get("url", None)
126
+ groups = data.get("groups", None)
127
+ force = data.get("force", None)
128
+
129
+ if weights is not None:
130
+ self.weights = weights
131
+ if status is not None:
132
+ self.status = status
133
+ if coverage is not None:
134
+ self.coverage = coverage
135
+ if url is not None:
136
+ self.url = url
137
+ if groups is not None:
138
+ self.groups = groups
139
+ if force is not None:
140
+ self.force = force
141
+
142
+
143
+ class Result(object):
144
+ def __init__(
145
+ self,
146
+ variationId: int,
147
+ inExperiment: bool,
148
+ value: Any,
149
+ hashUsed: bool,
150
+ hashAttribute: str,
151
+ hashValue: str,
152
+ featureId: Optional[str],
153
+ meta: Optional[VariationMeta] = None,
154
+ bucket: Optional[float] = None,
155
+ stickyBucketUsed: bool = False,
156
+ ) -> None:
157
+ self.variationId = variationId
158
+ self.inExperiment = inExperiment
159
+ self.value = value
160
+ self.hashUsed = hashUsed
161
+ self.hashAttribute = hashAttribute
162
+ self.hashValue = hashValue
163
+ self.featureId = featureId or None
164
+ self.bucket = bucket
165
+ self.stickyBucketUsed = stickyBucketUsed
166
+
167
+ self.key = str(variationId)
168
+ self.name = ""
169
+ self.passthrough = False
170
+
171
+ if meta:
172
+ if "name" in meta:
173
+ self.name = meta["name"]
174
+ if "key" in meta:
175
+ self.key = meta["key"]
176
+ if "passthrough" in meta:
177
+ self.passthrough = meta["passthrough"]
178
+
179
+ def to_dict(self) -> Dict[str, Any]:
180
+ obj: Dict[str, Any] = {
181
+ "featureId": self.featureId,
182
+ "variationId": self.variationId,
183
+ "inExperiment": self.inExperiment,
184
+ "value": self.value,
185
+ "hashUsed": self.hashUsed,
186
+ "hashAttribute": self.hashAttribute,
187
+ "hashValue": self.hashValue,
188
+ "key": self.key,
189
+ "stickyBucketUsed": self.stickyBucketUsed,
190
+ }
191
+
192
+ if self.bucket is not None:
193
+ obj["bucket"] = self.bucket
194
+ if self.name:
195
+ obj["name"] = self.name
196
+ if self.passthrough:
197
+ obj["passthrough"] = True
198
+
199
+ return obj
200
+
201
+ class FeatureResult(object):
202
+ def __init__(
203
+ self,
204
+ value: Any,
205
+ source: str,
206
+ experiment: Optional[Experiment] = None,
207
+ experimentResult: Optional[Result] = None,
208
+ ruleId: Optional[str] = None,
209
+ ) -> None:
210
+ self.value = value
211
+ self.source = source
212
+ self.ruleId = ruleId
213
+ self.experiment = experiment
214
+ self.experimentResult = experimentResult
215
+ self.on = bool(value)
216
+ self.off = not bool(value)
217
+
218
+ def to_dict(self) -> Dict[str, Any]:
219
+ data: Dict[str, Any] = {
220
+ "value": self.value,
221
+ "source": self.source,
222
+ "on": self.on,
223
+ "off": self.off,
224
+ "ruleId": self.ruleId or "",
225
+ }
226
+ if self.experiment:
227
+ data["experiment"] = self.experiment.to_dict()
228
+ if self.experimentResult:
229
+ data["experimentResult"] = self.experimentResult.to_dict()
230
+
231
+ return data
232
+
233
+ class Feature(object):
234
+ def __init__(self, defaultValue: Any = None, rules: Optional[List[Any]] = None) -> None:
235
+ if rules is None:
236
+ rules = []
237
+ self.defaultValue = defaultValue
238
+ self.rules: List[FeatureRule] = []
239
+ for rule in rules:
240
+ if isinstance(rule, FeatureRule):
241
+ self.rules.append(rule)
242
+ else:
243
+ self.rules.append(FeatureRule(
244
+ id=rule.get("id", None),
245
+ key=rule.get("key", ""),
246
+ variations=rule.get("variations", None),
247
+ weights=rule.get("weights", None),
248
+ coverage=rule.get("coverage", None),
249
+ condition=rule.get("condition", None),
250
+ namespace=rule.get("namespace", None),
251
+ force=rule.get("force", None),
252
+ hashAttribute=rule.get("hashAttribute", "id"),
253
+ fallbackAttribute=rule.get("fallbackAttribute", None),
254
+ hashVersion=rule.get("hashVersion", None),
255
+ range=rule.get("range", None),
256
+ ranges=rule.get("ranges", None),
257
+ meta=rule.get("meta", None),
258
+ filters=rule.get("filters", None),
259
+ seed=rule.get("seed", None),
260
+ name=rule.get("name", None),
261
+ phase=rule.get("phase", None),
262
+ disableStickyBucketing=rule.get("disableStickyBucketing", False),
263
+ bucketVersion=rule.get("bucketVersion", None),
264
+ minBucketVersion=rule.get("minBucketVersion", None),
265
+ parentConditions=rule.get("parentConditions", None),
266
+ ))
267
+
268
+ def to_dict(self) -> Dict[str, Any]:
269
+ return {
270
+ "defaultValue": self.defaultValue,
271
+ "rules": [rule.to_dict() for rule in self.rules],
272
+ }
273
+
274
+ class FeatureRule(object):
275
+ def __init__(
276
+ self,
277
+ id: Optional[str] = None,
278
+ key: str = "",
279
+ variations: Optional[List[Any]] = None,
280
+ weights: Optional[List[float]] = None,
281
+ coverage: Optional[float] = None,
282
+ condition: Optional[Dict[str, Any]] = None,
283
+ namespace: Optional[Tuple[str, float, float]] = None,
284
+ force: Optional[Any] = None,
285
+ hashAttribute: str = "id",
286
+ fallbackAttribute: Optional[str] = None,
287
+ hashVersion: Optional[int] = None,
288
+ range: Optional[Tuple[float, float]] = None,
289
+ ranges: Optional[List[Tuple[float, float]]] = None,
290
+ meta: Optional[List[VariationMeta]] = None,
291
+ filters: Optional[List[Filter]] = None,
292
+ seed: Optional[str] = None,
293
+ name: Optional[str] = None,
294
+ phase: Optional[str] = None,
295
+ disableStickyBucketing: bool = False,
296
+ bucketVersion: Optional[int] = None,
297
+ minBucketVersion: Optional[int] = None,
298
+ parentConditions: Optional[List[Dict[str, Any]]] = None,
299
+ ) -> None:
300
+
301
+ if disableStickyBucketing:
302
+ fallbackAttribute = None
303
+
304
+ self.id = id
305
+ self.key = key
306
+ self.variations = variations
307
+ self.weights = weights
308
+ self.coverage = coverage
309
+ self.condition = condition
310
+ self.namespace = namespace
311
+ self.force = force
312
+ self.hashAttribute = hashAttribute
313
+ self.fallbackAttribute = fallbackAttribute
314
+ self.hashVersion = hashVersion or 1
315
+ self.range = range
316
+ self.ranges = ranges
317
+ self.meta = meta
318
+ self.filters = filters
319
+ self.seed = seed
320
+ self.name = name
321
+ self.phase = phase
322
+ self.disableStickyBucketing = disableStickyBucketing
323
+ self.bucketVersion = bucketVersion or 0
324
+ self.minBucketVersion = minBucketVersion or 0
325
+ self.parentConditions = parentConditions
326
+
327
+ def to_dict(self) -> Dict[str, Any]:
328
+ data: Dict[str, Any] = {}
329
+ if self.id:
330
+ data["id"] = self.id
331
+ if self.key:
332
+ data["key"] = self.key
333
+ if self.variations is not None:
334
+ data["variations"] = self.variations
335
+ if self.weights is not None:
336
+ data["weights"] = self.weights
337
+ if self.coverage and self.coverage != 1:
338
+ data["coverage"] = self.coverage
339
+ if self.condition is not None:
340
+ data["condition"] = self.condition
341
+ if self.namespace is not None:
342
+ data["namespace"] = self.namespace
343
+ if self.force is not None:
344
+ data["force"] = self.force
345
+ if self.hashAttribute != "id":
346
+ data["hashAttribute"] = self.hashAttribute
347
+ if self.hashVersion:
348
+ data["hashVersion"] = self.hashVersion
349
+ if self.range is not None:
350
+ data["range"] = self.range
351
+ if self.ranges is not None:
352
+ data["ranges"] = self.ranges
353
+ if self.meta is not None:
354
+ data["meta"] = self.meta
355
+ if self.filters is not None:
356
+ data["filters"] = self.filters
357
+ if self.seed is not None:
358
+ data["seed"] = self.seed
359
+ if self.name is not None:
360
+ data["name"] = self.name
361
+ if self.phase is not None:
362
+ data["phase"] = self.phase
363
+ if self.fallbackAttribute:
364
+ data["fallbackAttribute"] = self.fallbackAttribute
365
+ if self.disableStickyBucketing:
366
+ data["disableStickyBucketing"] = True
367
+ if self.bucketVersion:
368
+ data["bucketVersion"] = self.bucketVersion
369
+ if self.minBucketVersion:
370
+ data["minBucketVersion"] = self.minBucketVersion
371
+ if self.parentConditions:
372
+ data["parentConditions"] = self.parentConditions
373
+
374
+ return data
375
+
376
+ class AbstractStickyBucketService(ABC):
377
+ @abstractmethod
378
+ def get_assignments(self, attributeName: str, attributeValue: str) -> Optional[Dict]:
379
+ pass
380
+
381
+ @abstractmethod
382
+ def save_assignments(self, doc: Dict) -> None:
383
+ pass
384
+
385
+ def get_key(self, attributeName: str, attributeValue: str) -> str:
386
+ return f"{attributeName}||{attributeValue}"
387
+
388
+ # By default, just loop through all attributes and call get_assignments
389
+ # Override this method in subclasses to perform a multi-query instead
390
+ def get_all_assignments(self, attributes: Dict[str, str]) -> Dict[str, Dict]:
391
+ docs = {}
392
+ for attributeName, attributeValue in attributes.items():
393
+ doc = self.get_assignments(attributeName, attributeValue)
394
+ if doc:
395
+ docs[self.get_key(attributeName, attributeValue)] = doc
396
+ return docs
397
+
398
+ @dataclass
399
+ class StackContext:
400
+ id: Optional[str] = None
401
+ evaluated_features: Set[str] = field(default_factory=set)
402
+
403
+ class FeatureRefreshStrategy(Enum):
404
+ STALE_WHILE_REVALIDATE = 'HTTP_REFRESH'
405
+ SERVER_SENT_EVENTS = 'SSE'
406
+ @dataclass
407
+ class UserContext:
408
+ # user_id: Optional[str] = None
409
+ url: str = ""
410
+ attributes: Dict[str, Any] = field(default_factory=dict)
411
+ groups: Dict[str, str] = field(default_factory=dict)
412
+ forced_variations: Dict[str, Any] = field(default_factory=dict)
413
+ overrides: Dict[str, Any] = field(default_factory=dict)
414
+ sticky_bucket_assignment_docs: Dict[str, Any] = field(default_factory=dict)
415
+ skip_all_experiments: bool = False
416
+
417
+ @dataclass
418
+ class Options:
419
+ url: Optional[str] = None
420
+ api_host: Optional[str] = "https://cdn.growthbook.io"
421
+ client_key: Optional[str] = None
422
+ decryption_key: Optional[str] = None
423
+ cache_ttl: int = 60
424
+ enabled: bool = True
425
+ qa_mode: bool = False
426
+ enable_dev_mode: bool = False
427
+ # forced_variations: Dict[str, Any] = field(default_factory=dict)
428
+ refresh_strategy: Optional[FeatureRefreshStrategy] = FeatureRefreshStrategy.STALE_WHILE_REVALIDATE
429
+ sticky_bucket_service: Optional[AbstractStickyBucketService] = None
430
+ sticky_bucket_identifier_attributes: Optional[List[str]] = None
431
+ on_experiment_viewed: Optional[Callable[[Experiment, Result, Optional[UserContext]], None]] = None
432
+ on_feature_usage: Optional[Callable[[str, 'FeatureResult', UserContext], None]] = None
433
+ tracking_plugins: Optional[List[Any]] = None
434
+
435
+
436
+ @dataclass
437
+ class GlobalContext:
438
+ options: Options
439
+ features: Dict[str, Any] = field(default_factory=dict)
440
+ saved_groups: Dict[str, Any] = field(default_factory=dict)
441
+
442
+ @dataclass
443
+ class EvaluationContext:
444
+ user: UserContext
445
+ global_ctx: GlobalContext
446
+ stack: StackContext