Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FF-2945 feat: Python SDK #29

Merged
merged 32 commits into from
Sep 17, 2024
Merged

FF-2945 feat: Python SDK #29

merged 32 commits into from
Sep 17, 2024

Conversation

rasendubi
Copy link
Collaborator

@rasendubi rasendubi commented Aug 21, 2024

This PR is ready for review.

It's probably best to start review with the announcement to understand main changes: Eppo-exp/eppo-docs#454

@rasendubi rasendubi changed the title feat: Python SDK FF-2945 feat: Python SDK Aug 21, 2024
readme = "README.md"
authors = [{ name = "Eppo", email = "[email protected]" }]
license = { file = "LICENSE" }
requires-python = ">=3.8"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ bumping required python version from 3.6 to 3.8

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, given 3.6 hit end of life two years ago 😬

@@ -0,0 +1,3 @@
[pytest]
markers =
rust_only: mark a test as only passing on Python-on-Rust SDK.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ All tests without this marker pass on both python-on-rust and native python implementations

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'`
The one file was growing too big. Split it into multiple files for
easier maintenance.
@rasendubi rasendubi force-pushed the feat-python-sdk branch 9 times, most recently from 4d098a2 to 61d7ebe Compare August 28, 2024 17:10
@rasendubi rasendubi force-pushed the feat-python-sdk branch 3 times, most recently from 88f4b6d to 7f5f777 Compare September 4, 2024 14:00
- allow setting initial configuration
- allow disabling polling
Negative values are already forbidden.
Breaking changes:
- ConfigurationStore::set_configuration() now accepts Arc<Configuration>
- Fixed default poll interval to 30 (from 5 minutes)
This helps to avoid breakage in benchmarks and examples.
Copy link

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪 💪 💪 Awesome stuff! 📈

This is really exciting. I'm not approving because I don't know Python well and Rust at all. Although this makes me want to learn the latter 🤓

Thanks to helpful comments and method/variable names, I think I was able to decently follow. 🙏 Left a bunch of minor comments you can take or leave as you see fit!

@@ -0,0 +1,280 @@
# This file is autogenerated by maturin v1.7.0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 maturin looks pretty sweet!

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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the helpful comments

