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.
Files changed (45) hide show
  1. featurevisor-1.0.0/PKG-INFO +640 -0
  2. featurevisor-1.0.0/README.md +623 -0
  3. {featurevisor-0.2.0 → featurevisor-1.0.0}/pyproject.toml +1 -1
  4. featurevisor-1.0.0/src/featurevisor/__init__.py +10 -0
  5. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/child.py +6 -4
  6. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/cli.py +8 -6
  7. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/datafile_reader.py +17 -6
  8. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/evaluate.py +16 -20
  9. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/events.py +3 -3
  10. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/helpers.py +10 -8
  11. featurevisor-1.0.0/src/featurevisor/instance.py +371 -0
  12. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/logger.py +7 -8
  13. featurevisor-1.0.0/src/featurevisor/modules.py +184 -0
  14. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/project.py +17 -20
  15. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/tester.py +87 -51
  16. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/types.py +17 -7
  17. featurevisor-1.0.0/src/featurevisor.egg-info/PKG-INFO +640 -0
  18. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/SOURCES.txt +2 -1
  19. featurevisor-1.0.0/tests/test_cli.py +58 -0
  20. {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_conditions_parity.py +8 -3
  21. featurevisor-1.0.0/tests/test_conformance.py +43 -0
  22. {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_datafile_reader_parity.py +14 -5
  23. featurevisor-1.0.0/tests/test_helpers_parity.py +52 -0
  24. {featurevisor-0.2.0 → featurevisor-1.0.0}/tests/test_instance_parity.py +34 -18
  25. featurevisor-1.0.0/tests/test_sdk.py +408 -0
  26. featurevisor-0.2.0/PKG-INFO +0 -374
  27. featurevisor-0.2.0/README.md +0 -357
  28. featurevisor-0.2.0/src/featurevisor/__init__.py +0 -52
  29. featurevisor-0.2.0/src/featurevisor/hooks.py +0 -31
  30. featurevisor-0.2.0/src/featurevisor/instance.py +0 -200
  31. featurevisor-0.2.0/src/featurevisor.egg-info/PKG-INFO +0 -374
  32. featurevisor-0.2.0/tests/test_cli.py +0 -35
  33. featurevisor-0.2.0/tests/test_helpers_parity.py +0 -47
  34. featurevisor-0.2.0/tests/test_sdk.py +0 -171
  35. {featurevisor-0.2.0 → featurevisor-1.0.0}/LICENSE +0 -0
  36. {featurevisor-0.2.0 → featurevisor-1.0.0}/setup.cfg +0 -0
  37. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/__main__.py +0 -0
  38. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/bucketer.py +0 -0
  39. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/compare_versions.py +0 -0
  40. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/conditions.py +0 -0
  41. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/emitter.py +0 -0
  42. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor/murmurhash.py +0 -0
  43. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/dependency_links.txt +0 -0
  44. {featurevisor-0.2.0 → featurevisor-1.0.0}/src/featurevisor.egg-info/entry_points.txt +0 -0
  45. {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)