Skip to content

Commit 2125c5a

Browse files
authored
Merge pull request #64 from launchdarkly/dr/changeSdkKey
Allow SDK key/config to be changed after client has been initialized.
2 parents 9cdce32 + 62ecd42 commit 2125c5a

21 files changed

+629
-152
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to the LaunchDarkly Python SDK will be documented in this fi
44

55
## [3.0.3] - 2016-11-03
66
### Changed
7-
- Add backoff when retryign stream connection.
7+
- Add backoff when retrying stream connection.
88
- More correct initialized state.
99

1010
## [3.0.2] - 2016-10-26

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ Development information (for developing this module itself)
1818
pip install -r test-requirements.txt
1919

2020
1. Run tests: You'll need redis running locally on its default port of 6379.
21-
22-
$ py.test testing
21+
1. If you want integration tests to run, set the ```LD_SDK_KEY``` environment variable to a valid production SDK Key.
22+
1. ```$ py.test testing```
2323

2424
Developing with different python versions
2525
-----------------------------------------

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ Quick setup
1616
2. Configure the library with your sdk key:
1717

1818
import ldclient
19-
ldclient.sdk_key = "your sdk key"
2019

2120
3. Get the client:
2221

22+
ldclient.set_sdk_key("your sdk key")
2323
client = ldclient.get()
2424

2525
Your first feature flag
@@ -40,7 +40,7 @@ Twisted is supported for LDD mode only. To run in Twisted/LDD mode,
4040
1. Use this dependency:
4141

4242
```
43-
ldclient-py[twisted]==3.0.1
43+
ldclient-py[twisted]>=3.0.1
4444
```
4545
2. Configure the client:
4646

demo/demo.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
root.addHandler(ch)
1616

1717
if __name__ == '__main__':
18-
ldclient.sdk_key = 'sdk_key'
1918
ldclient.start_wait = 10
20-
client = ldclient.get()
19+
ldclient.set_sdk_key('YOUR_SDK_KEY')
2120

2221
user = {u'key': 'userKey'}
23-
print(client.variation("update-app", user, False))
22+
print(ldclient.get().variation("update-app", user, False))
2423

25-
client.close()
24+
ldclient.get().close()

ldclient/__init__.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,92 @@
1212
__BUILTINS__ = ["key", "ip", "country", "email",
1313
"firstName", "lastName", "avatar", "name", "anonymous"]
1414

15-
1615
"""Settings."""
17-
client = None
18-
sdk_key = None
1916
start_wait = 5
20-
config = Config()
2117

22-
_lock = ReadWriteLock()
18+
__client = None
19+
__config = Config()
20+
__lock = ReadWriteLock()
21+
22+
23+
# 2 Use Cases:
24+
# 1. Initial setup: sets the config for the uninitialized client
25+
# 2. Allows on-the-fly changing of the config. When this function is called after the client has been initialized
26+
# the client will get re-initialized with the new config. In order for this to work, the return value of
27+
# ldclient.get() should never be assigned
28+
def set_config(config):
29+
global __config
30+
global __client
31+
global __lock
32+
try:
33+
__lock.lock()
34+
if __client:
35+
log.info("Reinitializing LaunchDarkly Client " + version.VERSION + " with new config")
36+
new_client = LDClient(config=config, start_wait=start_wait)
37+
old_client = __client
38+
__client = new_client
39+
old_client.close()
40+
finally:
41+
__config = config
42+
__lock.unlock()
43+
44+
45+
# 2 Use Cases:
46+
# 1. Initial setup: sets the sdk key for the uninitialized client
47+
# 2. Allows on-the-fly changing of the sdk key. When this function is called after the client has been initialized
48+
# the client will get re-initialized with the new sdk key. In order for this to work, the return value of
49+
# ldclient.get() should never be assigned
50+
def set_sdk_key(sdk_key):
51+
global __config
52+
global __client
53+
global __lock
54+
sdk_key_changed = False
55+
try:
56+
__lock.rlock()
57+
if sdk_key is __config.sdk_key:
58+
log.info("New sdk_key is the same as the existing one. doing nothing.")
59+
else:
60+
sdk_key_changed = True
61+
finally:
62+
__lock.runlock()
63+
64+
if sdk_key_changed:
65+
try:
66+
__lock.lock()
67+
__config = __config.copy_with_new_sdk_key(new_sdk_key=sdk_key)
68+
if __client:
69+
log.info("Reinitializing LaunchDarkly Client " + version.VERSION + " with new sdk key")
70+
new_client = LDClient(config=__config, start_wait=start_wait)
71+
old_client = __client
72+
__client = new_client
73+
old_client.close()
74+
finally:
75+
__lock.unlock()
2376

