Skip to content
Merged
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
67 changes: 40 additions & 27 deletions splitio/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import logging
import time
import six
from splitio.engine.evaluator import Evaluator, CONTROL
from splitio.engine.splitters import Splitter
from splitio.models.impressions import Impression, Label
Expand Down Expand Up @@ -84,36 +85,21 @@ def _send_impression_to_listener(self, impression, attributes):
)
self._logger.debug('Error', exc_info=True)

def get_treatment(self, key, feature, attributes=None):
"""
Get the treatment for a feature and key, with an optional dictionary of attributes.

This method never raises an exception. If there's a problem, the appropriate log message
will be generated and the method will return the CONTROL treatment.

:param key: The key for which to get the treatment
:type key: str
:param feature: The name of the feature for which to get the treatment
:type feature: str
:param attributes: An optional dictionary of attributes
:type attributes: dict
:return: The treatment for the key and feature
:rtype: str
"""
def get_treatment_with_config(self, key, feature, attributes=None):
try:
if self.destroyed:
self._logger.error("Client has already been destroyed - no calls possible")
return CONTROL
return CONTROL, None

start = int(round(time.time() * 1000))

matching_key, bucketing_key = input_validator.validate_key(key, 'get_treatment')
matching_key, bucketing_key = input_validator.validate_key(key)
feature = input_validator.validate_feature_name(feature)

if (matching_key is None and bucketing_key is None) \
or feature is None \
or not input_validator.validate_attributes(attributes, 'get_treatment'):
return CONTROL
or not input_validator.validate_attributes(attributes):
return CONTROL, None

