diff --git a/package-testing/python-sdk-relay/Dockerfile b/package-testing/python-sdk-relay/Dockerfile index 183d3ab7..4c900f11 100644 --- a/package-testing/python-sdk-relay/Dockerfile +++ b/package-testing/python-sdk-relay/Dockerfile @@ -1,5 +1,9 @@ FROM python:3.12 +# Install Cargo and Rust dependencies +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + WORKDIR /app COPY requirements.txt . @@ -8,4 +12,12 @@ RUN pip install -r requirements.txt # Copy the source code COPY src/ ./src/ -CMD ["python", "/app/src/server.py"] +COPY --chmod=755 build-and-run.sh / + +ENV SDK_RELAY_HOST=0.0.0.0 + +EXPOSE $SDK_RELAY_PORT + +HEALTHCHECK CMD curl --fail http://localhost:${SDK_RELAY_PORT} || exit 1 + +CMD ["/build-and-run.sh"] diff --git a/package-testing/python-sdk-relay/README.md b/package-testing/python-sdk-relay/README.md index 5c56e290..536d7e6f 100644 --- a/package-testing/python-sdk-relay/README.md +++ b/package-testing/python-sdk-relay/README.md @@ -2,6 +2,12 @@ Post test case files to this server and check the results against what's expected. +## Build and run + +```shell +./build-and-run.sh +``` + ## Running locally with Docker Build the docker image: diff --git a/package-testing/python-sdk-relay/build-and-run.sh b/package-testing/python-sdk-relay/build-and-run.sh new file mode 100755 index 00000000..f9a7e645 --- /dev/null +++ b/package-testing/python-sdk-relay/build-and-run.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Set default values for vars + +: "${SDK_REF:=main}" +: "${SDK_RELAY_HOST:=localhost}" +: "${SDK_RELAY_PORT:=4000}" +SDK="https://github.com/Eppo-exp/eppo-multiplatform.git" + +# checkout the specified ref of the SDK repo, build it, and then insert it into vendors here. +rm -rf tmp +mkdir -p tmp + +echo "Cloning ${SDK}@${SDK_REF}" +git clone -b ${SDK_REF} --depth 1 --single-branch ${SDK} tmp || ( echo "Cloning repo failed"; exit 1 ) + +# Run the poller +python3 -m venv tmp/.venv +source tmp/.venv/bin/activate +pip install maturin + +pip install -r requirements.txt + +# Build the wheel file in tmp directory +maturin build --release --out tmp/dist --find-interpreter --manifest-path ./tmp/python-sdk/Cargo.toml + +# Get Python version and find matching wheel +PYTHON_VERSION=$(python3 -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")') +echo "Looking for wheel for Python version: ${PYTHON_VERSION}" + +WHEEL_FILE=$(find tmp/dist -name "eppo_server_sdk-*-${PYTHON_VERSION}-*.whl" | head -n 1) +echo "Found wheel file: ${WHEEL_FILE}" + +if [ -z "$WHEEL_FILE" ]; then + echo "Error: Wheel file not found for Python version ${PYTHON_VERSION}" + echo "Available wheels:" + ls tmp/dist + exit 1 +fi + +pip install "${WHEEL_FILE}" + +echo "Listening on port ${SDK_RELAY_HOST}:${SDK_RELAY_PORT}" + +python3 src/server.py diff --git a/package-testing/python-sdk-relay/requirements.txt b/package-testing/python-sdk-relay/requirements.txt index 98476c97..9c2583ce 100644 --- a/package-testing/python-sdk-relay/requirements.txt +++ b/package-testing/python-sdk-relay/requirements.txt @@ -1,2 +1,2 @@ flask -eppo-server-sdk==4.1.0 +# eppo-server-sdk installed from local wheel diff --git a/package-testing/python-sdk-relay/src/server.py b/package-testing/python-sdk-relay/src/server.py index 4f1aa887..c0849ca1 100644 --- a/package-testing/python-sdk-relay/src/server.py +++ b/package-testing/python-sdk-relay/src/server.py @@ -38,7 +38,7 @@ def get_sdk_details(): return jsonify({ "sdkName": "python-sdk", "sdkVersion": "4.1.0", - "supportsBandits": False, + "supportsBandits": True, "supportsDynamicTyping": False }) @@ -52,7 +52,6 @@ def handle_assignment(): assignment_type=data['assignmentType'], default_value=data['defaultValue'] ) - print(f"Request object: {request_obj}") client = eppo_client.get_instance() @@ -112,40 +111,80 @@ def handle_assignment(): } return jsonify(response) -@dataclass -class BanditActionRequest: - flag: str - subject_key: str - subject_attributes: dict - actions: list - default_value: any - @app.route('/bandits/v1/action', methods=['POST']) def handle_bandit(): data = request.json - request_obj = BanditActionRequest( - flag=data['flag'], - subject_key=data['subjectKey'], - subject_attributes=data['subjectAttributes'], - default_value=data['defaultValue'], - actions=data['actions'] - ) - print(f"Request object: {request_obj}") + print(f"Request data: {data}") - # TODO: Implement bandit logic - return jsonify({ - "result": "action", - "assignmentLog": [], - "banditLog": [], - "error": None - }) + flag = data['flag'] + subject_key = data['subjectKey'] + subject_attributes = data['subjectAttributes'] + default_value = data['defaultValue'] + actions = data['actions'] + + try: + # Create subject context using ContextAttributes constructor + subject_context = eppo_client.bandit.ContextAttributes( + numeric_attributes=subject_attributes['numericAttributes'], + categorical_attributes=subject_attributes['categoricalAttributes'] + ) + + # Create actions dictionary using ContextAttributes constructor + actions = {} + for action in actions: + action_key = action['actionKey'] + action_context = eppo_client.bandit.ContextAttributes( + numeric_attributes=action['numericAttributes'], + categorical_attributes=action['categoricalAttributes'] + ) + actions[action_key] = action_context + + print(f"\nExecuting bandit action:") + print(f"Flag: {flag}") + print(f"Subject: {subject_key}") + print(f"Default: {default_value}") + print(f"Available actions: {list(actions.keys())}") + + client = eppo_client.get_instance() + result = client.get_bandit_action( + flag, + subject_key, + subject_context, + actions, + default_value + ) + print(f"Raw result from get_bandit_action: {result}") + + response = { + "result": { + "variation": result.variation, + "action": result.action + }, + "assignmentLog": [], + "banditLog": [], + "error": None + } + return jsonify(response) + + except Exception as e: + print(f"Error processing bandit: {str(e)}") + response = { + "result": None, + "assignmentLog": [], + "banditLog": [], + "error": str(e) + } + return jsonify(response) def initialize_client_and_wait(): print("Initializing client") api_key = environ.get('EPPO_API_KEY', 'NOKEYSPECIFIED') base_url = environ.get('EPPO_BASE_URL', 'http://localhost:5000/api') + # Add debug logging for initialization + print(f"Initializing with API key: {api_key}, base URL: {base_url}") + client_config = Config( api_key=api_key, base_url=base_url, @@ -153,14 +192,25 @@ def initialize_client_and_wait(): ) eppo_client.init(client_config) client = eppo_client.get_instance() + + # Add debug logging for initialization status + print("Waiting for initialization...") client.wait_for_initialization() - print("Client initialized") + print("Client initialization complete") + + # Try to fetch a configuration to verify it's working + try: + config = client.get_configuration() + print(f"Test configuration: {config}") + except Exception as e: + print(f"Error fetching test configuration: {e}") if __name__ == "__main__": initialize_client_and_wait() port = int(environ.get('SDK_RELAY_PORT', 7001)) - host = environ.get('SDK_RELAY_HOST', '0.0.0.0') + #host = environ.get('SDK_RELAY_HOST', '0.0.0.0') + host = '0.0.0.0' print(f"Starting server on {host}:{port}") app.run( host=host,