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