Skip to content

Commit 7a0c80e

Browse files
authored
prepare 6.0.0 release (#83)
1 parent 768329f commit 7a0c80e

23 files changed

+1514
-596
lines changed

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,9 @@ Your first feature flag
3737
else:
3838
# the code to run if the feature is off
3939

40-
Python 2.6
40+
Supported Python versions
4141
----------
42-
Python 2.6 requires an extra dependency. Here's how to set it up:
43-
44-
1. Use the `python2.6` extra in your requirements.txt:
45-
`ldclient-py[python2.6]`
42+
The SDK is tested with the most recent patch releases of Python 2.7, 3.3, 3.4, 3.5, and 3.6. Python 2.6 is no longer supported.
4643

4744
Learn more
4845
-----------

ldclient/client.py

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import hashlib
44
import hmac
55
import threading
6-
import time
76

87
import requests
98
from builtins import object
109

1110
from ldclient.config import Config as Config
11+
from ldclient.event_processor import NullEventProcessor
1212
from ldclient.feature_requester import FeatureRequesterImpl
1313
from ldclient.flag import evaluate
1414
from ldclient.polling import PollingUpdateProcessor
@@ -21,7 +21,7 @@
2121
import queue
2222
except:
2323
# noinspection PyUnresolvedReferences,PyPep8Naming
24-
import Queue as queue
24+
import Queue as queue # Python 3
2525

2626
from cachecontrol import CacheControl
2727
from threading import Lock
@@ -43,46 +43,46 @@ def __init__(self, sdk_key=None, config=None, start_wait=5):
4343
self._config._validate()
4444

4545
self._session = CacheControl(requests.Session())
46-
self._queue = queue.Queue(self._config.events_max_pending)
47-
self._event_consumer = None
46+
self._event_processor = None
4847
self._lock = Lock()
4948

5049
self._store = self._config.feature_store
5150
""" :type: FeatureStore """
5251

52+
if self._config.offline or not self._config.send_events:
53+
self._event_processor = NullEventProcessor()
54+
else:
55+
self._event_processor = self._config.event_processor_class(self._config)
56+
5357
if self._config.offline:
5458
log.info("Started LaunchDarkly Client in offline mode")
5559
return
5660

57-
if self._config.send_events:
58-
self._event_consumer = self._config.event_consumer_class(self._queue, self._config)
59-
self._event_consumer.start()
60-
6161
if self._config.use_ldd:
6262
log.info("Started LaunchDarkly Client in LDD mode")
6363
return
6464

65-
if self._config.feature_requester_class:
66-
self._feature_requester = self._config.feature_requester_class(self._config)
67-
else:
68-
self._feature_requester = FeatureRequesterImpl(self._config)
69-
""" :type: FeatureRequester """
70-
7165
update_processor_ready = threading.Event()
7266

7367
if self._config.update_processor_class:
7468
log.info("Using user-specified update processor: " + str(self._config.update_processor_class))
7569
self._update_processor = self._config.update_processor_class(
76-
self._config, self._feature_requester, self._store, update_processor_ready)
70+
self._config, self._store, update_processor_ready)
7771
else:
72+
if self._config.feature_requester_class:
73+
feature_requester = self._config.feature_requester_class(self._config)
74+
else:
75+
feature_requester = FeatureRequesterImpl(self._config)
76+
""" :type: FeatureRequester """
77+
7878
if self._config.stream:
7979
self._update_processor = StreamingUpdateProcessor(
80-
self._config, self._feature_requester, self._store, update_processor_ready)
80+
self._config, feature_requester, self._store, update_processor_ready)
8181
else:
8282
log.info("Disabling streaming API")
8383
log.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support")
8484
self._update_processor = PollingUpdateProcessor(
85-
self._config, self._feature_requester, self._store, update_processor_ready)
85+
self._config, feature_requester, self._store, update_processor_ready)
8686
""" :type: UpdateProcessor """
8787