2477

2578
def get():
79+
global __config
80+
global __client
81+
global __lock
2682
try:
27-
_lock.rlock()
28-
if client:
29-
return client
83+
__lock.rlock()
84+
if __client:
85+
return __client
3086
finally:
31-
_lock.runlock()
87+
__lock.runlock()
3288

3389
try:
34-
global client
35-
_lock.lock()
36-
if not client:
90+
__lock.lock()
91+
if not __client:
3792
log.info("Initializing LaunchDarkly Client " + version.VERSION)
38-
client = LDClient(sdk_key, config, start_wait)
39-
return client
93+
__client = LDClient(config=__config, start_wait=start_wait)
94+
return __client
4095
finally:
41-
_lock.unlock()
96+
__lock.unlock()
4297

4398

4499
# Add a NullHandler for Python < 2.7 compatibility
45100
class NullHandler(logging.Handler):
46-
47101
def emit(self, record):
48102
pass
49103

ldclient/client.py

Lines changed: 23 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
import requests
99
from builtins import object
1010

11-
from ldclient.event_consumer import EventConsumerImpl
11+
from ldclient.config import Config as Config
1212
from ldclient.feature_requester import FeatureRequesterImpl
13-
from ldclient.feature_store import InMemoryFeatureStore
1413
from ldclient.flag import evaluate
15-
from ldclient.interfaces import FeatureStore
1614
from ldclient.polling import PollingUpdateProcessor
1715
from ldclient.streaming import StreamingUpdateProcessor
1816
from ldclient.util import check_uwsgi, log
@@ -27,80 +25,21 @@
2725
from cachecontrol import CacheControl
2826
from threading import Lock
2927

30-
GET_LATEST_FEATURES_PATH = '/sdk/latest-flags'
31-
STREAM_FEATURES_PATH = '/flags'
32-
33-
34-
class Config(object):
35-
def __init__(self,
36-
base_uri='https://app.launchdarkly.com',
37-
events_uri='https://events.launchdarkly.com',
38-
connect_timeout=10,
39-
read_timeout=15,
40-
events_upload_max_batch_size=100,
41-
events_max_pending=10000,
42-
stream_uri='https://stream.launchdarkly.com',
43-
stream=True,
44-
verify_ssl=True,
45-
defaults=None,
46-
events_enabled=True,
47-
update_processor_class=None,
48-
poll_interval=1,
49-
use_ldd=False,
50-
feature_store=InMemoryFeatureStore(),
51-
feature_requester_class=None,
52-
event_consumer_class=None,
53-
offline=False):
54-
"""
55-
56-
:param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key, config,
57-
and FeatureStore implementation
58-
:type update_processor_class: (str, Config, FeatureStore) -> UpdateProcessor
59-
:param feature_store: A FeatureStore implementation
60-
:type feature_store: FeatureStore
61-
:param feature_requester_class: A factory for a FeatureRequester implementation taking the sdk key and config
62-
:type feature_requester_class: (str, Config, FeatureStore) -> FeatureRequester
63-
:param event_consumer_class: A factory for an EventConsumer implementation taking the event queue, sdk key, and config
64-
:type event_consumer_class: (queue.Queue, str, Config) -> EventConsumer
65-
"""
66-
if defaults is None:
67-
defaults = {}
68-
69-
self.base_uri = base_uri.rstrip('\\')
70-
self.get_latest_features_uri = self.base_uri + GET_LATEST_FEATURES_PATH
71-
self.events_uri = events_uri.rstrip('\\') + '/bulk'
72-
self.stream_uri = stream_uri.rstrip('\\') + STREAM_FEATURES_PATH
73-
self.update_processor_class = update_processor_class
74-
self.stream = stream
75-
if poll_interval < 1:
76-
poll_interval = 1
77-
self.poll_interval = poll_interval
78-
self.use_ldd = use_ldd
79-
self.feature_store = InMemoryFeatureStore() if not feature_store else feature_store
80-
self.event_consumer_class = EventConsumerImpl if not event_consumer_class else event_consumer_class
81-
self.feature_requester_class = feature_requester_class
82-
self.connect_timeout = connect_timeout
83-
self.read_timeout = read_timeout
84-
self.events_enabled = events_enabled
85-
self.events_upload_max_batch_size = events_upload_max_batch_size
86-
self.events_max_pending = events_max_pending
87-
self.verify_ssl = verify_ssl
88-
self.defaults = defaults
89-
self.offline = offline
90-
91-
def get_default(self, key, default):
92-
return default if key not in self.defaults else self.defaults[key]
93-
94-
@classmethod
95-
def default(cls):
96-
return cls()
97-
9828

