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 +22 -0
- growthbook/common_types.py +446 -0
- growthbook/core.py +985 -0
- growthbook/growthbook.py +1175 -0
- growthbook/growthbook_client.py +661 -0
- growthbook/plugins/__init__.py +16 -0
- growthbook/plugins/base.py +103 -0
- growthbook/plugins/growthbook_tracking.py +285 -0
- growthbook/plugins/request_context.py +358 -0
- growthbook/py.typed +0 -0
- growthbook-2.1.2.dist-info/METADATA +700 -0
- growthbook-2.1.2.dist-info/RECORD +15 -0
- growthbook-2.1.2.dist-info/WHEEL +6 -0
- growthbook-2.1.2.dist-info/licenses/LICENSE +22 -0
- growthbook-2.1.2.dist-info/top_level.txt +1 -0
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
|