8888
self._update_processor.start()
@@ -102,19 +102,13 @@ def close(self):
102102
log.info("Closing LaunchDarkly client..")
103103
if self.is_offline():
104104
return
105-
if self._event_consumer and self._event_consumer.is_alive():
106-
self._event_consumer.stop()
105+
if self._event_processor:
106+
self._event_processor.stop()
107107
if self._update_processor and self._update_processor.is_alive():
108108
self._update_processor.stop()
109109

110110
def _send_event(self, event):
111-
if self._config.offline or not self._config.send_events:
112-
return
113-
event['creationDate'] = int(time.time() * 1000)
114-
if self._queue.full():
115-
log.warning("Event queue is full-- dropped an event")
116-
else:
117-
self._queue.put(event)
111+
self._event_processor.send_event(event)
118112

119113
def track(self, event_name, user, data=None):
120114
self._sanitize_user(user)
@@ -135,24 +129,26 @@ def is_initialized(self):
135129
return self.is_offline() or self._config.use_ldd or self._update_processor.initialized()
136130

137131
def flush(self):
138-
if self._config.offline or not self._config.send_events:
132+
if self._config.offline:
139133
return
140-
return self._event_consumer.flush()
134+
return self._event_processor.flush()
141135

142136
def toggle(self, key, user, default):
143137
log.warn("Deprecated method: toggle() called. Use variation() instead.")
144138
return self.variation(key, user, default)
145139

146140
def variation(self, key, user, default):
147141
default = self._config.get_default(key, default)
148-
self._sanitize_user(user)
142+
if user is not None:
143+
self._sanitize_user(user)
149144

150145
if self._config.offline:
151146
return default
152147

153148
def send_event(value, version=None):
154-
self._send_event({'kind': 'feature', 'key': key,
155-
'user': user, 'value': value, 'default': default, 'version': version})
149+
self._send_event({'kind': 'feature', 'key': key, 'user': user, 'variation': None,
150+
'value': value, 'default': default, 'version': version,
151+
'trackEvents': False, 'debugEventsUntilDate': None})
156152

157153
if not self.is_initialized():
158154
if self._store.initialized:
@@ -163,12 +159,7 @@ def send_event(value, version=None):
163159
send_event(default)
164160
return default
165161

166-
if user is None or user.get('key') is None:
167-
log.warn("Missing user or user key when evaluating Feature Flag key: " + key + ". Returning default.")
168-
send_event(default)
169-
return default
170-
171-
if user.get('key', "") == "":
162+
if user is not None and user.get('key', "") == "":
172163
log.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.")
173164

174165
def cb(flag):
@@ -182,6 +173,7 @@ def cb(flag):
182173

183174
except Exception as e:
184175
log.error("Exception caught in variation: " + e.message + " for flag key: " + key + " and user: " + str(user))
176+
send_event(default)
185177

186178
return default
187179

@@ -191,14 +183,22 @@ def _evaluate(self, flag, user):
191183
return evaluate(flag, user, self._store)
192184

193185
def _evaluate_and_send_events(self, flag, user, default):
194-
value, events = self._evaluate(flag, user)
195-
for event in events or []:
196-
self._send_event(event)
197-
198-
if value is None:
186+
if user is None or user.get('key') is None:
187+
log.warn("Missing user or user key when evaluating Feature Flag key: " + flag.get('key') + ". Returning default.")
199188
value = default
189+
variation = None
190+
else:
191+
result = evaluate(flag, user, self._store)
192+
for event in result.events or []:
193+
self._send_event(event)
194+
value = default if result.value is None else result.value
195+
variation = result.variation
196+
200197
self._send_event({'kind': 'feature', 'key': flag.get('key'),
201-
'user': user, 'value': value, 'default': default, 'version': flag.get('version')})
198+
'user': user, 'variation': variation, 'value': value,
199+
'default': default, 'version': flag.get('version'),
200+
'trackEvents': flag.get('trackEvents'),
201+
'debugEventsUntilDate': flag.get('debugEventsUntilDate')})
202202
return value
203203

