From 4a197faa46cc2b773264e553a4a0ee5581bf98b2 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Tue, 2 Aug 2016 15:34:01 -0700 Subject: [PATCH 1/8] [wip] v2 polish --- README.md | 2 +- demo/demo.py | 2 +- demo/demo_twisted.py | 2 +- ldclient/client.py | 24 ++++++++++++++++-------- ldclient/flag.py | 7 +++++-- ldclient/redis_feature_store.py | 26 +++++++++++++++----------- ldd/test_ldd.py | 2 +- testing/test_ldclient.py | 14 +++++++------- 8 files changed, 47 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1e36ff20..5d833503 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Your first feature flag 1. Create a new feature flag on your [dashboard](https://app.launchdarkly.com) 2. In your application code, use the feature's key to check wthether the flag is on for each user: - if client.toggle("your.flag.key", {"key": "user@test.com"}, False): + if client.variation("your.flag.key", {"key": "user@test.com"}, False): # application code to show the feature else: # the code to run if the feature is off diff --git a/demo/demo.py b/demo/demo.py index 9cf4e3fc..cc8a0635 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -20,6 +20,6 @@ client = ldclient.get() user = {u'key': 'userKey'} - print(client.toggle("update-app", user, False)) + print(client.variation("update-app", user, False)) client.close() diff --git a/demo/demo_twisted.py b/demo/demo_twisted.py index a7d0acf5..f973704d 100644 --- a/demo/demo_twisted.py +++ b/demo/demo_twisted.py @@ -13,7 +13,7 @@ def main(_): u'bizzle': u'def' } } - val = yield client.toggle('foo', user) + val = yield client.variation('foo', user) yield client.flush() print("Value: {}".format(val)) diff --git a/ldclient/client.py b/ldclient/client.py index f7d59b85..374f01f1 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -108,6 +108,7 @@ def __init__(self, api_key, config=None, start_wait=5): """ :type: FeatureStore """ if self._config.offline: + self._config.events_enabled = False log.info("Started LaunchDarkly Client in offline mode") return @@ -177,10 +178,14 @@ def _send_event(self, event): def track(self, event_name, user, data=None): self._sanitize_user(user) + if user.get('key', "") == "": + log.warn("Missing or empty User key when calling track().") self._send_event({'kind': 'custom', 'key': event_name, 'user': user, 'data': data}) def identify(self, user): self._sanitize_user(user) + if user.get('key', "") == "": + log.warn("Missing or empty User key when calling identify().") self._send_event({'kind': 'identify', 'key': user.get('key'), 'user': user}) def is_offline(self): @@ -195,15 +200,19 @@ def flush(self): return self._event_consumer.flush() def toggle(self, key, user, default): + log.warn("Deprecated method: toggle() called. Use variation() instead.") + return self.variation(key, user, default) + + def variation(self, key, user, default): default = self._config.get_default(key, default) self._sanitize_user(user) if self._config.offline: return default - def send_event(value): + def send_event(value, version=None): self._send_event({'kind': 'feature', 'key': key, - 'user': user, 'value': value, 'default': default}) + 'user': user, 'value': value, 'default': default, 'version': version}) if not self.is_initialized(): log.warn("Feature Flag evaluation attempted before client has finished initializing! Returning default: " @@ -229,18 +238,17 @@ def send_event(value): self._send_event(e) if value is not None: - send_event(value) + send_event(value, flag.get('version')) return value if 'offVariation' in flag and flag['offVariation']: - value = _get_variation(flag, flag['offVariation']) - send_event(value) - return value + value = _get_variation(flag, flag['offVariation']) + send_event(value, flag.get('version')) + return value - send_event(default) + send_event(default, flag.get('version')) return default - def _sanitize_user(self, user): if 'key' in user: user['key'] = str(user['key']) diff --git a/ldclient/flag.py b/ldclient/flag.py index fb18b21b..27c0c9a7 100644 --- a/ldclient/flag.py +++ b/ldclient/flag.py @@ -16,6 +16,7 @@ def evaluate(flag, user, store, prereq_events=[]): failed_prereq = None + prereq_value = None for prereq in flag.get('prerequisites') or []: prereq_flag = store.get(prereq.get('key')) if prereq_flag is None: @@ -24,14 +25,16 @@ def evaluate(flag, user, store, prereq_events=[]): break if prereq_flag.get('on', False) is True: prereq_value, prereq_events = evaluate(prereq_flag, user, store, prereq_events) - event = {'kind': 'feature', 'key': prereq.get('key'), 'user': user, 'value': prereq_value} - prereq_events.append(event) variation = _get_variation(prereq_flag, prereq.get('variation')) if prereq_value is None or not prereq_value == variation: failed_prereq = prereq else: failed_prereq = prereq + event = {'kind': 'feature', 'key': prereq.get('key'), 'user': user, + 'value': prereq_value, 'version': prereq_flag.get('version'), 'prereqOf': prereq.get('key')} + prereq_events.append(event) + if failed_prereq is not None: return None, prereq_events diff --git a/ldclient/redis_feature_store.py b/ldclient/redis_feature_store.py index ddd615ed..be1db995 100644 --- a/ldclient/redis_feature_store.py +++ b/ldclient/redis_feature_store.py @@ -2,6 +2,7 @@ import redis +from ldclient import log from ldclient.expiringdict import ExpiringDict from ldclient.interfaces import FeatureStore @@ -25,6 +26,7 @@ def __init__(self, self._cache = ForgetfulDict() if expiration == 0 else ExpiringDict(max_len=capacity, max_age_seconds=expiration) self._pool = redis.ConnectionPool.from_url(url=url, max_connections=max_connections) + log.info("Started RedisFeatureStore connected to URL: " + url + " using prefix: " + prefix) def init(self, features): pipe = redis.Redis(connection_pool=self._pool).pipeline() @@ -50,24 +52,26 @@ def all(self): def get(self, key): f = self._cache.get(key) - if f: + if f is not None: # reset ttl self._cache[key] = f - if 'deleted' in f and f['deleted']: + if f.get('deleted', False) is True: + log.warn("RedisFeatureStore: get returned deleted flag from in-memory cache. Returning None.") return None return f r = redis.Redis(connection_pool=self._pool) f_json = r.hget(self._features_key, key) - if f_json: - f = json.loads(f_json.decode('utf-8')) - if f: - if 'deleted' in f and f['deleted']: - return None - self._cache[key] = f - return f - - return None + if f_json is None or f_json is "": + log.warn("RedisFeatureStore: feature flag with key: " + key + " not found in Redis. Returning None.") + return None + + f = json.loads(f_json.decode('utf-8')) + if f.get('deleted', False) is True: + log.warn("RedisFeatureStore: get returned deleted flag from Redis. Returning None.") + return None + self._cache[key] = f + return f def delete(self, key, version): r = redis.Redis(connection_pool=self._pool) diff --git a/ldd/test_ldd.py b/ldd/test_ldd.py index e661d88d..1b7f7dc5 100644 --- a/ldd/test_ldd.py +++ b/ldd/test_ldd.py @@ -32,7 +32,7 @@ def test_sse_init(stream): client = LDClient("apikey", Config(use_ldd=True, feature_store=RedisFeatureStore(), events_enabled=False)) - wait_until(lambda: client.toggle( + wait_until(lambda: client.variation( "foo", user('xyz'), "blah") == "jim", timeout=10) diff --git a/testing/test_ldclient.py b/testing/test_ldclient.py index 363e949e..89270fea 100644 --- a/testing/test_ldclient.py +++ b/testing/test_ldclient.py @@ -125,7 +125,7 @@ def wait_for_event(c, cb): def test_toggle_offline(): - assert offline_client.toggle('feature.key', user, default=None) is None + assert offline_client.variation('feature.key', user, default=None) is None def test_sanitize_user(): @@ -134,7 +134,7 @@ def test_sanitize_user(): def test_toggle_event_offline(): - offline_client.toggle('feature.key', user, default=None) + offline_client.variation('feature.key', user, default=None) assert offline_client._queue.empty() @@ -187,7 +187,7 @@ def test_track_offline(): def test_defaults(): client = LDClient("API_KEY", Config( "http://localhost:3000", defaults={"foo": "bar"}, offline=True)) - assert "bar" == client.toggle('foo', user, default=None) + assert "bar" == client.variation('foo', user, default=None) def test_defaults_and_online(): @@ -197,7 +197,7 @@ def test_defaults_and_online(): event_consumer_class=MockConsumer, feature_requester_class=MockFeatureRequester, feature_store=InMemoryFeatureStore())) - actual = my_client.toggle('foo', user, default="originalDefault") + actual = my_client.variation('foo', user, default="originalDefault") assert actual == expected assert wait_for_event(my_client, lambda e: e['kind'] == 'feature' and e['key'] == u'foo' and e['user'] == user) @@ -207,7 +207,7 @@ def test_defaults_and_online_no_default(): defaults={"foo": "bar"}, event_consumer_class=MockConsumer, feature_requester_class=MockFeatureRequester)) - assert "jim" == client.toggle('baz', user, default="jim") + assert "jim" == client.variation('baz', user, default="jim") assert wait_for_event(client, lambda e: e['kind'] == 'feature' and e['key'] == u'baz' and e['user'] == user) @@ -223,12 +223,12 @@ def get_all(self): feature_store=InMemoryFeatureStore(), feature_requester_class=ExceptionFeatureRequester, event_consumer_class=MockConsumer)) - assert "bar" == client.toggle('foo', user, default="jim") + assert "bar" == client.variation('foo', user, default="jim") assert wait_for_event(client, lambda e: e['kind'] == 'feature' and e['key'] == u'foo' and e['user'] == user) def test_no_defaults(): - assert "bar" == offline_client.toggle('foo', user, default="bar") + assert "bar" == offline_client.variation('foo', user, default="bar") def drain(queue): From b6ecb82ce2be1789a26fc5d8f5fb4e6c8ac10932 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 10:22:24 -0700 Subject: [PATCH 2/8] Add all_flags, secure_mode_hash + more. Fix prereq event sending. --- demo/demo.py | 2 +- ldclient/client.py | 32 +++++++++++++++++++------------- ldclient/flag.py | 24 +++++++++++++++++++----- testing/test_ldclient.py | 7 ++++++- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/demo/demo.py b/demo/demo.py index cc8a0635..45a632ab 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -15,7 +15,7 @@ root.addHandler(ch) if __name__ == '__main__': - ldclient._api_key = 'api_key' + ldclient.api_key = 'api_key' ldclient.start_wait = 10 client = ldclient.get() diff --git a/ldclient/client.py b/ldclient/client.py index 374f01f1..82eee6b1 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -1,5 +1,7 @@ from __future__ import division, with_statement, absolute_import +import hashlib +import hmac import threading import time @@ -9,7 +11,7 @@ from ldclient.event_consumer import EventConsumerImpl from ldclient.feature_requester import FeatureRequesterImpl from ldclient.feature_store import InMemoryFeatureStore -from ldclient.flag import _get_off_variation, _evaluate_index, _get_variation, evaluate +from ldclient.flag import evaluate from ldclient.interfaces import FeatureStore from ldclient.polling import PollingUpdateProcessor from ldclient.streaming import StreamingUpdateProcessor @@ -231,25 +233,29 @@ def send_event(value, version=None): send_event(default) return default - if flag.get('on', False): - value, prereq_events = evaluate(flag, user, self._store) - if not self._config.offline: - for e in prereq_events: - self._send_event(e) + value, events = evaluate(flag, user, self._store) + log.debug("Got " + str(len(events)) + " prereq events for feature key: " + key) + for event in events or []: + self._send_event(event) + log.debug("Sending event: " + str(event)) - if value is not None: - send_event(value, flag.get('version')) - return value - - if 'offVariation' in flag and flag['offVariation']: - value = _get_variation(flag, flag['offVariation']) + if value is not None: send_event(value, flag.get('version')) return value send_event(default, flag.get('version')) return default - def _sanitize_user(self, user): + def all_flags(self, user): + return {k: evaluate(v, user, self._store)[0] for k, v in self._store.all().items() or {}} + + def secure_mode_hash(self, user): + if user.get('key', "") == "": + return "" + return hmac.new(self._api_key, user.get('key'), hashlib.sha256).hexdigest() + + @staticmethod + def _sanitize_user(user): if 'key' in user: user['key'] = str(user['key']) diff --git a/ldclient/flag.py b/ldclient/flag.py index 27c0c9a7..faa117ff 100644 --- a/ldclient/flag.py +++ b/ldclient/flag.py @@ -14,7 +14,21 @@ log = logging.getLogger(sys.modules[__name__].__name__) -def evaluate(flag, user, store, prereq_events=[]): +def evaluate(flag, user, store): + prereq_events = [] + if flag.get('on', False): + value, prereq_events = _evaluate(flag, user, store) + if value is not None: + return value, prereq_events + + if 'offVariation' in flag and flag['offVariation']: + value = _get_variation(flag, flag['offVariation']) + return value, prereq_events + return None, prereq_events + + +def _evaluate(flag, user, store, prereq_events=None): + events = prereq_events or [] failed_prereq = None prereq_value = None for prereq in flag.get('prerequisites') or []: @@ -24,7 +38,7 @@ def evaluate(flag, user, store, prereq_events=[]): failed_prereq = prereq break if prereq_flag.get('on', False) is True: - prereq_value, prereq_events = evaluate(prereq_flag, user, store, prereq_events) + prereq_value, events = _evaluate(prereq_flag, user, store, events) variation = _get_variation(prereq_flag, prereq.get('variation')) if prereq_value is None or not prereq_value == variation: failed_prereq = prereq @@ -33,13 +47,13 @@ def evaluate(flag, user, store, prereq_events=[]): event = {'kind': 'feature', 'key': prereq.get('key'), 'user': user, 'value': prereq_value, 'version': prereq_flag.get('version'), 'prereqOf': prereq.get('key')} - prereq_events.append(event) + events.append(event) if failed_prereq is not None: - return None, prereq_events + return None, events index = _evaluate_index(flag, user) - return _get_variation(flag, index), prereq_events + return _get_variation(flag, index), events def _evaluate_index(feature, user): diff --git a/testing/test_ldclient.py b/testing/test_ldclient.py index 89270fea..4f3f35d0 100644 --- a/testing/test_ldclient.py +++ b/testing/test_ldclient.py @@ -55,7 +55,7 @@ def get(self, key): client = LDClient("API_KEY", Config("http://localhost:3000", feature_store=MockFeatureStore())) -offline_client = LDClient("API_KEY", Config("http://localhost:3000", feature_store=MockFeatureStore(), offline=True)) +offline_client = LDClient("secret", Config("http://localhost:3000", feature_store=MockFeatureStore(), offline=True)) user = { u'key': u'xyz', @@ -231,6 +231,11 @@ def test_no_defaults(): assert "bar" == offline_client.variation('foo', user, default="bar") +def test_secure_mode_hash(): + user = {'key': 'Message'} + assert offline_client.secure_mode_hash(user) == "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597" + + def drain(queue): while not queue.empty(): queue.get() From 1093b0d80a54b437b26419ed58c835a64489eda7 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 10:34:59 -0700 Subject: [PATCH 3/8] attempt to fix python 3 test failure --- ldclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldclient/client.py b/ldclient/client.py index 82eee6b1..449fdd81 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -252,7 +252,7 @@ def all_flags(self, user): def secure_mode_hash(self, user): if user.get('key', "") == "": return "" - return hmac.new(self._api_key, user.get('key'), hashlib.sha256).hexdigest() + return hmac.new(bytes(self._api_key), bytes(user.get('key')), hashlib.sha256).hexdigest() @staticmethod def _sanitize_user(user): From 1f7ea6f1ed9723cc9b0cb4b8920584b946f7aa44 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 10:38:04 -0700 Subject: [PATCH 4/8] remove debug log statement --- ldclient/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ldclient/client.py b/ldclient/client.py index 449fdd81..e08ed254 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -234,7 +234,6 @@ def send_event(value, version=None): return default value, events = evaluate(flag, user, self._store) - log.debug("Got " + str(len(events)) + " prereq events for feature key: " + key) for event in events or []: self._send_event(event) log.debug("Sending event: " + str(event)) From 989878dca4ae24869fc12e4e19b813448b077107 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 10:48:37 -0700 Subject: [PATCH 5/8] Change string encoding --- ldclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldclient/client.py b/ldclient/client.py index e08ed254..2290d68f 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -251,7 +251,7 @@ def all_flags(self, user): def secure_mode_hash(self, user): if user.get('key', "") == "": return "" - return hmac.new(bytes(self._api_key), bytes(user.get('key')), hashlib.sha256).hexdigest() + return hmac.new(self._api_key.encode(), user.get('key').encode(), hashlib.sha256).hexdigest() @staticmethod def _sanitize_user(user): From 6ab8ed2c5ad0012f5834f0e81dec9fa79a3648db Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 16:32:17 -0700 Subject: [PATCH 6/8] Never. stop. reconnecting. --- ldclient/streaming.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/ldclient/streaming.py b/ldclient/streaming.py index f7e66632..6e3a690e 100644 --- a/ldclient/streaming.py +++ b/ldclient/streaming.py @@ -1,6 +1,7 @@ import json from threading import Thread +import time from sseclient import SSEClient from ldclient.interfaces import UpdateProcessor @@ -24,11 +25,17 @@ def run(self): self._running = True hdrs = _stream_headers(self._api_key) uri = self._config.stream_uri - messages = SSEClient(uri, verify=self._config.verify_ssl, headers=hdrs) - for msg in messages: - if not self._running: - break - self.process_message(self._store, self._requester, msg, self._ready) + while self._running: + try: + messages = SSEClient(uri, verify=self._config.verify_ssl, headers=hdrs) + for msg in messages: + if not self._running: + break + self.process_message(self._store, self._requester, msg, self._ready) + except Exception as e: + log.error("Could not connect to LaunchDarkly stream: " + str(e.message) + + " waiting 1 second before trying again.") + time.sleep(1) def stop(self): log.info("Stopping StreamingUpdateProcessor") From 99bb605448289e98c94cbd340021e9d45fc54438 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 16:54:51 -0700 Subject: [PATCH 7/8] api_key->sdk_key. Remove 'api_Key ' prefix from request auth header --- README.md | 4 ++-- demo/demo.py | 2 +- demo/demo_twisted.py | 4 ++-- ldclient/__init__.py | 4 ++-- ldclient/client.py | 28 ++++++++++++++-------------- ldclient/event_consumer.py | 6 +++--- ldclient/feature_requester.py | 8 ++++---- ldclient/polling.py | 4 ++-- ldclient/streaming.py | 6 +++--- ldclient/twisted_impls.py | 20 ++++++++++---------- ldclient/util.py | 8 ++++---- ldd/bootstrap.sh | 2 +- testing/test_ldclient.py | 10 +++++----- 13 files changed, 53 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 5d833503..b5593a53 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Quick setup pip install ldclient-py -2. Configure the library with your api key: +2. Configure the library with your sdk key: import ldclient - ldclient.api_key = "your api key" + ldclient.sdk_key = "your sdk key" 3. Get the client: diff --git a/demo/demo.py b/demo/demo.py index 45a632ab..ca9b580e 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -15,7 +15,7 @@ root.addHandler(ch) if __name__ == '__main__': - ldclient.api_key = 'api_key' + ldclient.sdk_key = 'sdk_key' ldclient.start_wait = 10 client = ldclient.get() diff --git a/demo/demo_twisted.py b/demo/demo_twisted.py index f973704d..2b2cd18b 100644 --- a/demo/demo_twisted.py +++ b/demo/demo_twisted.py @@ -5,8 +5,8 @@ @defer.inlineCallbacks def main(_): - api_key = 'whatever' - client = TwistedLDClient(api_key) + sdk_key = 'whatever' + client = TwistedLDClient(sdk_key) user = { u'key': u'xyz', u'custom': { diff --git a/ldclient/__init__.py b/ldclient/__init__.py index ee332a66..feecfb74 100644 --- a/ldclient/__init__.py +++ b/ldclient/__init__.py @@ -15,7 +15,7 @@ """Settings.""" client = None -api_key = None +sdk_key = None start_wait = 5 config = Config() @@ -35,7 +35,7 @@ def get(): _lock.lock() if not client: log.info("Initializing LaunchDarkly Client") - client = LDClient(api_key, config, start_wait) + client = LDClient(sdk_key, config, start_wait) return client finally: _lock.unlock() diff --git a/ldclient/client.py b/ldclient/client.py index 2290d68f..6bd24773 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -53,14 +53,14 @@ def __init__(self, offline=False): """ - :param update_processor_class: A factory for an UpdateProcessor implementation taking the api key, config, + :param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key, config, and FeatureStore implementation :type update_processor_class: (str, Config, FeatureStore) -> UpdateProcessor :param feature_store: A FeatureStore implementation :type feature_store: FeatureStore - :param feature_requester_class: A factory for a FeatureRequester implementation taking the api key and config + :param feature_requester_class: A factory for a FeatureRequester implementation taking the sdk key and config :type feature_requester_class: (str, Config, FeatureStore) -> FeatureRequester - :param event_consumer_class: A factory for an EventConsumer implementation taking the event queue, api key, and config + :param event_consumer_class: A factory for an EventConsumer implementation taking the event queue, sdk key, and config :type event_consumer_class: (queue.Queue, str, Config) -> EventConsumer """ if defaults is None: @@ -97,9 +97,9 @@ def default(cls): class LDClient(object): - def __init__(self, api_key, config=None, start_wait=5): + def __init__(self, sdk_key, config=None, start_wait=5): check_uwsgi() - self._api_key = api_key + self._sdk_key = sdk_key self._config = config or Config.default() self._session = CacheControl(requests.Session()) self._queue = queue.Queue(self._config.events_max_pending) @@ -116,7 +116,7 @@ def __init__(self, api_key, config=None, start_wait=5): if self._config.events_enabled: self._event_consumer = self._config.event_consumer_class( - self._queue, self._api_key, self._config) + self._queue, self._sdk_key, self._config) self._event_consumer.start() if self._config.use_ldd: @@ -128,23 +128,23 @@ def __init__(self, api_key, config=None, start_wait=5): if self._config.feature_requester_class: self._feature_requester = self._config.feature_requester_class( - api_key, self._config) + sdk_key, self._config) else: - self._feature_requester = FeatureRequesterImpl(api_key, self._config) + self._feature_requester = FeatureRequesterImpl(sdk_key, self._config) """ :type: FeatureRequester """ update_processor_ready = threading.Event() if self._config.update_processor_class: self._update_processor = self._config.update_processor_class( - api_key, self._config, self._feature_requester, self._store, update_processor_ready) + sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) else: if self._config.stream: self._update_processor = StreamingUpdateProcessor( - api_key, self._config, self._feature_requester, self._store, update_processor_ready) + sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) else: self._update_processor = PollingUpdateProcessor( - api_key, self._config, self._feature_requester, self._store, update_processor_ready) + sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) """ :type: UpdateProcessor """ self._update_processor.start() @@ -157,8 +157,8 @@ def __init__(self, api_key, config=None, start_wait=5): log.info("Initialization timeout exceeded for LaunchDarkly Client. Feature Flags may not yet be available.") @property - def api_key(self): - return self._api_key + def sdk_key(self): + return self._sdk_key def close(self): log.info("Closing LaunchDarkly client..") @@ -251,7 +251,7 @@ def all_flags(self, user): def secure_mode_hash(self, user): if user.get('key', "") == "": return "" - return hmac.new(self._api_key.encode(), user.get('key').encode(), hashlib.sha256).hexdigest() + return hmac.new(self._sdk_key.encode(), user.get('key').encode(), hashlib.sha256).hexdigest() @staticmethod def _sanitize_user(user): diff --git a/ldclient/event_consumer.py b/ldclient/event_consumer.py index be101f2e..5131e3f4 100644 --- a/ldclient/event_consumer.py +++ b/ldclient/event_consumer.py @@ -13,11 +13,11 @@ class EventConsumerImpl(Thread, EventConsumer): - def __init__(self, event_queue, api_key, config): + def __init__(self, event_queue, sdk_key, config): Thread.__init__(self) self._session = requests.Session() self.daemon = True - self._api_key = api_key + self.sdk_key = sdk_key self._config = config self._queue = event_queue self._running = True @@ -42,7 +42,7 @@ def do_send(should_retry): body = [events] else: body = events - hdrs = _headers(self._api_key) + hdrs = _headers(self.sdk_key) uri = self._config.events_uri r = self._session.post(uri, headers=hdrs, diff --git a/ldclient/feature_requester.py b/ldclient/feature_requester.py index 1c72c34a..85f6bd4c 100644 --- a/ldclient/feature_requester.py +++ b/ldclient/feature_requester.py @@ -8,13 +8,13 @@ class FeatureRequesterImpl(FeatureRequester): - def __init__(self, api_key, config): - self._api_key = api_key + def __init__(self, sdk_key, config): + self._sdk_key = sdk_key self._session = CacheControl(requests.Session()) self._config = config def get_all(self): - hdrs = _headers(self._api_key) + hdrs = _headers(self._sdk_key) uri = self._config.get_latest_features_uri r = self._session.get(uri, headers=hdrs, timeout=( self._config.connect_timeout, self._config.read_timeout)) @@ -23,7 +23,7 @@ def get_all(self): return features def get_one(self, key): - hdrs = _headers(self._api_key) + hdrs = _headers(self._sdk_key) uri = self._config.get_latest_features_uri + '/' + key r = self._session.get(uri, headers=hdrs, diff --git a/ldclient/polling.py b/ldclient/polling.py index dace8724..418543ce 100644 --- a/ldclient/polling.py +++ b/ldclient/polling.py @@ -6,10 +6,10 @@ class PollingUpdateProcessor(Thread, UpdateProcessor): - def __init__(self, api_key, config, requester, store, ready): + def __init__(self, sdk_key, config, requester, store, ready): Thread.__init__(self) self.daemon = True - self._api_key = api_key + self._sdk_key = sdk_key self._config = config self._requester = requester self._store = store diff --git a/ldclient/streaming.py b/ldclient/streaming.py index 6e3a690e..265b425c 100644 --- a/ldclient/streaming.py +++ b/ldclient/streaming.py @@ -10,10 +10,10 @@ class StreamingUpdateProcessor(Thread, UpdateProcessor): - def __init__(self, api_key, config, requester, store, ready): + def __init__(self, sdk_key, config, requester, store, ready): Thread.__init__(self) self.daemon = True - self._api_key = api_key + self._sdk_key = sdk_key self._config = config self._requester = requester self._store = store @@ -23,7 +23,7 @@ def __init__(self, api_key, config, requester, store, ready): def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._config.stream_uri) self._running = True - hdrs = _stream_headers(self._api_key) + hdrs = _stream_headers(self._sdk_key) uri = self._config.stream_uri while self._running: try: diff --git a/ldclient/twisted_impls.py b/ldclient/twisted_impls.py index acf299d2..97ddd4bc 100644 --- a/ldclient/twisted_impls.py +++ b/ldclient/twisted_impls.py @@ -17,8 +17,8 @@ class TwistedHttpFeatureRequester(FeatureRequester): - def __init__(self, api_key, config): - self._api_key = api_key + def __init__(self, sdk_key, config): + self._sdk_key = sdk_key self._session = CacheControl(txrequests.Session()) self._config = config @@ -47,7 +47,7 @@ def run(should_retry): @defer.inlineCallbacks def _get_all(self): - hdrs = _headers(self._api_key) + hdrs = _headers(self._sdk_key) uri = self._config.get_latest_features_uri r = yield self._session.get(uri, headers=hdrs, timeout=(self._config.connect, self._config.read)) r.raise_for_status() @@ -68,12 +68,12 @@ class TwistedStreamProcessor(UpdateProcessor): def close(self): self.sse_client.stop() - def __init__(self, api_key, config, store, requester, ready): + def __init__(self, sdk_key, config, store, requester, ready): self._store = store self._requester = requester self._ready = ready self.sse_client = TwistedSSEClient(config.stream_uri, - headers=_stream_headers(api_key, "PythonTwistedClient"), + headers=_stream_headers(sdk_key, "PythonTwistedClient"), verify_ssl=config.verify_ssl, on_event=partial(StreamingUpdateProcessor.process_message, self._store, @@ -97,14 +97,14 @@ def is_alive(self): class TwistedEventConsumer(EventConsumer): - def __init__(self, queue, api_key, config): + def __init__(self, queue, sdk_key, config): self._queue = queue """ @type: queue.Queue """ self._session = CacheControl(txrequests.Session()) """ :type: txrequests.Session """ - self._api_key = api_key + self._sdk_key = sdk_key self._config = config """ :type: ldclient.twisted.TwistedConfig """ @@ -145,7 +145,7 @@ def do_send(should_retry): body = [events] else: body = events - hdrs = _headers(self._api_key) + hdrs = _headers(self._sdk_key) r = yield self._session.post(self._config.events_uri, headers=hdrs, timeout=(self._config.connect, self._config.read), @@ -172,10 +172,10 @@ def do_send(should_retry): class TwistedLDClient(LDClient): - def __init__(self, api_key, config=None): + def __init__(self, sdk_key, config=None): if config is None: config = TwistedConfig() - LDClient.__init__(self, api_key, config) + LDClient.__init__(self, sdk_key, config) __all__ = ['TwistedConfig', 'TwistedLDClient'] diff --git a/ldclient/util.py b/ldclient/util.py index 55b5862e..6fd35201 100644 --- a/ldclient/util.py +++ b/ldclient/util.py @@ -31,13 +31,13 @@ __BASE_TYPES__ = (str, float, int, bool, unicode) -def _headers(api_key): - return {'Authorization': 'api_key ' + api_key, 'User-Agent': 'PythonClient/' + VERSION, +def _headers(sdk_key): + return {'Authorization': sdk_key, 'User-Agent': 'PythonClient/' + VERSION, 'Content-Type': "application/json"} -def _stream_headers(api_key, client="PythonClient"): - return {'Authorization': 'api_key ' + api_key, +def _stream_headers(sdk_key, client="PythonClient"): + return {'Authorization': sdk_key, 'User-Agent': '{}/{}'.format(client, VERSION), 'Cache-Control': 'no-cache', 'Accept': "text/event-stream"} diff --git a/ldd/bootstrap.sh b/ldd/bootstrap.sh index bfc60266..6a8cf631 100755 --- a/ldd/bootstrap.sh +++ b/ldd/bootstrap.sh @@ -48,7 +48,7 @@ host = "localhost" port = 6379 [main] -apiKey = "YOUR_API_KEY" +sdkKey = "YOUR_SDK_KEY" prefix = "launchdarkly" streamUri = "http://localhost:8000" EOF diff --git a/testing/test_ldclient.py b/testing/test_ldclient.py index 4f3f35d0..9dbf4d78 100644 --- a/testing/test_ldclient.py +++ b/testing/test_ldclient.py @@ -54,7 +54,7 @@ def get(self, key): return None -client = LDClient("API_KEY", Config("http://localhost:3000", feature_store=MockFeatureStore())) +client = LDClient("SDK_KEY", Config("http://localhost:3000", feature_store=MockFeatureStore())) offline_client = LDClient("secret", Config("http://localhost:3000", feature_store=MockFeatureStore(), offline=True)) user = { @@ -185,14 +185,14 @@ def test_track_offline(): def test_defaults(): - client = LDClient("API_KEY", Config( + client = LDClient("SDK_KEY", Config( "http://localhost:3000", defaults={"foo": "bar"}, offline=True)) assert "bar" == client.variation('foo', user, default=None) def test_defaults_and_online(): expected = "bar" - my_client = LDClient("API_KEY", Config("http://localhost:3000", + my_client = LDClient("SDK_KEY", Config("http://localhost:3000", defaults={"foo": expected}, event_consumer_class=MockConsumer, feature_requester_class=MockFeatureRequester, @@ -203,7 +203,7 @@ def test_defaults_and_online(): def test_defaults_and_online_no_default(): - client = LDClient("API_KEY", Config("http://localhost:3000", + client = LDClient("SDK_KEY", Config("http://localhost:3000", defaults={"foo": "bar"}, event_consumer_class=MockConsumer, feature_requester_class=MockFeatureRequester)) @@ -219,7 +219,7 @@ def __init__(self, *_): def get_all(self): raise Exception("blah") - client = LDClient("API_KEY", Config("http://localhost:3000", defaults={"foo": "bar"}, + client = LDClient("SDK_KEY", Config("http://localhost:3000", defaults={"foo": "bar"}, feature_store=InMemoryFeatureStore(), feature_requester_class=ExceptionFeatureRequester, event_consumer_class=MockConsumer)) From a2a70325c6dd2228b11b6054852e6168418af4a3 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 17:06:44 -0700 Subject: [PATCH 8/8] Add checks to all_flags function --- ldclient/client.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ldclient/client.py b/ldclient/client.py index 6bd24773..28a6f156 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -246,6 +246,18 @@ def send_event(value, version=None): return default def all_flags(self, user): + if self._config.offline: + log.warn("all_flags() called, but client is in offline mode. Returning None") + return None + + if not self.is_initialized(): + log.warn("all_flags() called before client has finished initializing! Returning None") + return None + + if user.get('key', "") == "": + log.warn("Missing or empty User key when calling all_flags(). Returning None.") + return None + return {k: evaluate(v, user, self._store)[0] for k, v in self._store.all().items() or {}} def secure_mode_hash(self, user):