featurevisor 0.2.0__tar.gz → 1.0.0__tar.gz
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.
- featurevisor-1.0.0/PKG-INFO +640 -0
- featurevisor-1.0.0/README.md +623 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/pyproject.toml +1 -1
- featurevisor-1.0.0/src/featurevisor/__init__.py +10 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/child.py +6 -4
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/cli.py +8 -6
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/datafile_reader.py +17 -6
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/evaluate.py +16 -20
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/events.py +3 -3
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/helpers.py +10 -8
- featurevisor-1.0.0/src/featurevisor/instance.py +371 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/logger.py +7 -8
- featurevisor-1.0.0/src/featurevisor/modules.py +184 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/project.py +17 -20
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/tester.py +87 -51
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/types.py +17 -7
- featurevisor-1.0.0/src/featurevisor.egg-info/PKG-INFO +640 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/SOURCES.txt +2 -1
- featurevisor-1.0.0/tests/test_cli.py +58 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_conditions_parity.py +8 -3
- featurevisor-1.0.0/tests/test_conformance.py +43 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_datafile_reader_parity.py +14 -5
- featurevisor-1.0.0/tests/test_helpers_parity.py +52 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_instance_parity.py +34 -18
- featurevisor-1.0.0/tests/test_sdk.py +408 -0
- featurevisor-0.2.0/PKG-INFO +0 -374
- featurevisor-0.2.0/README.md +0 -357
- featurevisor-0.2.0/src/featurevisor/__init__.py +0 -52
- featurevisor-0.2.0/src/featurevisor/hooks.py +0 -31
- featurevisor-0.2.0/src/featurevisor/instance.py +0 -200
- featurevisor-0.2.0/src/featurevisor.egg-info/PKG-INFO +0 -374
- featurevisor-0.2.0/tests/test_cli.py +0 -35
- featurevisor-0.2.0/tests/test_helpers_parity.py +0 -47
- featurevisor-0.2.0/tests/test_sdk.py +0 -171
- {featurevisor-0.2.0 → featurevisor-1.0.0}/LICENSE +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/setup.cfg +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/__main__.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/bucketer.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/compare_versions.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/conditions.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/emitter.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/murmurhash.py +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/dependency_links.txt +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/entry_points.txt +0 -0
- {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: featurevisor
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Featurevisor Python SDK
|
|
5
|
+
Author: Fahad Heylaal
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Featurevisor Python SDK <!-- omit in toc -->
|
|
19
|
+
|
|
20
|
+
This repository ports the latest Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/) to Python.
|
|
21
|
+
|
|
22
|
+
The package name is `featurevisor`, and it targets Python 3.10+.
|
|
23
|
+
|
|
24
|
+
This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
25
|
+
|
|
26
|
+
## Table of contents <!-- omit in toc -->
|
|
27
|
+
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Public API](#public-api)
|
|
30
|
+
- [Initialization](#initialization)
|
|
31
|
+
- [Evaluation types](#evaluation-types)
|
|
32
|
+
- [Context](#context)
|
|
33
|
+
- [Setting initial context](#setting-initial-context)
|
|
34
|
+
- [Setting after initialization](#setting-after-initialization)
|
|
35
|
+
- [Replacing existing context](#replacing-existing-context)
|
|
36
|
+
- [Manually passing context](#manually-passing-context)
|
|
37
|
+
- [Check if enabled](#check-if-enabled)
|
|
38
|
+
- [Getting variation](#getting-variation)
|
|
39
|
+
- [Getting variables](#getting-variables)
|
|
40
|
+
- [Type specific methods](#type-specific-methods)
|
|
41
|
+
- [Getting all evaluations](#getting-all-evaluations)
|
|
42
|
+
- [Sticky](#sticky)
|
|
43
|
+
- [Initialize with sticky](#initialize-with-sticky)
|
|
44
|
+
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
45
|
+
- [Setting datafile](#setting-datafile)
|
|
46
|
+
- [Merging by default](#merging-by-default)
|
|
47
|
+
- [Replacing](#replacing)
|
|
48
|
+
- [Loading datafiles on demand](#loading-datafiles-on-demand)
|
|
49
|
+
- [Updating datafile](#updating-datafile)
|
|
50
|
+
- [Interval-based update](#interval-based-update)
|
|
51
|
+
- [Diagnostics](#diagnostics)
|
|
52
|
+
- [Levels](#levels)
|
|
53
|
+
- [Handler](#handler)
|
|
54
|
+
- [Events](#events)
|
|
55
|
+
- [`datafile_set`](#datafile_set)
|
|
56
|
+
- [`context_set`](#context_set)
|
|
57
|
+
- [`sticky_set`](#sticky_set)
|
|
58
|
+
- [`error`](#error)
|
|
59
|
+
- [Modules](#modules)
|
|
60
|
+
- [Defining a module](#defining-a-module)
|
|
61
|
+
- [Registering modules](#registering-modules)
|
|
62
|
+
- [Child instance](#child-instance)
|
|
63
|
+
- [Close](#close)
|
|
64
|
+
- [CLI usage](#cli-usage)
|
|
65
|
+
- [Test](#test)
|
|
66
|
+
- [Benchmark](#benchmark)
|
|
67
|
+
- [Assess distribution](#assess-distribution)
|
|
68
|
+
- [Development](#development)
|
|
69
|
+
- [Releasing](#releasing)
|
|
70
|
+
- [License](#license)
|
|
71
|
+
|
|
72
|
+
<!-- FEATUREVISOR_DOCS_BEGIN -->
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install featurevisor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Public API
|
|
81
|
+
|
|
82
|
+
The main runtime API is `create_featurevisor()`:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from featurevisor import Featurevisor, create_featurevisor
|
|
86
|
+
|
|
87
|
+
f: Featurevisor = create_featurevisor({
|
|
88
|
+
"datafile": datafile_content,
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Most applications only need `create_featurevisor` and the `Featurevisor` instance type. Public extension and observability APIs include `FeaturevisorModule`, diagnostics, events, and the datafile dictionaries accepted by the factory.
|
|
93
|
+
|
|
94
|
+
## Initialization
|
|
95
|
+
|
|
96
|
+
Initialize the SDK with Featurevisor datafile content:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from urllib.request import urlopen
|
|
100
|
+
import json
|
|
101
|
+
|
|
102
|
+
from featurevisor import create_featurevisor
|
|
103
|
+
|
|
104
|
+
datafile_url = "https://cdn.yoursite.com/datafile.json"
|
|
105
|
+
|
|
106
|
+
with urlopen(datafile_url) as response:
|
|
107
|
+
datafile_content = json.load(response)
|
|
108
|
+
|
|
109
|
+
f = create_featurevisor({
|
|
110
|
+
"datafile": datafile_content,
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Evaluation types
|
|
115
|
+
|
|
116
|
+
We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/):
|
|
117
|
+
|
|
118
|
+
- [**Flag**](#check-if-enabled) (`bool`): whether the feature is enabled or not
|
|
119
|
+
- [**Variation**](#getting-variation) (`string`): the variation of the feature (if any)
|
|
120
|
+
- [**Variables**](#getting-variables): variable values of the feature (if any)
|
|
121
|
+
|
|
122
|
+
## Context
|
|
123
|
+
|
|
124
|
+
Context is a plain dictionary of attribute values used during evaluation:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
context = {
|
|
128
|
+
"userId": "123",
|
|
129
|
+
"country": "nl",
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Setting initial context
|
|
134
|
+
|
|
135
|
+
You can provide context at initialization:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
f = create_featurevisor({
|
|
139
|
+
"context": {
|
|
140
|
+
"deviceId": "123",
|
|
141
|
+
"country": "nl",
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Setting after initialization
|
|
147
|
+
|
|
148
|
+
You can merge more context later:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
f.set_context({
|
|
152
|
+
"userId": "234",
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Replacing existing context
|
|
157
|
+
|
|
158
|
+
Or replace the existing context:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
f.set_context(
|
|
162
|
+
{
|
|
163
|
+
"deviceId": "123",
|
|
164
|
+
"userId": "234",
|
|
165
|
+
"country": "nl",
|
|
166
|
+
"browser": "chrome",
|
|
167
|
+
},
|
|
168
|
+
True,
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Manually passing context
|
|
173
|
+
|
|
174
|
+
You can also pass additional per-evaluation context:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
is_enabled = f.is_enabled("my_feature", {"country": "nl"})
|
|
178
|
+
variation = f.get_variation("my_feature", {"country": "nl"})
|
|
179
|
+
variable_value = f.get_variable("my_feature", "my_variable", {"country": "nl"})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Check if enabled
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
if f.is_enabled("my_feature"):
|
|
186
|
+
pass
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Getting variation
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
variation = f.get_variation("my_feature")
|
|
193
|
+
|
|
194
|
+
if variation == "treatment":
|
|
195
|
+
pass
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Getting variables
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
bg_color = f.get_variable("my_feature", "bgColor")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Type specific methods
|
|
205
|
+
|
|
206
|
+
Typed convenience methods are also available:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
f.get_variable_boolean(feature_key, variable_key, context={})
|
|
210
|
+
f.get_variable_string(feature_key, variable_key, context={})
|
|
211
|
+
f.get_variable_integer(feature_key, variable_key, context={})
|
|
212
|
+
f.get_variable_double(feature_key, variable_key, context={})
|
|
213
|
+
f.get_variable_array(feature_key, variable_key, context={})
|
|
214
|
+
f.get_variable_object(feature_key, variable_key, context={})
|
|
215
|
+
f.get_variable_json(feature_key, variable_key, context={})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Type specific methods do not coerce values. `get_variable_integer()` returns `None` for the string `"1"`, and boolean getters return `None` for non-boolean values.
|
|
219
|
+
|
|
220
|
+
## Getting all evaluations
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
all_evaluations = f.get_all_evaluations()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Sticky
|
|
227
|
+
|
|
228
|
+
### Initialize with sticky
|
|
229
|
+
|
|
230
|
+
You can pin feature evaluations with sticky values:
|
|
231
|
+
|
|
232
|
+
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, {"sticky": ...})` when a child needs its own sticky state.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
f = create_featurevisor({
|
|
236
|
+
"sticky": {
|
|
237
|
+
"myFeatureKey": {
|
|
238
|
+
"enabled": True,
|
|
239
|
+
"variation": "treatment",
|
|
240
|
+
"variables": {
|
|
241
|
+
"myVariableKey": "myVariableValue",
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Set sticky afterwards
|
|
249
|
+
|
|
250
|
+
Or update them later:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
f.set_sticky({
|
|
254
|
+
"myFeatureKey": {
|
|
255
|
+
"enabled": False,
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Setting datafile
|
|
261
|
+
|
|
262
|
+
You may initialize the SDK without passing `datafile`, and set it later on. The SDK accepts either parsed JSON content or a JSON string:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
f.set_datafile(datafile_content)
|
|
266
|
+
f.set_datafile(json.dumps(datafile_content))
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Merging by default
|
|
270
|
+
|
|
271
|
+
By default, `set_datafile(datafile)` merges incoming content into the SDK's current datafile:
|
|
272
|
+
|
|
273
|
+
- top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile
|
|
274
|
+
- `segments` are merged, with incoming entries overriding existing ones
|
|
275
|
+
- `features` are merged, with incoming entries overriding existing ones
|
|
276
|
+
|
|
277
|
+
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes [loading datafiles on demand](#loading-datafiles-on-demand) possible.
|
|
278
|
+
|
|
279
|
+
### Replacing
|
|
280
|
+
|
|
281
|
+
To fully replace the stored datafile, pass `True` as the second argument:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
f.set_datafile(datafile_content, True)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Loading datafiles on demand
|
|
288
|
+
|
|
289
|
+
Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
|
|
290
|
+
|
|
291
|
+
This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application. You can load the datafile for the current part, and load others only when the user reaches them:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from urllib.request import urlopen
|
|
295
|
+
import json
|
|
296
|
+
|
|
297
|
+
from featurevisor import create_featurevisor
|
|
298
|
+
|
|
299
|
+
f = create_featurevisor({})
|
|
300
|
+
|
|
301
|
+
def load_datafile(target):
|
|
302
|
+
url = f"https://cdn.yoursite.com/production/featurevisor-{target}.json"
|
|
303
|
+
with urlopen(url) as response:
|
|
304
|
+
datafile = json.load(response)
|
|
305
|
+
|
|
306
|
+
# merges into whatever was loaded before
|
|
307
|
+
f.set_datafile(datafile)
|
|
308
|
+
|
|
309
|
+
load_datafile("products")
|
|
310
|
+
|
|
311
|
+
# later, when the user reaches checkout
|
|
312
|
+
load_datafile("checkout")
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Updating datafile
|
|
316
|
+
|
|
317
|
+
You can set the datafile as many times as you want in your application, which will emit a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly.
|
|
318
|
+
|
|
319
|
+
### Interval-based update
|
|
320
|
+
|
|
321
|
+
Here's a minimal interval-style example:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
import json
|
|
325
|
+
import threading
|
|
326
|
+
from urllib.request import urlopen
|
|
327
|
+
|
|
328
|
+
def update_datafile():
|
|
329
|
+
with urlopen(datafile_url) as response:
|
|
330
|
+
f.set_datafile(json.load(response))
|
|
331
|
+
|
|
332
|
+
threading.Timer(5 * 60, update_datafile).start()
|
|
333
|
+
|
|
334
|
+
update_datafile()
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Diagnostics
|
|
338
|
+
|
|
339
|
+
By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix.
|
|
340
|
+
|
|
341
|
+
### Levels
|
|
342
|
+
|
|
343
|
+
Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`.
|
|
344
|
+
|
|
345
|
+
Set the level during initialization or update it afterwards:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
f = create_featurevisor({"logLevel": "debug"})
|
|
349
|
+
f.set_log_level("info")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Handler
|
|
353
|
+
|
|
354
|
+
Use `onDiagnostic` to send structured diagnostics to your observability system:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
f = create_featurevisor({
|
|
358
|
+
"logLevel": "info",
|
|
359
|
+
"onDiagnostic": lambda diagnostic: print(
|
|
360
|
+
diagnostic["level"],
|
|
361
|
+
diagnostic["code"],
|
|
362
|
+
diagnostic["message"],
|
|
363
|
+
),
|
|
364
|
+
})
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` dictionary. Optional `module`, `moduleName`, and `originalError` fields describe provenance. Evaluation metadata belongs in `details`.
|
|
368
|
+
|
|
369
|
+
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
## Events
|
|
373
|
+
|
|
374
|
+
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
|
|
375
|
+
|
|
376
|
+
### `datafile_set`
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
def handle_datafile_set(event):
|
|
380
|
+
revision = event["revision"]
|
|
381
|
+
previous_revision = event["previousRevision"]
|
|
382
|
+
revision_changed = event["revisionChanged"]
|
|
383
|
+
features = event["features"]
|
|
384
|
+
replaced = event["replaced"]
|
|
385
|
+
|
|
386
|
+
unsubscribe = f.on("datafile_set", handle_datafile_set)
|
|
387
|
+
unsubscribe()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The `features` list will contain keys of features that have either been added, updated, or removed compared to the previous datafile content.
|
|
391
|
+
|
|
392
|
+
### `context_set`
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
unsubscribe = f.on("context_set", lambda event: print(event["context"]))
|
|
396
|
+
unsubscribe()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### `sticky_set`
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
unsubscribe = f.on("sticky_set", lambda event: print(event["features"]))
|
|
403
|
+
unsubscribe()
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `error`
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
unsubscribe = f.on("error", lambda event: print(event["diagnostic"]["message"]))
|
|
410
|
+
unsubscribe()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The `error` event is emitted for diagnostics reported with `level` set to `error`.
|
|
414
|
+
|
|
415
|
+
## Evaluation details
|
|
416
|
+
|
|
417
|
+
Besides logging with debug level enabled, you can also get more details about how feature variations and variables are evaluated at runtime against a given context:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
# flag
|
|
421
|
+
evaluation = f.evaluate_flag(feature_key, context={})
|
|
422
|
+
|
|
423
|
+
# variation
|
|
424
|
+
evaluation = f.evaluate_variation(feature_key, context={})
|
|
425
|
+
|
|
426
|
+
# variable
|
|
427
|
+
evaluation = f.evaluate_variable(feature_key, variable_key, context={})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
The returned object will always contain the following properties:
|
|
431
|
+
|
|
432
|
+
- `featureKey`: the feature key
|
|
433
|
+
- `reason`: the reason how the value was evaluated
|
|
434
|
+
|
|
435
|
+
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
436
|
+
|
|
437
|
+
- `bucketValue`: the bucket value between 0 and 100,000
|
|
438
|
+
- `ruleKey`: the rule key
|
|
439
|
+
- `error`: the error object
|
|
440
|
+
- `enabled`: if feature itself is enabled or not
|
|
441
|
+
- `variation`: the variation object
|
|
442
|
+
- `variationValue`: the variation value
|
|
443
|
+
- `variableKey`: the variable key
|
|
444
|
+
- `variableValue`: the variable value
|
|
445
|
+
- `variableSchema`: the variable schema
|
|
446
|
+
- `variableOverrideIndex`: index of matched variable override when applicable
|
|
447
|
+
|
|
448
|
+
## Modules
|
|
449
|
+
|
|
450
|
+
Modules can intercept evaluation and participate in SDK lifecycle:
|
|
451
|
+
|
|
452
|
+
- `setup`
|
|
453
|
+
- `before`
|
|
454
|
+
- `bucketKey`
|
|
455
|
+
- `bucketValue`
|
|
456
|
+
- `after`
|
|
457
|
+
- `close`
|
|
458
|
+
|
|
459
|
+
### Defining a module
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
my_module = {
|
|
463
|
+
"name": "my-module",
|
|
464
|
+
"setup": lambda api: api["onDiagnostic"](lambda diagnostic: print(diagnostic)),
|
|
465
|
+
"before": lambda options: {**options, "context": {**options["context"], "country": "nl"}},
|
|
466
|
+
"bucketKey": lambda options: options["bucketKey"],
|
|
467
|
+
"bucketValue": lambda options: options["bucketValue"],
|
|
468
|
+
"after": lambda evaluation, options: evaluation,
|
|
469
|
+
"close": lambda: None,
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The module API passed to `setup` exposes `getRevision`, `onDiagnostic`, and `reportDiagnostic`.
|
|
474
|
+
|
|
475
|
+
If `setup` raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present.
|
|
476
|
+
|
|
477
|
+
### Registering modules
|
|
478
|
+
|
|
479
|
+
Modules can be registered at initialization or afterwards:
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
|
|
483
|
+
f = create_featurevisor({
|
|
484
|
+
"modules": [my_module],
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
remove_module = f.add_module(my_module)
|
|
488
|
+
remove_module()
|
|
489
|
+
|
|
490
|
+
f.remove_module("my-module")
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Child instance
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
child = f.spawn({"country": "de"})
|
|
497
|
+
child.is_enabled("my_feature")
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Close
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
f.close()
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## CLI usage
|
|
507
|
+
|
|
508
|
+
The Python package also exposes a CLI:
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
python -m featurevisor test
|
|
512
|
+
python -m featurevisor benchmark
|
|
513
|
+
python -m featurevisor assess-distribution
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
These commands are intended for use from inside a Featurevisor project and rely on `npx featurevisor` being available locally.
|
|
517
|
+
|
|
518
|
+
All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
|
|
519
|
+
|
|
520
|
+
### Test
|
|
521
|
+
|
|
522
|
+
Run Featurevisor test specs using the Python SDK:
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
python -m featurevisor test \
|
|
526
|
+
--projectDirectoryPath=/path/to/featurevisor-project
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Useful options:
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
python -m featurevisor test --keyPattern=foo
|
|
533
|
+
python -m featurevisor test --assertionPattern=variation
|
|
534
|
+
python -m featurevisor test --onlyFailures
|
|
535
|
+
python -m featurevisor test --showDatafile
|
|
536
|
+
python -m featurevisor test --verbose
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
The Python test runner builds base datafiles and Target datafiles with `npx featurevisor build --json`. Assertions containing `target` are evaluated against the matching Target datafile.
|
|
540
|
+
|
|
541
|
+
### Benchmark
|
|
542
|
+
|
|
543
|
+
Benchmark repeated Python SDK evaluations against a built datafile:
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
python -m featurevisor benchmark \
|
|
547
|
+
--projectDirectoryPath=/path/to/featurevisor-project \
|
|
548
|
+
--environment=production \
|
|
549
|
+
--feature=my_feature \
|
|
550
|
+
--context='{"userId":"123"}' \
|
|
551
|
+
--n=1000
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
For variation benchmarks:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
python -m featurevisor benchmark \
|
|
558
|
+
--projectDirectoryPath=/path/to/featurevisor-project \
|
|
559
|
+
--environment=production \
|
|
560
|
+
--feature=my_feature \
|
|
561
|
+
--variation \
|
|
562
|
+
--context='{"userId":"123"}'
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
For variable benchmarks:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
python -m featurevisor benchmark \
|
|
569
|
+
--projectDirectoryPath=/path/to/featurevisor-project \
|
|
570
|
+
--environment=production \
|
|
571
|
+
--feature=my_feature \
|
|
572
|
+
--variable=my_variable_key \
|
|
573
|
+
--context='{"userId":"123"}'
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Assess distribution
|
|
577
|
+
|
|
578
|
+
Inspect enabled/disabled and variation distribution over repeated evaluations:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
python -m featurevisor assess-distribution \
|
|
582
|
+
--projectDirectoryPath=/path/to/featurevisor-project \
|
|
583
|
+
--environment=production \
|
|
584
|
+
--feature=my_feature \
|
|
585
|
+
--context='{"country":"nl"}' \
|
|
586
|
+
--n=1000
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
You can also populate UUID-based context keys per iteration:
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
python -m featurevisor assess-distribution \
|
|
593
|
+
--projectDirectoryPath=/path/to/featurevisor-project \
|
|
594
|
+
--environment=production \
|
|
595
|
+
--feature=my_feature \
|
|
596
|
+
--populateUuid=userId \
|
|
597
|
+
--populateUuid=deviceId
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
<!-- FEATUREVISOR_DOCS_END -->
|
|
601
|
+
|
|
602
|
+
## Development
|
|
603
|
+
|
|
604
|
+
This repository assumes:
|
|
605
|
+
|
|
606
|
+
- Python 3.10+
|
|
607
|
+
- Node.js with `npx`
|
|
608
|
+
- Access to a Featurevisor project for CLI and tester integration
|
|
609
|
+
|
|
610
|
+
Run the local test suite:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
make test
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Run the example project integration directly:
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
PYTHONPATH=src python3 -m featurevisor test \
|
|
620
|
+
--projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 \
|
|
621
|
+
--onlyFailures
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Or use:
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
make test-example-1
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## Releasing
|
|
631
|
+
|
|
632
|
+
- Update version in pyproject.toml
|
|
633
|
+
- Push commit to main branch
|
|
634
|
+
- Wait for CI to complete
|
|
635
|
+
- Tag the release with the version number `vX.X.X`
|
|
636
|
+
- This will trigger a new release to PyPI
|
|
637
|
+
|
|
638
|
+
## License
|
|
639
|
+
|
|
640
|
+
MIT © [Fahad Heylaal](https://fahad19.com)
|