Skip to content

Commit

Permalink
FF-2945 feat: Python SDK (#29)
Browse files Browse the repository at this point in the history
* feat(eppo_core): make PollerThread Sync

* feat: implement python-sdk

* test(python): adapt tests to run against eppo_client-3

To run tests against old SDK:
1. Create and activate a virtual for the old SDK.
2. Install old SDK with `pip install -e .`
3. Install pytest
4. Run pytest in this directory with `-k 'not rust_only'`

* refactor(python): split into multiple files

The one file was growing too big. Split it into multiple files for
easier maintenance.

* feat(python): add bandits

* feat(python): log bandit actions

* feat(python): add non-graceful mode

* test(python): exclude weakly-typed tests

* feat(python): add BanditResult.to_string()

* feat(python): add EppoClient.is_initialized() and EppoClient.wait_for_initialization()

* feat(python): add EppoClient.get_flag_keys() and EppoClient.get_bandit_keys()

* test(python): refactor tests

- add a common util module
- move tests outside of python source (this is recommended by pytest
  to avoid confusion)

* feat(python): add EppoClient.get_bandit_action_details()

* refactor(python): rename Config to ClientConfig

This is to free up space for Configuration type that holds UFC flags
and bandits configuration.

* feat(python): add Configuration

* feat(python): export version string

* test(python): add tox configuration

* chore(mock-server): rewrite prepare.sh with javascript

* chore(python): add CI configuration for Python SDK

* chore(python): run pytest with mock server

* chore(python): echo commands in CI

* chore(python): disable test on musllinux armv7 until upstream issue is fixed

* chore(python): bundle openssl on linux

* feat(python): restore feature parity with native SDK

- allow setting initial configuration
- allow disabling polling

* feat(python): forbid 0 for poll_interval_seconds

Negative values are already forbidden.

* feat: add AssignmentCacheLogger

Just a copy from the native SDK.

* feat(python): add typing information

* fix(python): small typing fixes

* fix: adapt tests to new configuration store API

* chore: bump eppo_core to 3.0.0

Breaking changes:
- ConfigurationStore::set_configuration() now accepts Arc<Configuration>
- Fixed default poll interval to 30 (from 5 minutes)

* chore(ci): build all targets in CI

This helps to avoid breakage in benchmarks and examples.

* docs(python): update README
  • Loading branch information
rasendubi authored Sep 17, 2024
1 parent a130914 commit 7d6848f
Show file tree
Hide file tree
Showing 58 changed files with 2,861 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
submodules: true
- run: npm ci
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo build --verbose --all-targets
- run: cargo test --verbose
- run: cargo doc --verbose

Expand Down
280 changes: 280 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# This file is autogenerated by maturin v1.7.0
# To update, run
#
# maturin generate-ci -o .github/workflows/python.yml --pytest --manifest-path ./python-sdk/Cargo.toml github
#
# Manually modified:
# - tag filter in Release job to only trigger on python-sdk@ tags
# - added checkout with submodules
# - replaced `pytest` with `npm run with-server test:python`
name: Python SDK

on:
push:
branches:
- main
- master
tags:
- '*'
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
- runner: ubuntu-latest
target: s390x
- runner: ubuntu-latest
target: ppc64le
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: 3.x

- name: Build wheels
uses: PyO3/maturin-action@v1
if: ${{ startsWith(matrix.platform.target, 'x86') }}
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./python-sdk/Cargo.toml
sccache: 'true'
manylinux: auto
before-script-linux: |
yum install -y perl-IPC-Cmd devtoolset-10-libatomic-devel
- name: Build wheels
uses: PyO3/maturin-action@v1
if: ${{ !startsWith(matrix.platform.target, 'x86') }}
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./python-sdk/Cargo.toml
sccache: 'true'
manylinux: auto
before-script-linux: |
apt-get update
apt-get install --no-install-recommends -y libssl-dev
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist

- name: pytest
if: ${{ startsWith(matrix.platform.target, 'x86_64') }}
shell: bash
run: |
set -ex
python3 -m venv .venv
source .venv/bin/activate
pip install eppo-server-sdk --find-links dist --force-reinstall
pip install pytest cachetools
npm ci
npm run with-server test:python
- name: pytest
if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }}
uses: uraimo/run-on-arch-action@v2
with:
arch: ${{ matrix.platform.target }}
distro: ubuntu22.04
githubToken: ${{ github.token }}
install: |
apt-get update
apt-get install -y --no-install-recommends python3 python3-pip nodejs npm
pip3 install -U pip pytest cachetools
run: |
set -ex
pip3 install eppo-server-sdk --find-links dist --force-reinstall
npm ci
npm run with-server test:python
musllinux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
- runner: ubuntu-latest
target: x86
- runner: ubuntu-latest
target: aarch64
- runner: ubuntu-latest
target: armv7
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./python-sdk/Cargo.toml
sccache: 'true'
manylinux: musllinux_1_2
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: dist
- name: pytest
if: ${{ startsWith(matrix.platform.target, 'x86_64') }}
uses: addnab/docker-run-action@v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
run: |
set -ex
apk add py3-pip py3-virtualenv nodejs npm
python3 -m virtualenv .venv
source .venv/bin/activate
pip install eppo-server-sdk --no-index --find-links dist --force-reinstall
pip install pytest cachetools
npm ci
npm run with-server test:python
- name: pytest
# `npm ci` just hangs on Alpine armv7 now.
# Disabling tests until this issue is fixed:
# https://github.com/nodejs/docker-node/issues/1829
if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'armv7' }}
uses: uraimo/run-on-arch-action@v2
with:
arch: ${{ matrix.platform.target }}
distro: alpine_latest
githubToken: ${{ github.token }}
install: |
apk add py3-virtualenv nodejs npm
run: |
set -ex
python3 -m virtualenv .venv
source .venv/bin/activate
pip install pytest cachetools
pip install eppo-server-sdk --find-links dist --force-reinstall
npm ci
npm run with-server test:python
windows:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: 3.x
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./python-sdk/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
- name: pytest
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
run: |
set -ex
python3 -m venv .venv
source .venv/Scripts/activate
pip install eppo-server-sdk --find-links dist --force-reinstall
pip install pytest cachetools
npm ci
npm run with-server test:python
macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: macos-12
target: x86_64
- runner: macos-14
target: aarch64
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./python-sdk/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
- name: pytest
run: |
set -ex
python3 -m venv .venv
source .venv/bin/activate
pip install eppo-server-sdk --find-links dist --force-reinstall
pip install pytest cachetools
npm ci
npm run with-server test:python
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist --manifest-path ./python-sdk/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist

release:
name: Release
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/python-sdk@')"
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"eppo_core",
"rust-sdk",
"python-sdk",
"ruby-sdk/ext/eppo_client",
]

Expand Down
10 changes: 9 additions & 1 deletion eppo_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "eppo_core"
version = "2.0.0"
version = "3.0.0"
edition = "2021"
description = "Eppo SDK core library"
repository = "https://github.com/Eppo-exp/rust-sdk"
Expand All @@ -9,6 +9,10 @@ keywords = ["eppo", "feature-flags"]
categories = ["config"]
rust-version = "1.71.1"

[features]
# Add implementation of `FromPyObject`/`ToPyObject` for some types.
pyo3 = ["dep:pyo3", "dep:serde-pyobject"]

[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
derive_more = "0.99.17"
Expand All @@ -23,6 +27,10 @@ serde_json = "1.0.116"
thiserror = "1.0.60"
url = "2.5.0"

# pyo3 dependencies
pyo3 = { version = "0.22.0", optional = true, default-features = false }
serde-pyobject = { version = "0.4.0", optional = true}

[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.11.3"
Expand Down
2 changes: 1 addition & 1 deletion eppo_core/benches/evaluation_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fs::File;
use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};

use eppo_core::{
ufc::{get_assignment, get_assignment_details},
eval::{get_assignment, get_assignment_details},
Configuration,
};

Expand Down
31 changes: 31 additions & 0 deletions eppo_core/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,34 @@ impl From<&str> for AttributeValue {
Self::String(value.to_owned())
}
}

#[cfg(feature = "pyo3")]
mod pyo3_impl {
use pyo3::{exceptions::PyTypeError, prelude::*, types::*};

use super::*;

impl<'py> FromPyObject<'py> for AttributeValue {
fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult<AttributeValue> {
if let Ok(s) = value.downcast::<PyString>() {
return Ok(AttributeValue::String(s.extract()?));
}
// In Python, Bool inherits from Int, so it must be checked first here.
if let Ok(s) = value.downcast::<PyBool>() {
return Ok(AttributeValue::Boolean(s.extract()?));
}
if let Ok(s) = value.downcast::<PyFloat>() {
return Ok(AttributeValue::Number(s.extract()?));
}
if let Ok(s) = value.downcast::<PyInt>() {
return Ok(AttributeValue::Number(s.extract()?));
}
if let Ok(_) = value.downcast::<PyNone>() {
return Ok(AttributeValue::Null);
}
Err(PyTypeError::new_err(
"invalid type for subject attribute value",
))
}
}
}
7 changes: 3 additions & 4 deletions eppo_core/src/configuration_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ impl ConfigurationStore {
}

/// Set new configuration.
pub fn set_configuration(&self, config: Configuration) {
let config = Arc::new(config);
pub fn set_configuration(&self, config: Arc<Configuration>) {
let mut configuration_slot = self
.configuration
.write()
Expand Down Expand Up @@ -66,7 +65,7 @@ mod tests {
{
let store = store.clone();
let _ = std::thread::spawn(move || {
store.set_configuration(Configuration::from_server_response(
store.set_configuration(Arc::new(Configuration::from_server_response(
UniversalFlagConfig {
created_at: Utc::now(),
environment: Environment {
Expand All @@ -76,7 +75,7 @@ mod tests {
bandits: HashMap::new(),
},
None,
))
)))
})
.join();
}
Expand Down
Loading

0 comments on commit 7d6848f

Please sign in to comment.