204204
def all_flags(self, user):
@@ -227,7 +227,7 @@ def cb(all_flags):
227227
return self._store.all(FEATURES, cb)
228228

229229
def _evaluate_multi(self, user, flags):
230-
return dict([(k, self._evaluate(v, user)[0]) for k, v in flags.items() or {}])
230+
return dict([(k, self._evaluate(v, user).value) for k, v in flags.items() or {}])
231231

232232
def secure_mode_hash(self, user):
233233
if user.get('key') is None or self._config.sdk_key is None:

ldclient/config.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ldclient.event_consumer import EventConsumerImpl
1+
from ldclient.event_processor import DefaultEventProcessor
22
from ldclient.feature_store import InMemoryFeatureStore
33
from ldclient.util import log
44

@@ -13,8 +13,8 @@ def __init__(self,
1313
events_uri='https://events.launchdarkly.com',
1414
connect_timeout=10,
1515
read_timeout=15,
16-
events_upload_max_batch_size=100,
1716
events_max_pending=10000,
17+
flush_interval=5,
1818
stream_uri='https://stream.launchdarkly.com',
1919
stream=True,
2020
verify_ssl=True,
@@ -26,10 +26,13 @@ def __init__(self,
2626
use_ldd=False,
2727
feature_store=InMemoryFeatureStore(),
2828
feature_requester_class=None,
29-
event_consumer_class=None,
29+
event_processor_class=None,
3030
private_attribute_names=(),
3131
all_attributes_private=False,
32-
offline=False):
32+
offline=False,
33+
user_keys_capacity=1000,
34+
user_keys_flush_interval=300,
35+
inline_users_in_events=False):
3336
"""
3437
:param string sdk_key: The SDK key for your LaunchDarkly account.
3538
:param string base_uri: The base URL for the LaunchDarkly server. Most users should use the default
@@ -43,6 +46,8 @@ def __init__(self,
4346
:param int events_max_pending: The capacity of the events buffer. The client buffers up to this many
4447
events in memory before flushing. If the capacity is exceeded before the buffer is flushed, events
4548
will be discarded.
49+
: param float flush_interval: The number of seconds in between flushes of the events buffer. Decreasing
50+
the flush interval means that the event buffer is less likely to reach capacity.
4651
:param string stream_uri: The URL for the LaunchDarkly streaming events server. Most users should
4752
use the default value.
4853
:param bool stream: Whether or not the streaming API should be used to receive flag updates. By
@@ -66,10 +71,17 @@ def __init__(self,
6671
private, not just the attributes specified in `private_attribute_names`.
6772
:param feature_store: A FeatureStore implementation
6873
:type feature_store: FeatureStore
74+
:param int user_keys_capacity: The number of user keys that the event processor can remember at any
75+
one time, so that duplicate user details will not be sent in analytics events.
76+
:param float user_keys_flush_interval: The interval in seconds at which the event processor will
77+
reset its set of known user keys.
78+
:param bool inline_users_in_events: Whether to include full user details in every analytics event.
79+
By default, events will only include the user key, except for one "index" event that provides the
80+
full details for the user.
6981
:param feature_requester_class: A factory for a FeatureRequester implementation taking the sdk key and config
7082
:type feature_requester_class: (str, Config, FeatureStore) -> FeatureRequester
71-
:param event_consumer_class: A factory for an EventConsumer implementation taking the event queue, sdk key, and config
72-
:type event_consumer_class: (queue.Queue, str, Config) -> EventConsumer
83+
:param event_processor_class: A factory for an EventProcessor implementation taking the config
84+
:type event_processor_class: (Config) -> EventProcessor
7385
:param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key,
7486
config, and FeatureStore implementation
7587
"""
@@ -86,12 +98,12 @@ def __init__(self,
8698
self.__poll_interval = max(poll_interval, 30)
8799
self.__use_ldd = use_ldd
88100
self.__feature_store = InMemoryFeatureStore() if not feature_store else feature_store
89-
self.__event_consumer_class = EventConsumerImpl if not event_consumer_class else event_consumer_class
101+
self.__event_processor_class = DefaultEventProcessor if not event_processor_class else event_processor_class
90102
self.__feature_requester_class = feature_requester_class
91103
self.__connect_timeout = connect_timeout
92104
self.__read_timeout = read_timeout
93-
self.__events_upload_max_batch_size = events_upload_max_batch_size
94105
self.__events_max_pending = events_max_pending
106+
self.__flush_interval = flush_interval
95107
self.__verify_ssl = verify_ssl
96108
self.__defaults = defaults
97109
if offline is True:
@@ -100,6 +112,9 @@ def __init__(self,
100112
self.__private_attribute_names = private_attribute_names
101113
self.__all_attributes_private = all_attributes_private
102114
self.__offline = offline
115+
self.__user_keys_capacity = user_keys_capacity
116+
self.__user_keys_flush_interval = user_keys_flush_interval
117+
self.__inline_users_in_events = inline_users_in_events
103118

104119
@classmethod
105120
def default(cls):
@@ -111,8 +126,8 @@ def copy_with_new_sdk_key(self, new_sdk_key):
111126
events_uri=self.__events_uri,
112127
connect_timeout=self.__connect_timeout,
113128
read_timeout=self.__read_timeout,
114-
events_upload_max_batch_size=self.__events_upload_max_batch_size,
115129
events_max_pending=self.__events_max_pending,
130+
flush_interval=self.__flush_interval,
116131
stream_uri=self.__stream_uri,
117132
stream=self.__stream,
118133
verify_ssl=self.__verify_ssl,
@@ -123,10 +138,13 @@ def copy_with_new_sdk_key(self, new_sdk_key):
123138
use_ldd=self.__use_ldd,
124139
feature_store=self.__feature_store,
125140
feature_requester_class=self.__feature_requester_class,
126-
event_consumer_class=self.__event_consumer_class,
141+
event_processor_class=self.__event_processor_class,
127142
private_attribute_names=self.__private_attribute_names,
128143
all_attributes_private=self.__all_attributes_private,
129-
offline=self.__offline)
144+
offline=self.__offline,
145+
user_keys_capacity=self.__user_keys_capacity,
146+
user_keys_flush_interval=self.__user_keys_flush_interval,
147+
inline_users_in_events=self.__inline_users_in_events)
130148

