This release is a pre-release and may not be stable for production use.
rust_decider
Rust implementation of bucketing, exposures, targeting, overrides, and dynamic config logic.
Usage
class Decider
A class with these APIs:
-
choose( feature_name: str, context: Mapping[str, JsonValue] ) -> Decision -
choose_all( context: Mapping[str, JsonValue], bucketing_field_filter: Optional[str] = None ) -> Dict[str, Decision]
(dynamic configurations)
-
get_bool( feature_name: str, context: Mapping[str, JsonValue], ) -> bool -
get_int( feature_name: str, context: Mapping[str, JsonValue], ) -> int -
get_float( feature_name: str, context: Mapping[str, JsonValue], ) -> float -
get_string( feature_name: str, context: Mapping[str, JsonValue], ) -> str -
get_map( feature_name: str, context: Mapping[str, JsonValue], ) -> Dict[str, Any] -
all_values( context: Mapping[str, JsonValue], ) -> Dict[str, Any]
misc:
-
get_feature( feature_name: str, ) -> Feature
choose() examples:
from rust_decider import Decider
from rust_decider import DeciderException
from rust_decider import FeatureNotFoundException
from rust_decider import DeciderInitException
from rust_decider import PartialLoadException
from rust_decider import ValueTypeMismatchException
# initialize Decider instance
try:
decider = Decider("../cfg.json")
except PartialLoadException as e:
# log errors of misconfigured features
print(f"{e.args[0]}: {e.args[2]}")
# use partially initialized Decider instance
decider = e.args[1]
except DeciderInitException as e:
print(e)
# get a Decision for a feature via choose()
try:
decision = decider.choose(feature_name="exp_1", context={"user_id": "3", "app_name": "ios"})
except DeciderException as e:
print(e)
assert dict(decision) == {
"variant": "variant_0",
"value": None,
"feature_id": 3246,
"feature_name": "exp_1",
"feature_version": 2,
"events": [
"0::::3246::::exp_1::::2::::variant_0::::3::::user_id::::37173982::::2147483648"
],
"full_events": [...]
}
# `user_id` targeting not satisfied so "variant" is `None` in the returned Decision
try:
decision = decider.choose(feature_name="exp_1", context={"user_id": "1"})
except DeciderException as e:
print(e)
assert dict(decision) == {
"variant": None,
"value": None,
"feature_id": 3246,
"feature_name": "exp_1",
"feature_version": 2,
"events": [],
"full_events": []
}
# handle "feature not found" exception
# (`FeatureNotFoundException` is a subclass of `DeciderException`)
try:
decision = decider.choose(feature_name="not_here", context={"user_id": "1"})
except FeatureNotFoundException as e:
print("handle feature not found exception:")
print(e)
except DeciderException as e:
print(e)
choose_all() examples:
# `decider` initialized same as above
decisions = decider.choose_all(context={"user_id": "3", "app_name": "ios"}, bucketing_field_filter="user_id")
assert dict(decisions["exp_67"]) == {
"variant": "variant_0",
"value": None,
"feature_id": 3125,
"feature_name": "exp_67",
"feature_version": 4,
"events": [
"0::::3125::::exp_67::::4::::variant_0::::3::::user_id::::37173982::::2147483648"
],
"full_events": [...]
}
class Exposer
Used to enable emitting expose v2 events in choose() API via expose or expose_holdout boolean params, e.g.:
from rust_decider import Decider
from rust_decider import Exposer
from baseplate.lib.events import EventQueue
serializer = lambda s: s.encode("utf-8")
eq = EventQueue(name="v2", event_serializer=serializer)
exposer = Exposer(expose_fn=eq.put)
decider_w_exposer = Decider("experiments.json", exposer)
# choose w/ expose
choice = decider_w_exposer.choose(
feature_name="exp_name",
context={
"user_id": "t2_abc",
"user_is_employee": True,
"other_info": { "arbitrary_field": "some_val" }
},
expose=True
)
# verify events were present to be emitted
len(choice.full_events)
dict(choice.full_events[0])
# `json_str` field contains json thrift schema encoded v2 event, which is passed to `expose_fn` of `Exposer` instance passed into `Decider``:
# {'decision_kind': 'FracAvail', 'exposure_key': 'decider_py_exposer_test:4:enabled:t2_88854', 'json_str': '{"1":{"str":"experiment"},"2":{"str":"expose"},"3":{"str":"user_id"},"5":{"i64":1697561871796},"6":{"str":"a6143b05-1e46-4c58-b6e8-4452c92dc1e7"},"8":{"str":"924c5785-b13f-4bf9-81e5-13ab6c89e794"},"107":{"rec":{"2":{"str":"ios"},"4":{"i32":0},"6":{"str":"us_en"}}},"108":{"rec":{"2":{"str":"d42b90e7-aae3-4ac5-b137-042de165ecf6"}}},"109":{"rec":{"17":{"str":"www.reddit.com"}}},"112":{"rec":{"1":{"str":"t2_88854"},"3":{"tf":1},"4":{"i64":1648859753},"16":{"tf":1}}},"114":{"rec":{"1":{"str":"t5_asdf"}}},"129":{"rec":{"1":{"i64":9787},"2":{"str":"decider_py_exposer_test"},"3":{"str":"redacted@reddit.com"},"4":{"str":"enabled"},"5":{"i64":1697561268},"6":{"i64":1698770868},"7":{"str":"user_id"},"8":{"str":"4"},"9":{"str":"t2_88854"},"10":{"tf":0}}},"500":{"rec":{"1":{"str":"UA"}}}}'}
If only holdouts are intended to be exposed, this can be accomplished via params:
choice = decider_w_exposer.choose(
feature_name="exp_name",
context={
...
},
expose=False,
expose_holdout=True
)
Dynamic Configurations + misc. examples:
# `decider` initialized same as above
try:
dc_bool = decider.get_bool("dc_bool", context={})
dc_int = decider.get_int("dc_int", context={})
dc_float = decider.get_float("dc_float", context={})
dc_string = decider.get_string("dc_string", context={})
dc_map = decider.get_map("dc_map", context={})
feature = decider.get_feature("dc_map")
except FeatureNotFoundException as e:
print("handle feature not found exception:")
print(e)
except ValueTypeMismatchException as e:
print("handle type mismatch:")
print(e)
except DeciderException as e:
print(e)
assert dc_bool == True
assert dc_int == 99
assert dc_float == 3.0
assert dc_string == "some_string"
assert dc_map == {
"v": {
"nested_map": {
"w": False,
"x": 1,
"y": "some_string",
"z": 3.0
}
},
"w": False,
"x": 1,
"y": "some_string",
"z": 3.0
}
assert dict(feature) == {
"id": 3393,
"name": "dc_bool",
"version": 2,
"bucket_val": '',
"start_ts": 0,
"stop_ts": 0,
"emit_event": False
}
Dynamic Configuration all_values() example:
# `decider` initialized same as above
decisions = decider.all_values(context={})
assert decisions["dc_int"] == 99
python bindings used in Decider class
import rust_decider
# Init decider
decider = rust_decider.init("darkmode overrides targeting holdout mutex_group fractional_availability value", "../cfg.json")
# Bucketing needs a context
ctx = rust_decider.make_ctx({"user_id": "7"})
# Get a decision
choice = decider.choose("exp_1", ctx)
assert choice.err() is None # check for errors
choice.decision() # get the variant
# Get a dynamic config value
dc = decider.get_map("dc_map", ctx) # fetch a map DC
assert dc.err() is None # check for errors
dc.val() # get the actual map itself
Development
Updating package with latest src/lib.rs changes
# In a virtualenv, python >= 3.7
$ cd decider-py
$ pip install -r requirements-dev.txt
$ maturin develop
Running tests
$ pytest decider-py/test/
Publishing
Use conventional commit format in PR titles to trigger releases via release-please task in drone pipeline.
chore:&build:commits don't trigger releases (used for changes like updating config files or documentation)fix:bumps the patch versionfeat:bumps the minor versionfeat!:bumps the major version
Cross-Compilation
We're using Zig for cross-compilation which is reflected in the switch from the "FLAVOR" approach to "TARGET". Cross-compilation is useful when we want to build code on one type of machine (like our CI server), but have it run on a different type of machine (like a server or user's machine with a different architecture).
To build wheels for multiple platforms more effectively, we use "TARGET" variable in the .drone.yml. This includes platforms like "linux-aarch64" and "linux-musl-x86_64".
Formatting / Linting
$ cargo fmt --manifest-path decider-py/test/Cargo.toml
$ cargo clippy --manifest-path decider-py/test/Cargo.toml
Metadata
Release files for reddit-decider 1.19.0b3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| reddit_decider-1.19.0b3-cp37-abi3-musllinux_1_2_x86_64.whl | CPython 3.7 | abi3 | Linux musl 1.2+ x86-64 | Details |
| reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.7 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.7 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| reddit_decider-1.19.0b3-cp37-abi3-macosx_11_0_arm64.whl | CPython 3.7 | abi3 | macOS 11.0+ ARM64 | Details |
| reddit_decider-1.19.0b3-cp37-abi3-macosx_10_7_x86_64.whl | CPython 3.7 | abi3 | macOS 10.7+ x86-64 | Details |
Total release size: 4.3 MB
Release files / reddit_decider-1.19.0b3-cp37-abi3-musllinux_1_2_x86_64.whl
| Download URL | reddit_decider-1.19.0b3-cp37-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 897.6 kB |
| Tags | CPython 3.7 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1ea032b7b075bfbc2cc1fc217a876fc3a10a9d48fd339af905c37b334d2a6b94
|
|
BLAKE2b-256 checksum How to use checksums |
6730ced6796980b68564a2eab58ecd0a1f9456f98259ae2d848213c0158eebda
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
|
Release files / reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 898.7 kB |
| Tags | CPython 3.7 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
877a483cbaca078f2b038dd0c505d9a2e7d18b1f4e4ebdfb8f20748353716bb7
|
|
BLAKE2b-256 checksum How to use checksums |
fe62635f9e3939619d9dc1fa7c9e3f2eb2662a1f059255aa25a6da91d614c475
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
|
Release files / reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | reddit_decider-1.19.0b3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 868.8 kB |
| Tags | CPython 3.7 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
76cafb9e490fa948f0a7374778b9386c8a7d818df1b1015ba3a1091a4fe907c5
|
|
BLAKE2b-256 checksum How to use checksums |
41cc4b5a6cf7e6ab0c63f051f602679bdbc277a68c2d706cc68de258bf063005
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
|
Release files / reddit_decider-1.19.0b3-cp37-abi3-macosx_11_0_arm64.whl
| Download URL | reddit_decider-1.19.0b3-cp37-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 776.1 kB |
| Tags | CPython 3.7 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8d926c848e918757cf341ed0fc5710c2264976a46b0c728f954de98ce0f2f93c
|
|
BLAKE2b-256 checksum How to use checksums |
457f7aebe6fe2d077b27165adb6821c776d44c2361348f4b8b0cccdfbe551660
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
|
Release files / reddit_decider-1.19.0b3-cp37-abi3-macosx_10_7_x86_64.whl
| Download URL | reddit_decider-1.19.0b3-cp37-abi3-macosx_10_7_x86_64.whl |
|---|---|
| Size | 813.2 kB |
| Tags | CPython 3.7 abi3 macOS 10.7+ x86-64 |
|
SHA-256 checksum How to use checksums |
e0c0925835d70a6466a2f8cc6dc061939f4c37c10011931d92241344a4313128
|
|
BLAKE2b-256 checksum How to use checksums |
4973e58d0fe4906745a766f89c1e585844e0766cf8bbb6c9c3bce322b451af74
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8
|