No project description provided
Project description
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 = Decider("experiments.json", exposer)
# choose w/ expose
choice = decider_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_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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
File details
Details for the file reddit_decider-1.12.0.tar.gz
.
File metadata
- Download URL: reddit_decider-1.12.0.tar.gz
- Upload date:
- Size: 71.3 kB
- Tags: Source
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5bfe3581d0ce3e05b732c3e2f4d31fda083643ba208d8fd1e0ad9c240e99d936 |
|
MD5 | 930e0947825375eb78f2aff93b439be4 |
|
BLAKE2b-256 | dde260f80b02c8bb299441db587b722d4ba5d79c0e952023a83cc237b03182f9 |
File details
Details for the file reddit_decider-1.12.0-cp37-abi3-musllinux_1_2_x86_64.whl
.
File metadata
- Download URL: reddit_decider-1.12.0-cp37-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 463.8 kB
- Tags: CPython 3.7+, musllinux: musl 1.2+ x86-64
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | e25ad17c3b068d3fb4820bba29e38b3bca3955346338c4a94192fe9fb1220087 |
|
MD5 | e9d120e2f7e94f224b2362fda8136250 |
|
BLAKE2b-256 | 75a9ef398928a760fc24913f44a426ed5ba7f852863be189913c1e056600fe43 |
File details
Details for the file reddit_decider-1.12.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
.
File metadata
- Download URL: reddit_decider-1.12.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 424.4 kB
- Tags: CPython 3.7+, manylinux: glibc 2.17+ ARM64
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | d1266b10c6edcbf846f27fd541388269e8213ce8c8e1dfa2f75c33a7eb60521a |
|
MD5 | 71c194ba3e751439acbcf3e9e1047607 |
|
BLAKE2b-256 | fce7cfe757a811286ee1301a3ef150898faa5e9c36c068dce5ed15b531245b9c |
File details
Details for the file reddit_decider-1.12.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
.
File metadata
- Download URL: reddit_decider-1.12.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
- Upload date:
- Size: 429.9 kB
- Tags: CPython 3.7+, manylinux: glibc 2.12+ x86-64
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | b233439e1bac27c950d364f3d158ca4f9494b6728cbe669ecec6fcbfadf27751 |
|
MD5 | 18e47501577410ea3e78c44bdcb543c9 |
|
BLAKE2b-256 | dbbb4e631a743f1e829e8ef7260a1cac5ac7b0e7c075005da028da2475662663 |
File details
Details for the file reddit_decider-1.12.0-cp37-abi3-macosx_11_0_arm64.whl
.
File metadata
- Download URL: reddit_decider-1.12.0-cp37-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 374.7 kB
- Tags: CPython 3.7+, macOS 11.0+ ARM64
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | cefb3e52064b56d1ea0bc5cc6fc2204a3b18bc0688f94b84c81f6bcb2e6a0436 |
|
MD5 | c8702f2221c432db0e4e83c99aca51ff |
|
BLAKE2b-256 | bbc441bb1a00bd055ed1470f7ef6e051f921e54debf2777e61efa72c3eee637b |
File details
Details for the file reddit_decider-1.12.0-cp37-abi3-macosx_10_7_x86_64.whl
.
File metadata
- Download URL: reddit_decider-1.12.0-cp37-abi3-macosx_10_7_x86_64.whl
- Upload date:
- Size: 384.4 kB
- Tags: CPython 3.7+, macOS 10.7+ x86-64
- Uploaded using 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
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 93909a79ed1c5f2d0d733916a85f47f9a4e9dc30d3fed10677a051c723494379 |
|
MD5 | 4427bd0cf17fdd7a7242b1f49ea5d35d |
|
BLAKE2b-256 | e93b9ad9298a58c863e919154c4355a5a466e2f75235b32d4b42f8c7297c6d02 |