9929
class LDClient(object):
100-
def __init__(self, sdk_key, config=None, start_wait=5):
30+
def __init__(self, sdk_key=None, config=None, start_wait=5):
10131
check_uwsgi()
102-
self._sdk_key = sdk_key
103-
self._config = config or Config.default()
32+
33+
if config is not None and config.sdk_key is not None and sdk_key is not None:
34+
raise Exception("LaunchDarkly client init received both sdk_key and config with sdk_key. "
35+
"Only one of either is expected")
36+
37+
if sdk_key is not None:
38+
log.warn("Deprecated sdk_key argument was passed to init. Use config object instead.")
39+
self._config = Config(sdk_key=sdk_key)
40+
else:
41+
self._config = config or Config.default()
42+
10443
self._session = CacheControl(requests.Session())
10544
self._queue = queue.Queue(self._config.events_max_pending)
10645
self._event_consumer = None
@@ -110,39 +49,36 @@ def __init__(self, sdk_key, config=None, start_wait=5):
11049
""" :type: FeatureStore """
11150

11251
if self._config.offline:
113-
self._config.events_enabled = False
11452
log.info("Started LaunchDarkly Client in offline mode")
11553
return
11654

11755
if self._config.events_enabled:
118-
self._event_consumer = self._config.event_consumer_class(
119-
self._queue, self._sdk_key, self._config)
56+
self._event_consumer = self._config.event_consumer_class(self._queue, self._config)
12057
self._event_consumer.start()
12158

12259
if self._config.use_ldd:
12360
log.info("Started LaunchDarkly Client in LDD mode")
12461
return
12562

12663
if self._config.feature_requester_class:
127-
self._feature_requester = self._config.feature_requester_class(
128-
sdk_key, self._config)
64+
self._feature_requester = self._config.feature_requester_class(self._config)
12965
else:
130-
self._feature_requester = FeatureRequesterImpl(sdk_key, self._config)
66+
self._feature_requester = FeatureRequesterImpl(self._config)
13167
""" :type: FeatureRequester """
13268

13369
update_processor_ready = threading.Event()
13470

13571
if self._config.update_processor_class:
13672
log.info("Using user-specified update processor: " + str(self._config.update_processor_class))
13773
self._update_processor = self._config.update_processor_class(
138-
sdk_key, self._config, self._feature_requester, self._store, update_processor_ready)
74+
self._config, self._feature_requester, self._store, update_processor_ready)
13975
else:
14076
if self._config.stream:
14177
self._update_processor = StreamingUpdateProcessor(
142-
sdk_key, self._config, self._feature_requester, self._store, update_processor_ready)
78+
self._config, self._feature_requester, self._store, update_processor_ready)
14379
else:
14480
self._update_processor = PollingUpdateProcessor(
145-
sdk_key, self._config, self._feature_requester, self._store, update_processor_ready)
81+
self._config, self._feature_requester, self._store, update_processor_ready)
14682
""" :type: UpdateProcessor """
14783

14884
self._update_processor.start()
@@ -155,9 +91,8 @@ def __init__(self, sdk_key, config=None, start_wait=5):
15591
log.warn("Initialization timeout exceeded for LaunchDarkly Client or an error occurred. "
15692
"Feature Flags may not yet be available.")
15793

158-
@property
159-
def sdk_key(self):
160-
return self._sdk_key
94+
def get_sdk_key(self):
95+
return self._config.sdk_key
16196

16297
def close(self):
16398
log.info("Closing LaunchDarkly client..")
@@ -285,9 +220,9 @@ def _evaluate_multi(self, user, flags):
285220
return {k: self._evaluate(v, user)[0] for k, v in flags.items() or {}}
286221

287222
def secure_mode_hash(self, user):
288-
if user.get('key') is None:
223+
if user.get('key') is None or self._config.sdk_key is None:
289224
return ""
290-
return hmac.new(self._sdk_key.encode(), user.get('key').encode(), hashlib.sha256).hexdigest()
225+
return hmac.new(self._config.sdk_key.encode(), user.get('key').encode(), hashlib.sha256).hexdigest()
291226

292227
@staticmethod
293228
def _sanitize_user(user):

0 commit comments

Comments
 (0)