Skip to main content

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 version
  • feat: bumps the minor version
  • feat!: 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

reddit_decider-1.13.0.tar.gz (70.9 kB view details)

Uploaded Source

Built Distributions

reddit_decider-1.13.0-cp37-abi3-musllinux_1_2_x86_64.whl (464.0 kB view details)

Uploaded CPython 3.7+ musllinux: musl 1.2+ x86-64

reddit_decider-1.13.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (425.3 kB view details)

Uploaded CPython 3.7+ manylinux: glibc 2.17+ ARM64

reddit_decider-1.13.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (431.0 kB view details)

Uploaded CPython 3.7+ manylinux: glibc 2.12+ x86-64

reddit_decider-1.13.0-cp37-abi3-macosx_11_0_arm64.whl (375.2 kB view details)

Uploaded CPython 3.7+ macOS 11.0+ ARM64

reddit_decider-1.13.0-cp37-abi3-macosx_10_7_x86_64.whl (384.8 kB view details)

Uploaded CPython 3.7+ macOS 10.7+ x86-64

File details

Details for the file reddit_decider-1.13.0.tar.gz.

File metadata

  • Download URL: reddit_decider-1.13.0.tar.gz
  • Upload date:
  • Size: 70.9 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

Hashes for reddit_decider-1.13.0.tar.gz
Algorithm Hash digest
SHA256 4338f1df21a01f70e4ad08a0ed3e88e5dc4e235c3e08b9da0d3a3ddf68440d8a
MD5 d3739f4a2be4f8b5bb223b2080dc00eb
BLAKE2b-256 8a0e04522e2c9568911f7d1201971b160f6ff27bfdefe1aa66c44f01ffa7dbd4

See more details on using hashes here.

File details

Details for the file reddit_decider-1.13.0-cp37-abi3-musllinux_1_2_x86_64.whl.

File metadata

  • Download URL: reddit_decider-1.13.0-cp37-abi3-musllinux_1_2_x86_64.whl
  • Upload date:
  • Size: 464.0 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

Hashes for reddit_decider-1.13.0-cp37-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 af4c86e89c3fe9334d067d2a5358715e018b1d44ce330cd3ea762a98d0c572de
MD5 4e0ae020bf8a67a5b0e710ca6b7e4a80
BLAKE2b-256 cfe64fa3d9a772c9604b348946171d780364d64edf371a15dea44fb75c2a2983

See more details on using hashes here.

File details

Details for the file reddit_decider-1.13.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for reddit_decider-1.13.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 979b893d6c294278e25b9cfa68cd1f34be1d6bb016e354c577a9214b22990ccb
MD5 4a48b301fe4150c9c4dcb9cb68f26803
BLAKE2b-256 2144ba23ac107b6b8dca1dd37d136a5f9c54ba864f666f636e3e2f18262fe141

See more details on using hashes here.

File details

Details for the file reddit_decider-1.13.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for reddit_decider-1.13.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 ce056c3cb0a517504655b78b98dc68fc4b35610a8ced88efe56eb9034526d523
MD5 dca437ed00d5b59cdcd77e229c5b5742
BLAKE2b-256 46bd852021fb2beb8c4e6d6257c808d25a411e7f74d4f89e4d01404ab294d36d

See more details on using hashes here.

File details

Details for the file reddit_decider-1.13.0-cp37-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: reddit_decider-1.13.0-cp37-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 375.2 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

Hashes for reddit_decider-1.13.0-cp37-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5c7b56c8a5feaca9bc08c62c6692a13e4c34940fd47c24cba57104c3a79898e9
MD5 28062a61419b0fb02e820d2ee7d8f8c6
BLAKE2b-256 b7f3e16e886528de57c7b0aff52c4104d885c6cdb51433fcbda440f57c8d182f

See more details on using hashes here.

File details

Details for the file reddit_decider-1.13.0-cp37-abi3-macosx_10_7_x86_64.whl.

File metadata

  • Download URL: reddit_decider-1.13.0-cp37-abi3-macosx_10_7_x86_64.whl
  • Upload date:
  • Size: 384.8 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

Hashes for reddit_decider-1.13.0-cp37-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 c3d33990d5b8f7037f4aeb4e64e342ea63e91f988b8fa33ebcc636a3f91af9bd
MD5 4c71659b50fbf3f462968f5972d8d385
BLAKE2b-256 623f3e73e36609fbb3b59f07cd93e8fff01c773c427261a9bf6e977c02ce5f97

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page