GitHub - featurevisor/featurevisor-python at 50a8ddd18055823515545f018c95d351507a1d11 · GitHub
Skip to content

About

Python SDK for Featurevisor

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Featurevisor Python SDK

This repository ports the latest Featurevisor JavaScript SDK to Python.

The package name is featurevisor, and it targets Python 3.10+.

This SDK is compatible with Featurevisor v3 projects and v2 datafiles.

Table of contents

Installation

pip install featurevisor

Initialization

Initialize the SDK with Featurevisor datafile content:

from urllib.request import urlopen
import json

from featurevisor import create_instance

datafile_url = "https://cdn.yoursite.com/datafile.json"

with urlopen(datafile_url) as response:
    datafile_content = json.load(response)

f = create_instance({
    "datafile": datafile_content,
})

Evaluation types

We can evaluate 3 types of values against a particular feature:

  • Flag (bool): whether the feature is enabled or not
  • Variation (string): the variation of the feature (if any)
  • Variables: variable values of the feature (if any)

Context

Context is a plain dictionary of attribute values used during evaluation:

context = {
    "userId": "123",
    "country": "nl",
}

Setting initial context

You can provide context at initialization:

f = create_instance({
    "context": {
        "deviceId": "123",
        "country": "nl",
    },
})

Setting after initialization

You can merge more context later:

f.set_context({
    "userId": "234",
})

Replacing existing context

Or replace the existing context:

f.set_context(
    {
        "deviceId": "123",
        "userId": "234",
        "country": "nl",
        "browser": "chrome",
    },
    True,
)

Manually passing context

You can also pass additional per-evaluation context:

is_enabled = f.is_enabled("my_feature", {"country": "nl"})
variation = f.get_variation("my_feature", {"country": "nl"})
variable_value = f.get_variable("my_feature", "my_variable", {"country": "nl"})

Check if enabled

if f.is_enabled("my_feature"):
    pass

Getting variation

variation = f.get_variation("my_feature")

if variation == "treatment":
    pass

Getting variables

bg_color = f.get_variable("my_feature", "bgColor")

Type specific methods

Typed convenience methods are also available:

f.get_variable_boolean(feature_key, variable_key, context={})
f.get_variable_string(feature_key, variable_key, context={})
f.get_variable_integer(feature_key, variable_key, context={})
f.get_variable_double(feature_key, variable_key, context={})
f.get_variable_array(feature_key, variable_key, context={})
f.get_variable_object(feature_key, variable_key, context={})
f.get_variable_json(feature_key, variable_key, context={})

Getting all evaluations

all_evaluations = f.get_all_evaluations()

Sticky

Initialize with sticky

You can pin feature evaluations with sticky values:

f = create_instance({
    "sticky": {
        "myFeatureKey": {
            "enabled": True,
            "variation": "treatment",
            "variables": {
                "myVariableKey": "myVariableValue",
            },
        }
    }
})

Set sticky afterwards

Or update them later:

f.set_sticky({
    "myFeatureKey": {
        "enabled": False,
    }
})

Setting datafile

You may initialize the SDK without passing datafile, and set it later on. The SDK accepts either parsed JSON content or a JSON string:

f.set_datafile(datafile_content)
f.set_datafile(json.dumps(datafile_content))

Merging by default

By default, set_datafile(datafile) merges incoming content into the SDK's current datafile:

  • top-level metadata such as schemaVersion, revision, and featurevisorVersion comes from the incoming datafile
  • segments are merged, with incoming entries overriding existing ones
  • features are merged, with incoming entries overriding existing ones

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 possible.

Replacing

To fully replace the stored datafile, pass True as the second argument:

f.set_datafile(datafile_content, True)

Loading datafiles on demand

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.

This pairs well with 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:

from urllib.request import urlopen
import json

from featurevisor import create_instance

f = create_instance({})

def load_datafile(target):
    url = f"https://cdn.yoursite.com/production/featurevisor-{target}.json"
    with urlopen(url) as response:
        datafile = json.load(response)

    # merges into whatever was loaded before
    f.set_datafile(datafile)

load_datafile("products")

# later, when the user reaches checkout
load_datafile("checkout")

Updating datafile

You can set the datafile as many times as you want in your application, which will emit a datafile_set event that you can listen and react to accordingly.

Interval-based update

Here's a minimal interval-style example:

import json
import threading
from urllib.request import urlopen

def update_datafile():
    with urlopen(datafile_url) as response:
        f.set_datafile(json.load(response))

    threading.Timer(5 * 60, update_datafile).start()

update_datafile()

Logging

By default, Featurevisor SDKs report diagnostics/logs for info level and above.

Levels

Supported log levels:

  • fatal
  • error
  • warn
  • info
  • debug

Customizing levels

from featurevisor import create_instance

f = create_instance({
    "datafile": datafile_content,
    "logLevel": "debug",
})

You can also set the log level from the SDK instance afterwards:

