Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add all_flags, secure_mode_hash + more #51

Merged
merged 8 commits into from
Aug 4, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]"}, False):
if client.variation("your.flag.key", {"key": "[email protected]"}, False):
# application code to show the feature
else:
# the code to run if the feature is off
Expand Down
4 changes: 2 additions & 2 deletions demo/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
root.addHandler(ch)

if __name__ == '__main__':
ldclient._api_key = 'api_key'
ldclient.api_key = 'api_key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change api_key to sdk_key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a wholesale change- including removing the api_key prefix from the auth header.

ldclient.start_wait = 10
client = ldclient.get()

user = {u'key': 'userKey'}
print(client.toggle("update-app", user, False))
print(client.variation("update-app", user, False))

client.close()
2 changes: 1 addition & 1 deletion demo/demo_twisted.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
49 changes: 31 additions & 18 deletions ldclient/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import division, with_statement, absolute_import

import hashlib
import hmac
import threading
import time

Expand All @@ -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
Expand Down Expand Up @@ -108,6 +110,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

Expand Down Expand Up @@ -177,10 +180,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):
Expand All @@ -195,15 +202,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: "
Expand All @@ -222,26 +233,28 @@ def send_event(value):
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)
for event in events or []:
self._send_event(event)
log.debug("Sending event: " + str(event))

if value is not None:
send_event(value)
return value
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'])
send_event(value)
return value

send_event(default)
send_event(default, flag.get('version'))
return default

def all_flags(self, user):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkodumal take note of these checks when implementing in other sdks.

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.encode(), user.get('key').encode(), hashlib.sha256).hexdigest()

def _sanitize_user(self, user):
@staticmethod
def _sanitize_user(user):
if 'key' in user:
user['key'] = str(user['key'])

Expand Down
29 changes: 23 additions & 6 deletions ldclient/flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,46 @@
log = logging.getLogger(sys.modules[__name__].__name__)


def evaluate(flag, user, store, prereq_events=[]):
def evaluate(flag, user, store):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved all the flag eval logic into the flag file.

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):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we had the default value for prereq_events set to [] it was getting reused across function calls and the array just kept getting appended and we sent back a ton of prereq events.

events = prereq_events or []
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:
log.warn("Missing prereq flag: " + prereq.get('key'))
failed_prereq = prereq
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)
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
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')}
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):
Expand Down
26 changes: 15 additions & 11 deletions ldclient/redis_feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import redis

from ldclient import log
from ldclient.expiringdict import ExpiringDict
from ldclient.interfaces import FeatureStore

Expand All @@ -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()
Expand All @@ -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)
Expand Down
17 changes: 12 additions & 5 deletions ldclient/streaming.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from threading import Thread

import time
from sseclient import SSEClient

from ldclient.interfaces import UpdateProcessor
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion ldd/test_ldd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
21 changes: 13 additions & 8 deletions testing/test_ldclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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():
Expand All @@ -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()


Expand Down Expand Up @@ -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():
Expand All @@ -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)

Expand All @@ -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)


Expand All @@ -223,12 +223,17 @@ 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 test_secure_mode_hash():
user = {'key': 'Message'}
assert offline_client.secure_mode_hash(user) == "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"


def drain(queue):
Expand Down