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 all 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.sdk_key = 'sdk_key'
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()
6 changes: 3 additions & 3 deletions demo/demo_twisted.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

@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': {
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
4 changes: 2 additions & 2 deletions ldclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

"""Settings."""
client = None
api_key = None
sdk_key = None
start_wait = 5
config = Config()

Expand All @@ -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()
Expand Down
87 changes: 56 additions & 31 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 @@ -51,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:
Expand Down Expand Up @@ -95,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)
Expand All @@ -108,12 +110,13 @@ 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

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:
Expand All @@ -125,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()
Expand All @@ -154,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..")
Expand All @@ -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,40 @@ 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.

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):
if user.get('key', "") == "":
return ""
return hmac.new(self._sdk_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
6 changes: 3 additions & 3 deletions ldclient/event_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions ldclient/feature_requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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,
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
4 changes: 2 additions & 2 deletions ldclient/polling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading