Skip to content

Commit ae2de24

Browse files
authored
Merge pull request #156 from splitio/development
Development
2 parents 25aa964 + 98e4093 commit ae2de24

29 files changed

+576
-160
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
8.1.4 (Oct 14, 2019)
2+
- Added logic to fetch multiple splits at once on get_treatments/get_treatments_with_config.
3+
- Added flag `ipAddressesEnabled` into config to enable/disable sending machineName and machineIp when data is posted in headers.
4+
15
8.1.3 (Oct 4, 2019)
26
- Fixed race condition related to segment fetching and SDK_READY event
37

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w
77
[![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware)
88

99
## Compatibility
10-
This SDK is compatible with Python 2.7 and higher.
10+
This SDK is compatible with **Python 2.7 and higher**.
1111

1212
## Getting started
1313
Below is a simple example that describes the instantiation and most basic usage of our SDK:
@@ -22,14 +22,20 @@ factory = get_factory('YOUR_SDK_TYPE_API_KEY', config=config)
2222
try:
2323
factory.block_until_ready(5) # wait up to 5 seconds
2424
split = factory.client()
25-
print(split.get_treatment('CUSTOMER_ID', 'SPLIT_NAME'))
25+
treatment = split.get_treatment('CUSTOMER_ID', 'SPLIT_NAME')
26+
if treatment == "on":
27+
# insert code here to show on treatment
28+
elif treatment == "off":
29+
# insert code here to show off treatment
30+
else:
31+
# insert your control treatment code here
2632
except TimeoutException:
2733
# Now the user can choose whether to abort the whole execution, or just keep going
2834
# without a ready client, which if configured properly, should become ready at some point.
2935
pass
3036
```
3137

32-
Please refer to [our official docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup.
38+
Please refer to [our official docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup.
3339

3440
## Submitting issues
3541
The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/python-client/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner.

splitio/api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Split API module."""
22

3+
34
class APIException(Exception):
45
"""Exception to raise when an API call fails."""
56

@@ -28,4 +29,6 @@ def headers_from_metadata(sdk_metadata):
2829
'SplitSDKVersion': sdk_metadata.sdk_version,
2930
'SplitSDKMachineIP': sdk_metadata.instance_ip,
3031
'SplitSDKMachineName': sdk_metadata.instance_name
32+
} if sdk_metadata.instance_ip != 'NA' and sdk_metadata.instance_ip != 'unknown' else {
33+
'SplitSDKVersion': sdk_metadata.sdk_version,
3134
}

splitio/client/client.py

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,13 @@ def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=No
9696
}
9797
}
9898

99-
return self._evaluator.evaluate_treatment(
99+
return self._evaluator.evaluate_feature(
100100
feature,
101101
matching_key,
102102
bucketing_key,
103103
attributes
104104
)
105105

106-
107106
def get_treatment_with_config(self, key, feature, attributes=None):
108107
"""
109108
Get the treatment and config for a feature and key, with optional dictionary of attributes.
@@ -131,7 +130,7 @@ def get_treatment_with_config(self, key, feature, attributes=None):
131130
feature = input_validator.validate_feature_name(
132131
feature,
133132
self.ready,
134-
self._factory._get_storage('splits') #pylint: disable=protected-access
133+
self._factory._get_storage('splits') # pylint: disable=protected-access
135134
)
136135

137136
if (matching_key is None and bucketing_key is None) \
@@ -193,6 +192,24 @@ def get_treatment(self, key, feature, attributes=None):
193192
treatment, _ = self.get_treatment_with_config(key, feature, attributes)
194193
return treatment
195194

195+
def _evaluate_features_if_ready(self, matching_key, bucketing_key, features, attributes=None):
196+
if not self.ready:
197+
return {
198+
feature: {
199+
'treatment': CONTROL,
200+
'configurations': None,
201+
'impression': {'label': Label.NOT_READY, 'change_number': None}
202+
}
203+
for feature in features
204+
}
205+
206+
return self._evaluator.evaluate_features(
207+
features,
208+
matching_key,
209+
bucketing_key,
210+
attributes
211+
)
212+
196213
def get_treatments_with_config(self, key, features, attributes=None):
197214
"""
198215
Evaluate multiple features and return a dict with feature -> (treatment, config).
@@ -233,39 +250,47 @@ def get_treatments_with_config(self, key, features, attributes=None):
233250
bulk_impressions = []
234251
treatments = {name: (CONTROL, None) for name in missing}
235252

236-
for feature in features:
253+
try:
254+
evaluations = self._evaluate_features_if_ready(matching_key, bucketing_key,
255+
list(features), attributes)
256+
257+
for feature in features:
258+
try:
259+
result = evaluations[feature]
260+
impression = self._build_impression(matching_key,
261+
feature,
262+
result['treatment'],
263+
result['impression']['label'],
264+
result['impression']['change_number'],
265+
bucketing_key,
266+
start)
267+
268+
bulk_impressions.append(impression)
269+
treatments[feature] = (result['treatment'], result['configurations'])
270+
271+
except Exception: # pylint: disable=broad-except
272+
self._logger.error('get_treatments: An exception occured when evaluating '
273+
'feature ' + feature + ' returning CONTROL.')
274+
treatments[feature] = CONTROL, None
275+
self._logger.debug('Error: ', exc_info=True)
276+
continue
277+
278+
# Register impressions
237279
try:
238-
result = self._evaluate_if_ready(matching_key, bucketing_key, feature, attributes)
239-
impression = self._build_impression(matching_key,
240-
feature,
241-
result['treatment'],
242-
result['impression']['label'],
243-
result['impression']['change_number'],
244-
bucketing_key,
245-
start)
246-
247-
bulk_impressions.append(impression)
248-
treatments[feature] = (result['treatment'], result['configurations'])
249-
280+
if bulk_impressions:
281+
self._record_stats(bulk_impressions, start, self._METRIC_GET_TREATMENTS)
282+
for impression in bulk_impressions:
283+
self._send_impression_to_listener(impression, attributes)
250284
except Exception: # pylint: disable=broad-except
251-
self._logger.error('get_treatments: An exception occured when evaluating '
252-
'feature ' + feature + ' returning CONTROL.')
253-
treatments[feature] = CONTROL, None
285+
self._logger.error('get_treatments: An exception when trying to store '
286+
'impressions.')
254287
self._logger.debug('Error: ', exc_info=True)
255-
continue
256288

257-
# Register impressions
258-
try:
259-
if bulk_impressions:
260-
self._record_stats(bulk_impressions, start, self._METRIC_GET_TREATMENTS)
261-
for impression in bulk_impressions:
262-
self._send_impression_to_listener(impression, attributes)
289+
return treatments
263290
except Exception: # pylint: disable=broad-except
264-
self._logger.error('get_treatments: An exception when trying to store '
265-
'impressions.')
291+
self._logger.error('Error getting treatment for features')
266292
self._logger.debug('Error: ', exc_info=True)
267-
268-
return treatments
293+
return input_validator.generate_control_treatments(list(features))
269294

270295
def get_treatments(self, key, features, attributes=None):
271296
"""
@@ -354,11 +379,11 @@ def track(self, key, traffic_type, event_type, value=None, properties=None):
354379

355380
key = input_validator.validate_track_key(key)
356381
event_type = input_validator.validate_event_type(event_type)
357-
should_validate_existance = self.ready and self._factory._apikey != 'localhost' #pylint: disable=protected-access
382+
should_validate_existance = self.ready and self._factory._apikey != 'localhost' # pylint: disable=protected-access
358383
traffic_type = input_validator.validate_traffic_type(
359384
traffic_type,
360385
should_validate_existance,
361-
self._factory._get_storage('splits'), #pylint: disable=protected-access
386+
self._factory._get_storage('splits'), # pylint: disable=protected-access
362387
)
363388

364389
value = input_validator.validate_value(value)

splitio/client/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'eventsBulkSize': 5000,
1717
'eventsQueueSize': 10000,
1818
'labelsEnabled': True,
19+
'ipAddressesEnabled': True,
1920
'impressionListener': None,
2021
'redisHost': 'localhost',
2122
'redisPort': 6379,

splitio/client/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self, factory):
1818
"""
1919
self._logger = logging.getLogger(self.__class__.__name__)
2020
self._factory = factory
21-
self._storage = factory._get_storage('splits') #pylint: disable=protected-access
21+
self._storage = factory._get_storage('splits') # pylint: disable=protected-access
2222

2323
def split_names(self):
2424
"""

splitio/client/util.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def _get_ip():
1717
# doesn't even have to be reachable
1818
sock.connect(('10.255.255.255', 1))
1919
ip_address = sock.getsockname()[0]
20-
except Exception: #pylint: disable=broad-except
20+
except Exception: # pylint: disable=broad-except
2121
ip_address = 'unknown'
2222
finally:
2323
sock.close()
@@ -28,6 +28,16 @@ def _get_hostname(ip_address):
2828
return 'unknown' if ip_address == 'unknown' else 'ip-' + ip_address.replace('.', '-')
2929

3030

31+
def _get_hostname_and_ip(config):
32+
if config.get('ipAddressesEnabled') is False:
33+
return 'NA', 'NA'
34+
ip_from_config = config.get('machineIp')
35+
machine_from_config = config.get('machineName')
36+
ip_address = ip_from_config if ip_from_config is not None else _get_ip()
37+
hostname = machine_from_config if machine_from_config is not None else _get_hostname(ip_address)
38+
return ip_address, hostname
39+
40+
3141
def get_metadata(config):
3242
"""
3343
Gather SDK metadata and return a tuple with such info.
@@ -39,10 +49,7 @@ def get_metadata(config):
3949
:rtype: SdkMetadata
4050
"""
4151
version = 'python-%s' % __version__
42-
ip_from_config = config.get('machineIp')
43-
machine_from_config = config.get('machineName')
44-
ip_address = ip_from_config if ip_from_config is not None else _get_ip()
45-
hostname = machine_from_config if machine_from_config is not None else _get_hostname(ip_address)
52+
ip_address, hostname = _get_hostname_and_ip(config)
4653
return SdkMetadata(version, hostname, ip_address)
4754

4855

@@ -61,7 +68,7 @@ def get_calls(classes_filter=None):
6168
inspect.getframeinfo(frame[0]).function
6269
for frame in inspect.stack()
6370
if classes_filter is None
64-
or 'self' in frame[0].f_locals and frame[0].f_locals['self'].__class__.__name__ in classes_filter #pylint: disable=line-too-long
71+
or 'self' in frame[0].f_locals and frame[0].f_locals['self'].__class__.__name__ in classes_filter # pylint: disable=line-too-long
6572
]
66-
except Exception: #pylint: disable=broad-except
73+
except Exception: # pylint: disable=broad-except
6774
return []

splitio/engine/evaluator.py

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Split evaluator module."""
22
import logging
3+
import six
34
from splitio.models.grammar.condition import ConditionType
45
from splitio.models.impressions import Label
56

@@ -25,8 +26,7 @@ def __init__(self, split_storage, segment_storage, splitter):
2526
self._segment_storage = segment_storage
2627
self._splitter = splitter
2728

28-
def evaluate_treatment(self, feature, matching_key,
29-
bucketing_key, attributes=None):
29+
def _evaluate_treatment(self, feature, matching_key, bucketing_key, attributes, split):
3030
"""
3131
Evaluate the user submitted data against a feature and return the resulting treatment.
3232
@@ -42,20 +42,19 @@ def evaluate_treatment(self, feature, matching_key,
4242
:param attributes: An optional dictionary of attributes
4343
:type attributes: dict
4444
45+
:param split: Split object
46+
:type attributes: splitio.models.splits.Split|None
47+
4548
:return: The treatment for the key and split
4649
:rtype: object
4750
"""
4851
label = ''
4952
_treatment = CONTROL
5053
_change_number = -1
5154

52-
# Fetching Split definition
53-
split = self._split_storage.get(feature)
54-
5555
if split is None:
5656
self._logger.warning('Unknown or invalid feature: %s', feature)
5757
label = Label.SPLIT_NOT_FOUND
58-
_treatment = CONTROL
5958
else:
6059
_change_number = split.change_number
6160
if split.killed:
@@ -83,6 +82,70 @@ def evaluate_treatment(self, feature, matching_key,
8382
}
8483
}
8584

85+
def evaluate_feature(self, feature, matching_key, bucketing_key, attributes=None):
86+
"""
87+
Evaluate the user submitted data against a feature and return the resulting treatment.
88+
89+
:param feature: The feature for which to get the treatment
90+
:type feature: str
91+
92+
:param matching_key: The matching_key for which to get the treatment
93+
:type matching_key: str
94+
95+
:param bucketing_key: The bucketing_key for which to get the treatment
96+
:type bucketing_key: str
97+
98+
:param attributes: An optional dictionary of attributes
99+
:type attributes: dict
100+
101+
:return: The treatment for the key and split
102+
:rtype: object
103+
"""
104+
# Fetching Split definition
105+
split = self._split_storage.get(feature)
106+
107+
# Calling evaluation
108+
evaluation = self._evaluate_treatment(feature, matching_key,
109+
bucketing_key, attributes, split)
110+
111+
return evaluation
112+
113+
def evaluate_features(self, features, matching_key, bucketing_key, attributes=None):
114+
"""
115+
Evaluate the user submitted data against multiple features and return the resulting
116+
treatment.
117+
118+
:param features: The features for which to get the treatments
119+
:type feature: list(str)
120+
121+
:param matching_key: The matching_key for which to get the treatment
122+
:type matching_key: str
123+
124+
:param bucketing_key: The bucketing_key for which to get the treatment
125+
:type bucketing_key: str
126+
127+
:param attributes: An optional dictionary of attributes
128+
:type attributes: dict
129+
130+
:return: The treatments for the key and splits
131+
:rtype: object
132+
"""
133+
evaluations = dict()
134+
135+
# Fetching Split definition
136+
splits = self._split_storage.fetch_many(features)
137+
# Calling evaluations
138+
for feature in features:
139+
split = splits[feature]
140+
evaluations[feature] = self._evaluate_treatment(feature, matching_key,
141+
bucketing_key, attributes, split)
142+
return evaluations
143+
return {
144+
feature: self._evaluate_treatment(feature, matching_key,
145+
bucketing_key, attributes, split)
146+
for (feature, split) in six.iteritems(self._split_storage.fetch_many(features))
147+
}
148+
86149
def _get_treatment_for_split(self, split, matching_key, bucketing_key, attributes=None):
87150
"""
88151
Evaluate the feature considering the conditions.

splitio/models/grammar/matchers/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def _match(self, key, attributes=None, context=None):
4141

4242
bucketing_key = context.get('bucketing_key')
4343

44-
result = evaluator.evaluate_treatment(self._split_name, key, bucketing_key, attributes)
44+
result = evaluator.evaluate_feature(self._split_name, key, bucketing_key, attributes)
4545
return result['treatment'] in self._treatments
4646

4747
def _add_matcher_specific_properties_to_json(self):

splitio/models/impressions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020

2121

22-
class Label(object): #pylint: disable=too-few-public-methods
22+
class Label(object): # pylint: disable=too-few-public-methods
2323
"""Impressions labels."""
2424

2525
# Condition: Split Was Killed

0 commit comments

Comments
 (0)