Skip to main content
Pre-release

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

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)

Table of built distributions (wheels) for reddit-decider 1.19.0b3
File
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

Release history Release notifications | RSS feed

This release

1.19.0b3 This release

5 release files

1.18.2

5 release files

1.18.1

5 release files

1.16.4

5 release files

1.16.3

5 release files

1.16.2

5 release files

1.14.0

6 release files

1.13.0

6 release files

1.12.0

6 release files

1.11.0

6 release files

1.7.0

6 release files

1.6.0

6 release files

1.5.8

6 release files

1.5.7

6 release files

1.5.6

5 release files

1.5.5

5 release files

1.5.4

5 release files

1.5.3

5 release files

1.5.2

5 release files

1.5.1

5 release files

1.5.0

5 release files

1.4.2

5 release files

1.4.1

5 release files

1.4.0

5 release files

1.3.1

5 release files

1.3.0

5 release files

1.2.31

5 release files

1.2.30

5 release files

1.2.29

5 release files

1.2.24

5 release files

1.2.20

5 release files

1.2.19

5 release files

1.2.18

5 release files

1.2.17

5 release files

1.2.16

5 release files

1.2.15

5 release files

1.2.9

5 release files

1.2.8

5 release files

1.2.7

5 release files

1.2.6

5 release files

1.2.5

5 release files

1.2.4

5 release files

1.2.3

5 release files

1.2.2

5 release files

1.2.1

5 release files

1.2.0

5 release files

1.1.24

5 release files

1.1.23

5 release files

1.1.22

5 release files

1.1.21

5 release files

1.1.20

5 release files

1.1.19

5 release files

1.1.18

5 release files

1.1.17

5 release files

1.1.16

5 release files

1.1.15

5 release files

1.1.11

5 release files

1.1.10

5 release files

1.1.9

5 release files

1.1.8

5 release files

1.1.7

5 release files

1.1.6

5 release files

1.1.5

5 release files

1.1.4

5 release files

1.1.3

5 release files

1.1.2

5 release files

1.1.1

5 release files

1.1.0

5 release files

1.0.19

5 release files

1.0.18

5 release files

1.0.17

5 release files

1.0.16

5 release files

1.0.15

5 release files

1.0.14

1 release file

1.0.13

1 release file

1.0.12

2 release files

1.0.11

1 release file

1.0.9

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page