131149
def get_default(self, key, default):
132150
return default if key not in self.__defaults else self.__defaults[key]
@@ -176,8 +194,8 @@ def feature_store(self):
176194
return self.__feature_store
177195

178196
@property
179-
def event_consumer_class(self):
180-
return self.__event_consumer_class
197+
def event_processor_class(self):
198+
return self.__event_processor_class
181199

182200
@property
183201
def feature_requester_class(self):
@@ -199,14 +217,14 @@ def events_enabled(self):
199217
def send_events(self):
200218
return self.__send_events
201219

202-
@property
203-
def events_upload_max_batch_size(self):
204-
return self.__events_upload_max_batch_size
205-
206220
@property
207221
def events_max_pending(self):
208222
return self.__events_max_pending
209223

224+
@property
225+
def flush_interval(self):
226+
return self.__flush_interval
227+
210228
@property
211229
def verify_ssl(self):
212230
return self.__verify_ssl
@@ -223,6 +241,18 @@ def all_attributes_private(self):
223241
def offline(self):
224242
return self.__offline
225243

244+
@property
245+
def user_keys_capacity(self):
246+
return self.__user_keys_capacity
247+
248+
@property
249+
def user_keys_flush_interval(self):
250+
return self.__user_keys_flush_interval
251+
252+
@property
253+
def inline_users_in_events(self):
254+
return self.__inline_users_in_events
255+
226256
def _validate(self):
227257
if self.offline is False and self.sdk_key is None or self.sdk_key is '':
228258
log.warn("Missing or blank sdk_key.")

0 commit comments

Comments
 (0)