@@ -28,7 +28,7 @@ pub struct PollerThreadConfig {

impl PollerThreadConfig {
/// Default value for [`PollerThreadConfig::interval`].
pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(5 * 60);
pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(30);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for conforming to our SDK standard of default 30 second polling

@@ -0,0 +1,48 @@
// This is a prepare script that is automatically run before "start" command.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change to JS and requirement of node?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make it cross-platform and easier to run in CI. Alpine linux doesn't have bash by default (it has sh) and Windows doesn't have any posix shell by default

docs/_build/

# PyCharm
.idea/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe, I don't actually use intellij products. this was auto-generated

),
default: str,
) -> EvaluationResult: ...
def get_bandit_action_details(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

Comment on lines +118 to +119
Dict[str, ContextAttributes]
| Dict[str, Dict[str, Union[str, int, float, bool, None]]]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For later, we should also support contextless actions. I know we try to steer customers away from this, but I've seen this use case come up on many calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean supporting actions: Dict[str, None] or list[str]? We should be able to add this in the future in a backward-compatible way

subject_attributes: Attributes,
default: Py<PyString>,
) -> PyResult<EvaluationResult> {
slf.get().get_assignment_details(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense for the underlying rust function to have details passed in as boolean like pass in type? Then one less "public" function upstream SDKs need to worry about.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean a type-level parameter like this?

get_assignment::<WithDetails>(...)

It's possible but is a bit convoluted to implement (requires setting up traits and proxying implementation through trait), so isn't worth it for internal function

PySet::new_bound(
py,
self.configuration
.bandits
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor but we're deprecating the array-valued bandits in favor of object-valued banditReferences. Both have a top-level object though whose keys are the bandit keys, so should be an easy swap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I shall update the core later. (.bandits here is the whole /bandits response. "bandits" field in config response is handled deeper in the core.)

assert result.to_string() == "action"


# Native python implementation does not convert variation to string.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.

But yeah in the Eppo application UI, we only allow creating string-valued bandit flags

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4.0 extends BanditResult to EvaluationResult so different-typed variations are allowed. BanditResult is now an alias to new EvaluationResult

Something like this:

# old BanditResult:
class BanditResult:
    variation: str
    action: str | None

# new EvaluationResult:
class EvaluationResult:
    variation: Any
    action: str | None
    evaluation_details: Any | None

@leoromanovsky
Copy link
Member

I'm able to install with pip but not able to integrate it into a sample program:

➜  python-sdk git:(feat-python-sdk) ✗ pip install -e .
Obtaining file:///Users/leo/src/rust-sdk/python-sdk
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: eppo-server-sdk
  Building editable for eppo-server-sdk (pyproject.toml) ... done
  Created wheel for eppo-server-sdk: filename=eppo_server_sdk-4.0.0-cp311-cp311-macosx_11_0_arm64.whl size=4297 sha256=d3076a814895414aff8a1e5b29785242d953393c21c320d91d93054dc412e1cb
  Stored in directory: /private/var/folders/1f/95qsl9p95fz0k6jgh5cgcd3r0000gn/T/pip-ephem-wheel-cache-wsivpflm/wheels/05/a2/8b/febb161210fb0561c324e64b7ff9400edef701dbe616d25850
Successfully built eppo-server-sdk
Installing collected packages: eppo-server-sdk
  Attempting uninstall: eppo-server-sdk
    Found existing installation: eppo-server-sdk 4.0.0
    Uninstalling eppo-server-sdk-4.0.0:
      Successfully uninstalled eppo-server-sdk-4.0.0
Successfully installed eppo-server-sdk-4.0.0

[notice] A new release of pip is available: 24.1.2 -> 24.2
[notice] To update, run: python3.11 -m pip install --upgrade pip

➜  python-sdk git:(feat-python-sdk) ✗ python3 -c "from eppo_client import EppoClient; print(EppoClient)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'eppo_client'

The existing sample program I have is:

import eppo_client
import time
from eppo_client.config import Config
from eppo_client.assignment_logger import AssignmentLogger

# Set this as your Eppo API Key - https://docs.geteppo.com/prerequisites/feature-flagging/randomization-sdk/api-keys
api_key = ""

# Set this one of your Eppo experiment keys
experiment_key = ""

class ExampleAssignmentLogger(AssignmentLogger):
  def log_assignment(self, assignment_event):
    return print("Assigned experiment %s variation %s to subject %s" %(assignment_event["experiment"], assignment_event["variation"], assignment_event["subject"]))

if __name__ == "__main__":
  if not api_key:
    print("Edit example.py to enter your Eppo API key")
    exit(1)
  
  config = Config(api_key=api_key, assignment_logger=ExampleAssignmentLogger())
  client = eppo_client.init(config)
  time.sleep(1) # wait for initialization
  variation = client.get_assignment("my-user", experiment_key)
  if variation is None:
    print("No variation assigned. Check that the experiment is running.")

Are adjustments needed to it or to pyproject.toml ?

@rasendubi
Copy link
Collaborator Author

rasendubi commented Sep 12, 2024

@leoromanovsky that's interesting. The import should work without any changes.

Could you give me the output of the following:

  • which pip
  • which python3
  • python3 --version
  • ls python/eppo_client

also, does it work with non-editable install (pip install .)?

@leoromanovsky
Copy link
Member

leoromanovsky commented Sep 12, 2024

Could you give me the output of the following:

✅ got it working!

Copy link
Member

@leoromanovsky leoromanovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran it in a test python server and it operated as expected.

Really appreciate all the comments you added and explaining your rationale for decisions.

@rasendubi
Copy link
Collaborator Author

Going to merge this, so next PRs take python into account. Will coordinate with you to release it later

@rasendubi rasendubi merged commit 7d6848f into main Sep 17, 2024
24 checks passed
@rasendubi rasendubi deleted the feat-python-sdk branch September 17, 2024 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants