Skip to content

Commit b15f769

Browse files
authored
Merge pull request #162 from splitio/development
Development
2 parents 0bbd418 + a3ea2d1 commit b15f769

File tree

11 files changed

+205
-167
lines changed

11 files changed

+205
-167
lines changed

.travis.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
language: python
22

3-
python:
4-
- "2.7"
5-
- "3.6"
3+
git:
4+
depth: false
5+
6+
matrix:
7+
include:
8+
- python: '2.7'
9+
- python: '3.6'
10+
after_success:
11+
- bash sonar-scanner.sh
612

713
services:
814
- redis-server
915

16+
addons:
17+
sonarqube: true
18+
1019
install:
1120
- pip install -U setuptools pip
1221
- pip install -e .[cpphash,redis,uwsgi]

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
8.1.6 (Oct 31, 2019)
2+
- Fixed input validation performance issue.
3+
14
8.1.5 (Oct 15, 2019)
25
- Added logic to fetch multiple splits at once on get_treatments/get_treatments_with_config.
36
- Added flag `IPAddressesEnabled` into config to enable/disable sending machineName and machineIp when data is posted in headers.

sonar-scanner.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#/bin/bash -e
2+
3+
sonar_scanner() {
4+
local params=$@
5+
6+
sonar-scanner \
7+
-Dsonar.host.url='https://sonarqube.split-internal.com' \
8+
-Dsonar.login="$SONAR_TOKEN" \
9+
-Dsonar.ws.timeout='300' \
10+
-Dsonar.sources='splitio' \
11+
-Dsonar.projectName='python-client' \
12+
-Dsonar.projectKey='python-client' \
13+
-Dsonar.links.ci='https://travis-ci.com/splitio/python-client' \
14+
-Dsonar.links.scm='https://github.com/splitio/python-client' \
15+
${params}
16+
17+
return $?
18+
}
19+
20+
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
21+
sonar_scanner \
22+
-Dsonar.pullrequest.provider='GitHub' \
23+
-Dsonar.pullrequest.github.repository='splitio/python-client' \
24+
-Dsonar.pullrequest.key=$TRAVIS_PULL_REQUEST \
25+
-Dsonar.pullrequest.branch=$TRAVIS_PULL_REQUEST_BRANCH \
26+
-Dsonar.pullrequest.base=$TRAVIS_BRANCH
27+
else
28+
if [ "$TRAVIS_BRANCH" == 'master' ]; then
29+
sonar_scanner \
30+
-Dsonar.branch.name=$TRAVIS_BRANCH
31+
else
32+
if [ "$TRAVIS_BRANCH" == 'development' ]; then
33+
TARGET_BRANCH='master'
34+
else
35+
TARGET_BRANCH='development'
36+
fi
37+
sonar_scanner \
38+
-Dsonar.branch.name=$TRAVIS_BRANCH \
39+
-Dsonar.branch.target=$TARGET_BRANCH
40+
fi
41+
fi

splitio/client/client.py

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
1919

2020
_METRIC_GET_TREATMENT = 'sdk.getTreatment'
2121
_METRIC_GET_TREATMENTS = 'sdk.getTreatments'
22+
_METRIC_GET_TREATMENT_WITH_CONFIG = 'sdk.getTreatmentWithConfig'
23+
_METRIC_GET_TREATMENTS_WITH_CONFIG = 'sdk.getTreatmentsWithConfig'
2224

