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.12.0.tar.gz (71.3 kB view details)

Uploaded Source

Built Distributions

reddit_decider-1.12.0-cp37-abi3-musllinux_1_2_x86_64.whl (463.8 kB view details)

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

reddit_decider-1.12.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (424.4 kB view details)

Uploaded CPython 3.7+ manylinux: glibc 2.17+ ARM64

reddit_decider-1.12.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (429.9 kB view details)

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

reddit_decider-1.12.0-cp37-abi3-macosx_11_0_arm64.whl (374.7 kB view details)

Uploaded CPython 3.7+ macOS 11.0+ ARM64

reddit_decider-1.12.0-cp37-abi3-macosx_10_7_x86_64.whl (384.4 kB view details)

Uploaded CPython 3.7+ macOS 10.7+ x86-64

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

Hashes for reddit_decider-1.12.0.tar.gz
Algorithm Hash digest
SHA256 5bfe3581d0ce3e05b732c3e2f4d31fda083643ba208d8fd1e0ad9c240e99d936
MD5 930e0947825375eb78f2aff93b439be4
BLAKE2b-256 dde260f80b02c8bb299441db587b722d4ba5d79c0e952023a83cc237b03182f9

See more details on using hashes here.

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

Hashes for reddit_decider-1.12.0-cp37-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 e25ad17c3b068d3fb4820bba29e38b3bca3955346338c4a94192fe9fb1220087
MD5 e9d120e2f7e94f224b2362fda8136250
BLAKE2b-256 75a9ef398928a760fc24913f44a426ed5ba7f852863be189913c1e056600fe43

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for reddit_decider-1.12.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d1266b10c6edcbf846f27fd541388269e8213ce8c8e1dfa2f75c33a7eb60521a
MD5 71c194ba3e751439acbcf3e9e1047607
BLAKE2b-256 fce7cfe757a811286ee1301a3ef150898faa5e9c36c068dce5ed15b531245b9c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for reddit_decider-1.12.0-cp37-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 b233439e1bac27c950d364f3d158ca4f9494b6728cbe669ecec6fcbfadf27751
MD5 18e47501577410ea3e78c44bdcb543c9
BLAKE2b-256 dbbb4e631a743f1e829e8ef7260a1cac5ac7b0e7c075005da028da2475662663

See more details on using hashes here.

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

Hashes for reddit_decider-1.12.0-cp37-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 cefb3e52064b56d1ea0bc5cc6fc2204a3b18bc0688f94b84c81f6bcb2e6a0436
MD5 c8702f2221c432db0e4e83c99aca51ff
BLAKE2b-256 bbc441bb1a00bd055ed1470f7ef6e051f921e54debf2777e61efa72c3eee637b

See more details on using hashes here.

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

Hashes for reddit_decider-1.12.0-cp37-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 93909a79ed1c5f2d0d733916a85f47f9a4e9dc30d3fed10677a051c723494379
MD5 4427bd0cf17fdd7a7242b1f49ea5d35d
BLAKE2b-256 e93b9ad9298a58c863e919154c4355a5a466e2f75235b32d4b42f8c7297c6d02

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