result = self._evaluator.evaluate_treatment(
feature,
Expand All @@ -134,7 +120,7 @@ def get_treatment(self, key, feature, attributes=None):

self._record_stats(impression, start, self._METRIC_GET_TREATMENT)
self._send_impression_to_listener(impression, attributes)
return result['treatment']
return result['treatment'], result['configurations']
except Exception: #pylint: disable=broad-except
self._logger.error('Error getting treatment for feature')
self._logger.debug('Error: ', exc_info=True)
Expand All @@ -153,9 +139,28 @@ def get_treatment(self, key, feature, attributes=None):
except Exception: # pylint: disable=broad-except
self._logger.error('Error reporting impression into get_treatment exception block')
self._logger.debug('Error: ', exc_info=True)
return CONTROL
return CONTROL, None

def get_treatments(self, key, features, attributes=None):
def get_treatment(self, key, feature, attributes=None):
"""
Get the treatment for a feature and key, with an optional dictionary of attributes.

This method never raises an exception. If there's a problem, the appropriate log message
will be generated and the method will return the CONTROL treatment.

:param key: The key for which to get the treatment
:type key: str
:param feature: The name of the feature for which to get the treatment
:type feature: str
:param attributes: An optional dictionary of attributes
:type attributes: dict
:return: The treatment for the key and feature
:rtype: str
"""
treatment, _ = self.get_treatment_with_config(key, feature, attributes)
return treatment

def get_treatments_with_config(self, key, features, attributes=None):
"""
Evaluate multiple features and return a dictionary with all the feature/treatments.

Expand All @@ -177,11 +182,11 @@ def get_treatments(self, key, features, attributes=None):

start = int(round(time.time() * 1000))

matching_key, bucketing_key = input_validator.validate_key(key, 'get_treatments')
matching_key, bucketing_key = input_validator.validate_key(key)
if matching_key is None and bucketing_key is None:
return input_validator.generate_control_treatments(features)

if input_validator.validate_attributes(attributes, 'get_treatments') is False:
if input_validator.validate_attributes(attributes) is False:
return input_validator.generate_control_treatments(features)

features = input_validator.validate_features_get_treatments(features)
Expand Down Expand Up @@ -209,13 +214,15 @@ def get_treatments(self, key, features, attributes=None):
start)

bulk_impressions.append(impression)
treatments[feature] = treatment['treatment']
treatments[feature] = (treatment['treatment'], treatment['configurations'])

except Exception: #pylint: disable=broad-except
self._logger.error('get_treatments: An exception occured when evaluating '
'feature ' + feature + ' returning CONTROL.')
treatments[feature] = CONTROL
treatments[feature] = CONTROL, None
self._logger.debug('Error: ', exc_info=True)
import traceback
traceback.print_exc()
continue

# Register impressions
Expand All @@ -231,6 +238,12 @@ def get_treatments(self, key, features, attributes=None):

return treatments


def get_treatments(self, key, features, attributes=None):
"""TODO"""
with_config = self.get_treatments_with_config(key, features, attributes)
return {feature: result[0] for (feature, result) in six.iteritems(with_config)}

def _build_impression( #pylint: disable=too-many-arguments
self,
matching_key,
Expand Down
47 changes: 34 additions & 13 deletions splitio/client/input_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from splitio.api import APIException
from splitio.client.key import Key
from splitio.client.util import get_calls
from splitio.engine.evaluator import CONTROL


Expand All @@ -19,6 +20,22 @@
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'


def _get_first_split_sdk_call():
"""
Get the method name of the original call on the SplitClient methods.

:return: Name of the method called by the user.
:rtype: str
"""
unknown_method = 'unknown-method'
try:
calls = get_calls(['Client', 'SplitManager'])
if calls:
return calls[-1]
return unknown_method
except Exception: #pylint: disable=broad-except
return unknown_method

def _check_not_null(value, name, operation):
"""
Check if value is null.
Expand Down Expand Up @@ -198,7 +215,7 @@ def _remove_empty_spaces(value, operation):
return strip_value


def validate_key(key, operation):
def validate_key(key):
"""
Validate Key parameter for get_treatment/s.

Expand All @@ -211,6 +228,7 @@ def validate_key(key, operation):
:return: The tuple key
:rtype: (matching_key,bucketing_key)
"""
operation = _get_first_split_sdk_call()
matching_key_result = None
bucketing_key_result = None
if key is None:
Expand Down Expand Up @@ -243,11 +261,12 @@ def validate_feature_name(feature_name):
:return: feature_name
:rtype: str|None
"""
if (not _check_not_null(feature_name, 'feature_name', 'get_treatment')) or \
(not _check_is_string(feature_name, 'feature_name', 'get_treatment')) or \
(not _check_string_not_empty(feature_name, 'feature_name', 'get_treatment')):
operation = _get_first_split_sdk_call()
if (not _check_not_null(feature_name, 'feature_name', operation)) or \
(not _check_is_string(feature_name, 'feature_name', operation)) or \
(not _check_string_not_empty(feature_name, 'feature_name', operation)):
return None
return _remove_empty_spaces(feature_name, 'get_treatment')
return _remove_empty_spaces(feature_name, operation)


def validate_track_key(key):
Expand Down Expand Up @@ -348,20 +367,21 @@ def validate_features_get_treatments(features): #pylint: disable=invalid-name
:return: filtered_features
:rtype: list|None
"""
operation = _get_first_split_sdk_call()
if features is None or not isinstance(features, list):
_LOGGER.error('get_treatments: feature_names must be a non-empty array.')
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
return None
if not features:
_LOGGER.error('get_treatments: feature_names must be a non-empty array.')
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
return []
filtered_features = set(
_remove_empty_spaces(feature, 'get_treatments') for feature in features
_remove_empty_spaces(feature, operation) for feature in features
if feature is not None and
_check_is_string(feature, 'feature_name', 'get_treatments') and
_check_string_not_empty(feature, 'feature_name', 'get_treatments')
_check_is_string(feature, 'feature_name', operation) and
_check_string_not_empty(feature, 'feature_name', operation)
)
if not filtered_features:
_LOGGER.error('get_treatments: feature_names must be a non-empty array.')
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
return None
return filtered_features

Expand All @@ -375,10 +395,10 @@ def generate_control_treatments(features):
:return: dict
:rtype: dict|None
"""
return {feature: CONTROL for feature in validate_features_get_treatments(features)}
return {feature: (CONTROL, None) for feature in validate_features_get_treatments(features)}


def validate_attributes(attributes, operation):
def validate_attributes(attributes):
"""
Check if attributes is valid.

Expand All @@ -389,6 +409,7 @@ def validate_attributes(attributes, operation):
:return: bool
:rtype: True|False
"""
operation = _get_first_split_sdk_call()
if attributes is None:
return True
if not isinstance(attributes, dict):
Expand Down
22 changes: 22 additions & 0 deletions splitio/client/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""General purpose SDK utilities."""

import inspect
import socket
from collections import namedtuple
from splitio.version import __version__
Expand All @@ -10,6 +11,8 @@
)




def _get_ip():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
Expand All @@ -26,9 +29,28 @@ def _get_ip():
def _get_hostname(ip_address):
return 'unknown' if ip_address == 'unknown' else 'ip-' + ip_address.replace('.', '-')


def get_metadata(*args, **kwargs):
"""Gather SDK metadata and return a tuple with such info."""
version = 'python-%s' % __version__
ip_address = _get_ip()
hostname = _get_hostname(ip_address)
return SdkMetadata(version, hostname, ip_address)


def get_calls(classes_filter=None):
"""
Inspect the stack and retrieve an ordered list of caller functions.

:param class_filter: If not None, only methods from that classes will be returned.
:type class: list(str)

:return: list of callers ordered by most recent first.
:rtype: list(tuple(str, str))
"""
return [
inspect.getframeinfo(frame[0]).function
for frame in inspect.stack()
if classes_filter is None
or 'self' in frame[0].f_locals and frame[0].f_locals['self'].__class__.__name__ in classes_filter #pylint: disable=line-too-long
]
1 change: 1 addition & 0 deletions splitio/engine/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def evaluate_treatment(self, feature, matching_key,

return {
'treatment': _treatment,
'configurations': split.get_configurations_for(_treatment) if split else None,
'impression': {
'label': label,
'change_number': _change_number
Expand Down
15 changes: 12 additions & 3 deletions splitio/models/splits.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def __init__( #pylint: disable=too-many-arguments
conditions=None,
algo=None,
traffic_allocation=None,
traffic_allocation_seed=None
traffic_allocation_seed=None,
configurations=None
):
"""
Class constructor.
Expand Down Expand Up @@ -91,6 +92,8 @@ def __init__( #pylint: disable=too-many-arguments
except ValueError:
self._algo = HashAlgorithm.LEGACY

self._configurations = configurations

@property
def name(self):
"""Return name."""
Expand Down Expand Up @@ -146,6 +149,10 @@ def traffic_allocation_seed(self):
"""Return the traffic allocation seed of the split."""
return self._traffic_allocation_seed

def get_configurations_for(self, treatment):
"""Return the mapping of treatments to configurations."""
return self._configurations.get(treatment) if self._configurations else None

def get_segment_names(self):
"""
Return a list of segment names referenced in all matchers from this split.
Expand All @@ -168,7 +175,8 @@ def to_json(self):
'killed': self.killed,
'defaultTreatment': self.default_treatment,
'algo': self.algo.value,
'conditions': [c.to_json() for c in self.conditions]
'conditions': [c.to_json() for c in self.conditions],
'configurations': self._configurations
}

def to_split_view(self):
Expand Down Expand Up @@ -219,5 +227,6 @@ def from_raw(raw_split):
[condition.from_raw(c) for c in raw_split['conditions']],
raw_split.get('algo'),
traffic_allocation=raw_split.get('trafficAllocation'),
traffic_allocation_seed=raw_split.get('trafficAllocationSeed')
traffic_allocation_seed=raw_split.get('trafficAllocationSeed'),
configurations=raw_split.get('configurations')
)
Loading