2325
def __init__(self, factory, labels_enabled=True, impression_listener=None):
2426
"""
@@ -103,39 +105,25 @@ def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=No
103105
attributes
104106
)
105107

106-
def get_treatment_with_config(self, key, feature, attributes=None):
107-
"""
108-
Get the treatment and config for a feature and key, with optional dictionary of attributes.
109-
110-
This method never raises an exception. If there's a problem, the appropriate log message
111-
will be generated and the method will return the CONTROL treatment.
112-
113-
:param key: The key for which to get the treatment
114-
:type key: str
115-
:param feature: The name of the feature for which to get the treatment
116-
:type feature: str
117-
:param attributes: An optional dictionary of attributes
118-
:type attributes: dict
119-
:return: The treatment for the key and feature
120-
:rtype: tuple(str, str)
121-
"""
108+
def _make_evaluation(self, key, feature, attributes, method_name, metric_name):
122109
try:
123110
if self.destroyed:
124111
self._logger.error("Client has already been destroyed - no calls possible")
125112
return CONTROL, None
126113

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

129-
matching_key, bucketing_key = input_validator.validate_key(key)
116+
matching_key, bucketing_key = input_validator.validate_key(key, method_name)
130117
feature = input_validator.validate_feature_name(
131118
feature,
132119
self.ready,
133-
self._factory._get_storage('splits') # pylint: disable=protected-access
120+
self._factory._get_storage('splits'), # pylint: disable=protected-access
121+
method_name
134122
)
135123

136124
if (matching_key is None and bucketing_key is None) \
137125
or feature is None \
138-
or not input_validator.validate_attributes(attributes):
126+
or not input_validator.validate_attributes(attributes, method_name):
139127
return CONTROL, None
140128

141129
result = self._evaluate_if_ready(matching_key, bucketing_key, feature, attributes)
@@ -150,7 +138,7 @@ def get_treatment_with_config(self, key, feature, attributes=None):
150138
start
151139
)
152140

153-
self._record_stats(impression, start, self._METRIC_GET_TREATMENT)
141+
self._record_stats([impression], start, metric_name)
154142
self._send_impression_to_listener(impression, attributes)
155143
return result['treatment'], result['configurations']
156144
except Exception: # pylint: disable=broad-except
@@ -166,80 +154,29 @@ def get_treatment_with_config(self, key, feature, attributes=None):
166154
bucketing_key,
167155
start
168156
)
169-
self._record_stats(impression, start, self._METRIC_GET_TREATMENT)
157+
self._record_stats([impression], start, metric_name)
170158
self._send_impression_to_listener(impression, attributes)
171159
except Exception: # pylint: disable=broad-except
172160
self._logger.error('Error reporting impression into get_treatment exception block')
173161
self._logger.debug('Error: ', exc_info=True)
174162
return CONTROL, None
175163

176-
def get_treatment(self, key, feature, attributes=None):
177-
"""
178-
Get the treatment for a feature and key, with an optional dictionary of attributes.
179-
180-
This method never raises an exception. If there's a problem, the appropriate log message
181-
will be generated and the method will return the CONTROL treatment.
182-
183-
:param key: The key for which to get the treatment
184-
:type key: str
185-
:param feature: The name of the feature for which to get the treatment
186-
:type feature: str
187-
:param attributes: An optional dictionary of attributes
188-
:type attributes: dict
189-
:return: The treatment for the key and feature
190-
:rtype: str
191-
"""
192-
treatment, _ = self.get_treatment_with_config(key, feature, attributes)
193-
return treatment
194-
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-
213-
def get_treatments_with_config(self, key, features, attributes=None):
214-
"""
215-
Evaluate multiple features and return a dict with feature -> (treatment, config).
216-
217-
Get the treatments for a list of features considering a key, with an optional dictionary of
218-
attributes. This method never raises an exception. If there's a problem, the appropriate
219-
log message will be generated and the method will return the CONTROL treatment.
220-
:param key: The key for which to get the treatment
221-
:type key: str
222-
:param features: Array of the names of the features for which to get the treatment
223-
:type feature: list
224-
:param attributes: An optional dictionary of attributes
225-
:type attributes: dict
226-
:return: Dictionary with the result of all the features provided
227-
:rtype: dict
228-
"""
164+
def _make_evaluations(self, key, features, attributes, method_name, metric_name):
229165
if self.destroyed:
230166
self._logger.error("Client has already been destroyed - no calls possible")
231-
return input_validator.generate_control_treatments(features)
167+
return input_validator.generate_control_treatments(features, method_name)
232168

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

235-
matching_key, bucketing_key = input_validator.validate_key(key)
171+
matching_key, bucketing_key = input_validator.validate_key(key, method_name)
236172
if matching_key is None and bucketing_key is None:
237-
return input_validator.generate_control_treatments(features)
173+
return input_validator.generate_control_treatments(features, method_name)
238174

239-
if input_validator.validate_attributes(attributes) is False:
240-
return input_validator.generate_control_treatments(features)
175+
if input_validator.validate_attributes(attributes, method_name) is False:
176+
return input_validator.generate_control_treatments(features, method_name)
241177

242178
features, missing = input_validator.validate_features_get_treatments(
179+
method_name,
243180
features,
244181
self.ready,
245182
self._factory._get_storage('splits') # pylint: disable=protected-access
@@ -269,8 +206,8 @@ def get_treatments_with_config(self, key, features, attributes=None):
269206
treatments[feature] = (result['treatment'], result['configurations'])
270207

271208
except Exception: # pylint: disable=broad-except
272-
self._logger.error('get_treatments: An exception occured when evaluating '
273-
'feature ' + feature + ' returning CONTROL.')
209+
self._logger.error('%s: An exception occured when evaluating '
210+
'feature %s returning CONTROL.' % (method_name, feature))
274211
treatments[feature] = CONTROL, None
275212
self._logger.debug('Error: ', exc_info=True)
276213
continue
@@ -282,15 +219,91 @@ def get_treatments_with_config(self, key, features, attributes=None):
282219
for impression in bulk_impressions:
283220
self._send_impression_to_listener(impression, attributes)
284221
except Exception: # pylint: disable=broad-except
285-
self._logger.error('get_treatments: An exception when trying to store '
286-
'impressions.')
222+
self._logger.error('%s: An exception when trying to store '
223+
'impressions.' % method_name)
287224
self._logger.debug('Error: ', exc_info=True)
288225

289226
return treatments
290227
except Exception: # pylint: disable=broad-except
291228
self._logger.error('Error getting treatment for features')
292229
self._logger.debug('Error: ', exc_info=True)
293-
return input_validator.generate_control_treatments(list(features))
230+
return input_validator.generate_control_treatments(list(features), method_name)
231+
232+
def _evaluate_features_if_ready(self, matching_key, bucketing_key, features, attributes=None):
233+
if not self.ready:
234+
return {
235+
feature: {
236+
'treatment': CONTROL,
237+
'configurations': None,
238+
'impression': {'label': Label.NOT_READY, 'change_number': None}
239+
}
240+
for feature in features
241+
}
242+
243+
return self._evaluator.evaluate_features(
244+
features,
245+
matching_key,
246+
bucketing_key,
247+
attributes
248+
)
249+
250+
def get_treatment_with_config(self, key, feature, attributes=None):
251+
"""
252+
Get the treatment and config for a feature and key, with optional dictionary of attributes.
253+
254+
This method never raises an exception. If there's a problem, the appropriate log message
255+
will be generated and the method will return the CONTROL treatment.
256+
257+
:param key: The key for which to get the treatment
258+
:type key: str
259+
:param feature: The name of the feature for which to get the treatment
260+
:type feature: str
261+
:param attributes: An optional dictionary of attributes
262+
:type attributes: dict
263+
:return: The treatment for the key and feature
264+
:rtype: tuple(str, str)
265+
"""
266+
return self._make_evaluation(key, feature, attributes, 'get_treatment_with_config',
267+
self._METRIC_GET_TREATMENT_WITH_CONFIG)
268+
269+
def get_treatment(self, key, feature, attributes=None):
270+
"""
271+
Get the treatment for a feature and key, with an optional dictionary of attributes.
272+
273+
This method never raises an exception. If there's a problem, the appropriate log message
274+
will be generated and the method will return the CONTROL treatment.
275+
276+
:param key: The key for which to get the treatment
277+
:type key: str
278+
:param feature: The name of the feature for which to get the treatment
279+
:type feature: str
280+
:param attributes: An optional dictionary of attributes
281+
:type attributes: dict
282+
:return: The treatment for the key and feature
283+
:rtype: str
284+
"""
285+
treatment, _ = self._make_evaluation(key, feature, attributes, 'get_treatment',
286+
self._METRIC_GET_TREATMENT)
287+
return treatment
288+
289+
def get_treatments_with_config(self, key, features, attributes=None):
290+
"""
291+
Evaluate multiple features and return a dict with feature -> (treatment, config).
292+
293+
Get the treatments for a list of features considering a key, with an optional dictionary of
294+
attributes. This method never raises an exception. If there's a problem, the appropriate
295+
log message will be generated and the method will return the CONTROL treatment.
296+
:param key: The key for which to get the treatment
297+
:type key: str
298+
:param features: Array of the names of the features for which to get the treatment
299+
:type feature: list
300+
:param attributes: An optional dictionary of attributes
301+
:type attributes: dict
302+
:return: Dictionary with the result of all the features provided
303+
:rtype: dict
304+
"""
305+
return self._make_evaluations(key, features, attributes, 'get_treatments_with_config',
306+
self._METRIC_GET_TREATMENTS_WITH_CONFIG)
294307

295308
def get_treatments(self, key, features, attributes=None):
296309
"""
@@ -308,7 +321,8 @@ def get_treatments(self, key, features, attributes=None):
308321
:return: Dictionary with the result of all the features provided
309322
:rtype: dict
310323
"""
311-
with_config = self.get_treatments_with_config(key, features, attributes)
324+
with_config = self._make_evaluations(key, features, attributes, 'get_treatments',
325+
self._METRIC_GET_TREATMENTS)
312326
return {feature: result[0] for (feature, result) in six.iteritems(with_config)}
313327

314328
def _build_impression( # pylint: disable=too-many-arguments
@@ -346,10 +360,7 @@ def _record_stats(self, impressions, start, operation):
346360
"""
347361
try:
348362
end = int(round(time.time() * 1000))
349-
if operation == self._METRIC_GET_TREATMENT:
350-
self._impressions_storage.put([impressions])
351-
else:
352-
self._impressions_storage.put(impressions)
363+
self._impressions_storage.put(impressions)
353364
self._telemetry_storage.inc_latency(operation, get_latency_bucket_index(end - start))
354365
except Exception: # pylint: disable=broad-except
355366
self._logger.error('Error recording impressions and metrics')

0 commit comments

Comments
 (0)