Skip to content

Commit e801d2c

Browse files
authored
Merge pull request #41 from splitio/development
Development
2 parents 6d8dd52 + 6241395 commit e801d2c

13 files changed

+1632
-52
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2.1.0
2+
- Added enabled labels
3+
- Added impressions by sdk and version including bucketing key
4+
2.0.5
5+
- Added SDK Factory Method with Manager API and Sdk Client
6+
- Added Bucketing key support

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
tests_require=tests_require,
2727
extras_require={
2828
'test': tests_require,
29-
'redis': ['redis>=2.6', 'jsonpickle>=0.9.3']
29+
'redis': ['redis>=2.10.5', 'jsonpickle>=0.9.3']
3030
},
3131
setup_requires=['nose'],
3232
classifiers=[

splitio/clients.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
from __future__ import absolute_import, division, print_function, unicode_literals
33

44
import logging
5-
6-
import arrow
5+
import time
76

87
from os.path import expanduser, join
98
from random import randint
@@ -37,12 +36,13 @@ def __init__(self, matching_key, bucketing_key):
3736
self.bucketing_key = bucketing_key
3837

3938
class Client(object):
40-
def __init__(self):
39+
def __init__(self, labels_enabled=True):
4140
"""Basic interface of a Client. Specific implementations need to override the
4241
get_split_fetcher method (and optionally the get_splitter method).
4342
"""
4443
self._logger = logging.getLogger(self.__class__.__name__)
4544
self._splitter = Splitter()
45+
self._labels_enabled = labels_enabled
4646

4747
def get_split_fetcher(self): # pragma: no cover
4848
"""Get the split fetcher implementation. Subclasses need to override this method.
@@ -89,20 +89,21 @@ def get_treatment(self, key, feature, attributes=None):
8989
if key is None or feature is None:
9090
return CONTROL
9191

92-
start = arrow.utcnow().timestamp * 1000
92+
start = int(round(time.time() * 1000))
9393

9494
matching_key = None
9595
bucketing_key = None
9696
if isinstance(key, Key):
9797
matching_key = key.matching_key
9898
bucketing_key = key.bucketing_key
9999
else:
100-
matching_key = key
101-
bucketing_key = key
100+
matching_key = str(key)
101+
bucketing_key = None
102102

103103
try:
104104
label = ''
105105
_treatment = CONTROL
106+
_change_number = -1
106107

107108
#Fetching Split definition
108109
split = self.get_split_fetcher().fetch(feature)
@@ -112,19 +113,20 @@ def get_treatment(self, key, feature, attributes=None):
112113
label = Label.SPLIT_NOT_FOUND
113114
_treatment = CONTROL
114115
else:
116+
_change_number = split.change_number
115117
if split.killed:
116118
label = Label.KILLED
117119
_treatment = split.default_treatment
118120
else:
119-
treatment = self._get_treatment_for_split(split, matching_key, bucketing_key, attributes)
121+
treatment, label = self._get_treatment_for_split(split, matching_key, bucketing_key, attributes)
120122
if treatment is None:
121123
label = Label.NO_CONDITION_MATCHED
122124
_treatment = split.default_treatment
123125
else:
124126
_treatment = treatment
125127

126128
impression = self._build_impression(matching_key, feature, _treatment, label,
127-
self.get_split_fetcher().change_number, bucketing_key, start)
129+
_change_number, bucketing_key, start)
128130
self._record_stats(impression, start, SDK_GET_TREATMENT)
129131
return _treatment
130132
except:
@@ -140,12 +142,16 @@ def get_treatment(self, key, feature, attributes=None):
140142
return CONTROL
141143

142144
def _build_impression(self, matching_key, feature_name, treatment, label, change_number, bucketing_key, time):
145+
146+
if not self._labels_enabled:
147+
label = None
148+
143149
return Impression(matching_key=matching_key, feature_name=feature_name, treatment=treatment, label=label,
144150
change_number=change_number, bucketing_key=bucketing_key, time=time)
145151

146152
def _record_stats(self, impression, start, operation):
147153
try:
148-
end = arrow.utcnow().timestamp * 1000
154+
end = int(round(time.time() * 1000))
149155
self.get_treatment_log().log(impression)
150156
self.get_metrics().time(operation, end - start)
151157
except:
@@ -164,13 +170,15 @@ def _get_treatment_for_split(self, split, matching_key, bucketing_key, attribute
164170
:return: The treatment for the key and split
165171
:rtype: str
166172
"""
173+
if bucketing_key is None:
174+
bucketing_key = matching_key
167175

168176
for condition in split.conditions:
169177
if condition.matcher.match(matching_key, attributes=attributes):
170-
return self.get_splitter().get_treatment(bucketing_key, split.seed, condition.partitions)
178+
return self.get_splitter().get_treatment(bucketing_key, split.seed, condition.partitions), condition.label
171179

172180
# No condition matches
173-
return None
181+
return None, None
174182

175183

176184
def randomize_interval(value):
@@ -214,7 +222,11 @@ def __init__(self, api_key, config=None, sdk_api_base_url=None, events_api_base_
214222
:param events_api_base_url: An override for the default events base URL.
215223
:type events_api_base_url: str
216224
"""
217-
super(SelfRefreshingClient, self).__init__()
225+
labels_enabled = True
226+
if config is not None and 'labelsEnabled' in config:
227+
labels_enabled = config['labelsEnabled']
228+
229+
super(SelfRefreshingClient, self).__init__(labels_enabled)
218230

219231
self._api_key = api_key
220232
self._sdk_api_base_url = sdk_api_base_url
@@ -484,11 +496,11 @@ def get_metrics(self):
484496

485497

486498
class RedisClient(Client):
487-
def __init__(self, redis):
499+
def __init__(self, redis, labels_enabled=True):
488500
"""A Client implementation that uses Redis as its backend.
489501
:param redis: A redis client
490502
:type redis: StrctRedis"""
491-
super(RedisClient, self).__init__()
503+
super(RedisClient, self).__init__(labels_enabled)
492504

493505
split_cache = RedisSplitCache(redis)
494506
split_fetcher = CacheBasedSplitFetcher(split_cache)
@@ -678,4 +690,12 @@ def get_redis_client(api_key, **kwargs):
678690
return LocalhostEnvironmentClient(**kwargs)
679691

680692
redis = get_redis(config)
681-
return RedisClient(redis)
693+
694+
if 'labelsEnabled' in config:
695+
redis_client = RedisClient(redis, config['labelsEnabled'])
696+
else:
697+
redis_client = RedisClient(redis)
698+
699+
return redis_client
700+
701+

splitio/config.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,26 @@
3131
'ready': 0,
3232
'redisHost': 'localhost',
3333
'redisPort': 6379,
34-
'redisDb': 0
34+
'redisDb': 0,
35+
'redisPassword': None,
36+
'redisSocketTimeout': None,
37+
'redisSocketConnectTimeout': None,
38+
'redisSocketKeepalive': None,
39+
'redisSocketKeepaliveOptions': None,
40+
'redisConnectionPool': None,
41+
'redisUnixSocketPath': None,
42+
'redisEncoding': 'utf-8',
43+
'redisEncodingErrors': 'strict',
44+
'redisCharset': None,
45+
'redisErrors': None,
46+
'redisDecodeResponses': False,
47+
'redisRetryOnTimeout': False,
48+
'redisSsl': False,
49+
'redisSslKeyfile': None,
50+
'redisSslCertfile': None,
51+
'redisSslCertReqs': None,
52+
'redisSslCaCerts': None,
53+
'redisMaxConnections': None
3554
}
3655

3756
MAX_INTERVAL = 180

splitio/impressions.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def build_impressions_data(impressions):
2929
'treatment': impression.treatment,
3030
'time': impression.time,
3131
'changeNumber': impression.change_number,
32-
'label': impression.label
32+
'label': impression.label,
33+
'bucketingKey': impression.bucketing_key
3334
}
3435
for impression in feature_impressions
3536
]
@@ -48,12 +49,12 @@ class Label(object):
4849
# Condition: No condition matched
4950
# Treatment: Default Treatment
5051
# Label: no condition matched
51-
NO_CONDITION_MATCHED = 'no condition matched'
52+
NO_CONDITION_MATCHED = 'no rule matched'
5253

5354
#Condition: Split definition was not found
5455
#Treatment: control
5556
#Label: split not found
56-
SPLIT_NOT_FOUND = 'split not found'
57+
SPLIT_NOT_FOUND = 'rules not found'
5758

5859
# Condition: There was an exception
5960
# Treatment: control

splitio/redis_support.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ def add_impression(self, impression):
340340
'treatment':impression.treatment,
341341
'time':impression.time,
342342
'changeNumber':impression.change_number,
343-
'label':impression.label
343+
'label':impression.label,
344+
'bucketingKey':impression.bucketing_key
344345
}
345346
self._redis.sadd(self._IMPRESSIONS_KEY.format(feature_name=impression.feature_name), encode(cache_impression))
346347

@@ -378,12 +379,16 @@ def fetch_all_and_clear(self):
378379
if 'changeNumber' in impression_decoded:
379380
change_number = impression_decoded['changeNumber']
380381

382+
bucketing_key = ''
383+
if 'bucketingKey' in impression_decoded:
384+
bucketing_key = impression_decoded['bucketingKey']
385+
381386
impression_tuple = Impression(matching_key=impression_decoded['keyName'],
382387
feature_name=feature_name,
383388
treatment=impression_decoded['treatment'],
384389
label=label,
385390
change_number=change_number,
386-
bucketing_key='',
391+
bucketing_key=bucketing_key,
387392
time=impression_decoded['time']
388393
)
389394
impressions_list.append(impression_tuple)
@@ -638,7 +643,7 @@ def __init__(self, segment_cache):
638643

639644
def _parse_split(self, split, block_until_ready=False):
640645
return RedisSplit(split['name'], split['seed'], split['killed'], split['defaultTreatment'],
641-
split['trafficTypeName'], segment_cache=self._segment_cache)
646+
split['trafficTypeName'], split['status'], split['changeNumber'], segment_cache=self._segment_cache)
642647

643648
def _parse_matcher_in_segment(self, partial_split, matcher, block_until_ready=False, *args,
644649
**kwargs):
@@ -650,7 +655,7 @@ def _parse_matcher_in_segment(self, partial_split, matcher, block_until_ready=Fa
650655

651656

652657
class RedisSplit(Split):
653-
def __init__(self, name, seed, killed, default_treatment, traffic_type_name, conditions=None, segment_cache=None):
658+
def __init__(self, name, seed, killed, default_treatment, traffic_type_name, status, change_number, conditions=None, segment_cache=None):
654659
"""A split implementation that mantains a reference to the segment cache so segments can
655660
be easily pickled and unpickled.
656661
:param name: Name of the feature
@@ -666,7 +671,7 @@ def __init__(self, name, seed, killed, default_treatment, traffic_type_name, con
666671
:param segment_cache: A segment cache
667672
:type segment_cache: SegmentCache
668673
"""
669-
super(RedisSplit, self).__init__(name, seed, killed, default_treatment, traffic_type_name, conditions)
674+
super(RedisSplit, self).__init__(name, seed, killed, default_treatment, traffic_type_name, status, change_number, conditions)
670675
self._segment_cache = segment_cache
671676

672677
@property
@@ -710,7 +715,7 @@ def get_redis(config):
710715
:return: A redis client
711716
"""
712717
if 'redisFactory' in config:
713-
redis_factory = import_from_string(config['redisFactory'])
718+
redis_factory = import_from_string(config['redisFactory'],'redisFactory')
714719
return redis_factory()
715720

716721
return default_redis_factory(config)
@@ -726,5 +731,34 @@ def default_redis_factory(config):
726731
host = config.get('redisHost', 'localhost')
727732
port = config.get('redisPort', 6379)
728733
db = config.get('redisDb', 0)
729-
redis = StrictRedis(host=host, port=port, db=db)
734+
password = config.get('redisPassword', None)
735+
socket_timeout = config.get('redisSocketTimeout', None)
736+
socket_connect_timeout = config.get('redisSocketConnectTimeout', None)
737+
socket_keepalive = config.get('redisSocketKeepalive', None)
738+
socket_keepalive_options = config.get('redisSocketKeepaliveOptions', None)
739+
connection_pool = config.get('redisConnectionPool', None)
740+
unix_socket_path = config.get('redisUnixSocketPath', None)
741+
encoding = config.get('redisEncoding', 'utf-8')
742+
encoding_errors = config.get('redisEncodingErrors', 'strict')
743+
charset = config.get('redisCharset', None)
744+
errors = config.get('redisErrors', None)
745+
decode_responses = config.get('redisDecodeResponses', False)
746+
retry_on_timeout = config.get('redisRetryOnTimeout', False)
747+
ssl = config.get('redisSsl', False)
748+
ssl_keyfile = config.get('redisSslKeyfile', None)
749+
ssl_certfile = config.get('redisSslCertfile', None)
750+
ssl_cert_reqs = config.get('redisSslCertReqs', None)
751+
ssl_ca_certs = config.get('redisSslCaCerts', None)
752+
max_connections = config.get('redisMaxConnections', None)
753+
754+
redis = StrictRedis(host=host, port=port, db=db, password=password, socket_timeout=socket_timeout,
755+
socket_connect_timeout=socket_connect_timeout,
756+
socket_keepalive=socket_keepalive, socket_keepalive_options=socket_keepalive_options,
757+
connection_pool=connection_pool, unix_socket_path=unix_socket_path,
758+
encoding=encoding, encoding_errors=encoding_errors,
759+
charset=charset, errors=errors,
760+
decode_responses=decode_responses, retry_on_timeout=retry_on_timeout,
761+
ssl=ssl, ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile,
762+
ssl_cert_reqs=ssl_cert_reqs, ssl_ca_certs=ssl_ca_certs,
763+
max_connections=max_connections)
730764
return redis

0 commit comments

Comments
 (0)