Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion package-testing/python-sdk-relay/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 .
Expand All @@ -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"]
6 changes: 6 additions & 0 deletions package-testing/python-sdk-relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions package-testing/python-sdk-relay/build-and-run.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion package-testing/python-sdk-relay/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
flask
eppo-server-sdk==4.1.0
# eppo-server-sdk installed from local wheel
104 changes: 77 additions & 27 deletions package-testing/python-sdk-relay/src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_sdk_details():
return jsonify({
"sdkName": "python-sdk",
"sdkVersion": "4.1.0",
"supportsBandits": False,
"supportsBandits": True,
"supportsDynamicTyping": False
})

Expand All @@ -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()

Expand Down Expand Up @@ -112,55 +111,106 @@ 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,
assignment_logger=LocalAssignmentLogger()
)
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,
Expand Down
Loading