f.set_log_level("debug")

Handler

You can also provide a custom logger handler via create_logger.

Diagnostics

Diagnostics provide structured SDK and module events for observability:

f = create_instance({
    "onDiagnostic": lambda diagnostic: print(diagnostic),
})

If onDiagnostic is not provided, diagnostics are sent to the SDK logger.

Events

Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.

datafile_set

def handle_datafile_set(event):
    revision = event["revision"]
    previous_revision = event["previousRevision"]
    revision_changed = event["revisionChanged"]
    features = event["features"]
    replaced = event["replaced"]

unsubscribe = f.on("datafile_set", handle_datafile_set)
unsubscribe()

The features list will contain keys of features that have either been added, updated, or removed compared to the previous datafile content.

context_set

unsubscribe = f.on("context_set", lambda event: print(event["context"]))
unsubscribe()

sticky_set

unsubscribe = f.on("sticky_set", lambda event: print(event["features"]))
unsubscribe()

error

unsubscribe = f.on("error", lambda diagnostic: print(diagnostic["message"]))
unsubscribe()

The error event is emitted for diagnostics reported with level set to error or fatal.

Evaluation details

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:

# flag
evaluation = f.evaluate_flag(feature_key, context={})

# variation
evaluation = f.evaluate_variation(feature_key, context={})

# variable
evaluation = f.evaluate_variable(feature_key, variable_key, context={})

The returned object will always contain the following properties:

  • featureKey: the feature key
  • reason: the reason how the value was evaluated

And optionally these properties depending on whether you are evaluating a feature variation or a variable:

  • bucketValue: the bucket value between 0 and 100,000
  • ruleKey: the rule key
  • error: the error object
  • enabled: if feature itself is enabled or not
  • variation: the variation object
  • variationValue: the variation value
  • variableKey: the variable key
  • variableValue: the variable value
  • variableSchema: the variable schema
  • variableOverrideIndex: index of matched variable override when applicable

Modules

Modules can intercept evaluation and participate in SDK lifecycle:

  • setup
  • before
  • bucketKey
  • bucketValue
  • after
  • close

Defining a module

my_module = {
    "name": "my-module",
    "setup": lambda api: api["onDiagnostic"](lambda diagnostic: print(diagnostic)),
    "before": lambda options: {**options, "context": {**options["context"], "country": "nl"}},
    "bucketKey": lambda options: options["bucketKey"],
    "bucketValue": lambda options: options["bucketValue"],
    "after": lambda evaluation, options: evaluation,
    "close": lambda: None,
}

The module API passed to setup exposes getRevision, onDiagnostic, and reportDiagnostic.

Registering modules

Modules can be registered at initialization or afterwards:

f = create_instance({
    "modules": [my_module],
})

remove_module = f.add_module(my_module)
remove_module()

f.remove_module("my-module")

Child instance

child = f.spawn({"country": "de"})
child.is_enabled("my_feature")

Close

f.close()

CLI usage

The Python package also exposes a CLI:

python -m featurevisor test
python -m featurevisor benchmark
python -m featurevisor assess-distribution

These commands are intended for use from inside a Featurevisor project and rely on npx featurevisor being available locally.

Test

Run Featurevisor test specs using the Python SDK:

python -m featurevisor test \
  --projectDirectoryPath=/path/to/featurevisor-project

Useful options:

python -m featurevisor test --keyPattern=foo
python -m featurevisor test --assertionPattern=variation
python -m featurevisor test --onlyFailures
python -m featurevisor test --showDatafile
python -m featurevisor test --verbose

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.

Benchmark

Benchmark repeated Python SDK evaluations against a built datafile:

python -m featurevisor benchmark \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --context='{"userId":"123"}' \
  --n=1000

For variation benchmarks:

python -m featurevisor benchmark \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --variation \
  --context='{"userId":"123"}'

For variable benchmarks:

python -m featurevisor benchmark \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --variable=my_variable_key \
  --context='{"userId":"123"}'

Assess distribution

Inspect enabled/disabled and variation distribution over repeated evaluations:

python -m featurevisor assess-distribution \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --context='{"country":"nl"}' \
  --n=1000

You can also populate UUID-based context keys per iteration:

python -m featurevisor assess-distribution \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --populateUuid=userId \
  --populateUuid=deviceId

Development

This repository assumes:

  • Python 3.10+
  • Node.js with npx
  • Access to a Featurevisor project for CLI and tester integration

Run the local test suite:

make test

Run the example project integration directly:

PYTHONPATH=src python3 -m featurevisor test \
  --projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 \
  --onlyFailures

Or use:

make test-example-1

Releasing

  • Update version in pyproject.toml
  • Push commit to main branch
  • Wait for CI to complete
  • Tag the release with the version number vX.X.X
  • This will trigger a new release to PyPI

License

MIT © Fahad Heylaal

About

Python SDK for Featurevisor

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages