Skip to content
Merged

V2 #48

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
4 changes: 2 additions & 2 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ dependencies:
test:
override:
- pyenv shell 2.7.10; $(pyenv which py.test) testing
- pyenv shell 3.3.3; $(pyenv which py.test) --ignore=testing/test_integration_twisted.py -s testing
- pyenv shell 3.4.2; $(pyenv which py.test) --ignore=testing/test_integration_twisted.py -s testing
- pyenv shell 3.3.3; $(pyenv which py.test) -s testing
- pyenv shell 3.4.2; $(pyenv which py.test) -s testing
2 changes: 1 addition & 1 deletion ldclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get():
global client
_lock.lock()
if not client:
log.debug("Initializing LaunchDarkly Client")
log.info("Initializing LaunchDarkly Client")
client = LDClient(api_key, config, start_wait)
return client
finally:
Expand Down
67 changes: 40 additions & 27 deletions ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
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.interfaces import FeatureStore
from ldclient.polling import PollingUpdateProcessor
from ldclient.streaming import StreamingUpdateProcessor
from ldclient.util import check_uwsgi, _evaluate, log
from ldclient.util import check_uwsgi, log

# noinspection PyBroadException
try:
Expand All @@ -24,8 +25,8 @@
from cachecontrol import CacheControl
from threading import Lock

GET_LATEST_FEATURES_PATH = '/api/eval/latest-features'
STREAM_FEATURES_PATH = '/features'
GET_LATEST_FEATURES_PATH = '/sdk/latest-flags'
STREAM_FEATURES_PATH = '/flags'


class Config(object):
Expand Down Expand Up @@ -176,57 +177,69 @@ def _send_event(self, event):

def track(self, event_name, user, data=None):
self._sanitize_user(user)
self._send_event({'kind': 'custom', 'key': event_name,
'user': user, 'data': data})
self._send_event({'kind': 'custom', 'key': event_name, 'user': user, 'data': data})

def identify(self, user):
self._sanitize_user(user)
self._send_event({'kind': 'identify', 'key': user['key'], 'user': user})
self._send_event({'kind': 'identify', 'key': user.get('key'), 'user': user})

def is_offline(self):
return self._config.offline

def is_initialized(self):
return self.is_offline() or self._config.use_ldd or self._update_processor.initialized()

def flush(self):
if self._config.offline or not self._config.events_enabled:
return
return self._event_consumer.flush()

def get_flag(self, key, user, default=False):
return self.toggle(key, user, default)

def toggle(self, key, user, default=False):
def toggle(self, key, user, default):
default = self._config.get_default(key, default)

def send_event(value):
self._send_event({'kind': 'feature', 'key': key,
'user': user, 'value': value, 'default': default})
self._sanitize_user(user)

if self._config.offline:
return default

self._sanitize_user(user)
def send_event(value):
self._send_event({'kind': 'feature', 'key': key,
'user': user, 'value': value, 'default': default})

if 'key' in user and user['key']:
feature = self._store.get(key)
else:
if not self.is_initialized():
log.warn("Feature Flag evaluation attempted before client has finished initializing! Returning default: "
+ str(default) + " for feature key: " + key)
send_event(default)
log.warning("Missing or empty User key when evaluating Feature Flag key: " + key + ". Returning default.")
return default

if feature:
val = _evaluate(feature, user)
else:
log.warning("Feature Flag key: " + key + " not found in Feature Store. Returning default.")
if user.get('key', "") == "":
log.warn("Missing or empty User key when evaluating Feature Flag key: " + key + ". Returning default.")
send_event(default)
return default

if val is None:
flag = self._store.get(key)
if not flag:
log.warn("Feature Flag key: " + key + " not found in Feature Store. Returning default.")
send_event(default)
log.warning("Feature Flag key: " + key + " evaluation returned None. Returning default.")
return default

send_event(val)
return val
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)

if value is not None:
send_event(value)
return value

if 'offVariation' in flag and flag['offVariation']:
value = _get_variation(flag, flag['offVariation'])
send_event(value)
return value

send_event(default)
return default


def _sanitize_user(self, user):
if 'key' in user:
Expand Down
2 changes: 1 addition & 1 deletion ldclient/event_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def __init__(self, event_queue, api_key, config):
self._running = True

def run(self):
log.debug("Starting event consumer")
log.info("Starting event consumer")
self._running = True
while self._running:
self.send()
Expand Down
151 changes: 151 additions & 0 deletions ldclient/flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import hashlib
import logging

import six
import sys

from ldclient import operators

__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)

__BUILTINS__ = ["key", "ip", "country", "email",
"firstName", "lastName", "avatar", "name", "anonymous"]

log = logging.getLogger(sys.modules[__name__].__name__)


def evaluate(flag, user, store, prereq_events=[]):
failed_prereq = None
for prereq in flag.get('prerequisites', []):
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)
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

if failed_prereq is not None:
return None, prereq_events

index = _evaluate_index(flag, user)
return _get_variation(flag, index), prereq_events


def _evaluate_index(feature, user):
# Check to see if any user targets match:
for target in feature.get('targets', []):
for value in target.get('values', []):
if value == user['key']:
return target.get('variation')

# Now walk through the rules to see if any match
for rule in feature.get('rules', []):
if _rule_matches_user(rule, user):
return _variation_index_for_user(feature, rule, user)

# Walk through fallthrough and see if it matches
if feature.get('fallthrough') is not None:
return _variation_index_for_user(feature, feature['fallthrough'], user)

return None


def _get_variation(feature, index):
if index is not None and index < len(feature['variations']):
return feature['variations'][index]
return None


def _get_off_variation(feature):
if feature.get('offVariation') is not None:
return _get_variation(feature, feature.get('offVariation'))
return None


def _get_user_attribute(user, attr):
if attr is 'secondary':
return None, True
if attr in __BUILTINS__:
return user.get(attr), False
else: # custom attribute
if user.get('custom') is None or user['custom'].get(attr) is None:
return None, True
return user['custom'][attr], False


def _variation_index_for_user(feature, rule, user):
if rule.get('variation') is not None:
return rule['variation']

if rule.get('rollout') is not None:
bucket_by = 'key'
if rule['rollout'].get('bucketBy') is not None:
bucket_by = rule['rollout']['bucketBy']
bucket = _bucket_user(user, feature, bucket_by)
sum = 0.0
for wv in rule['rollout'].get('variations', []):
sum += wv.get('weight', 0.0) / 100000.0
if bucket < sum:
return wv.get('variation')

return None


def _bucket_user(user, feature, bucket_by):
u_value, should_pass = _get_user_attribute(user, bucket_by)
if should_pass is True or not isinstance(u_value, six.string_types):
return 0.0

id_hash = u_value
if user.get('secondary') is not None:
id_hash = id_hash + '.' + user['secondary']
hash_key = '%s.%s.%s' % (feature['key'], feature['salt'], id_hash)
hash_val = int(hashlib.sha1(hash_key.encode('utf-8')).hexdigest()[:15], 16)
result = hash_val / __LONG_SCALE__
return result


def _rule_matches_user(rule, user):
for clause in rule.get('clauses', []):
if clause.get('attribute') is not None:
if not _clause_matches_user(clause, user):
return False
return True


def _clause_matches_user(clause, user):
u_value, should_pass = _get_user_attribute(user, clause.get('attribute'))
if should_pass is True:
return False
if u_value is None:
return None
# is the attr an array?
op_fn = operators.ops[clause['op']]
if isinstance(u_value, (list, tuple)):
for u in u_value:
if _match_any(op_fn, u, clause.get('values', [])):
return _maybe_negate(clause, True)
return _maybe_negate(clause, True)
else:
return _maybe_negate(clause, _match_any(op_fn, u_value, clause.get('values', [])))


def _match_any(op_fn, u, vals):
for v in vals:
if op_fn(u, v):
return True
return False


def _maybe_negate(clause, val):
if clause.get('negate', False):
return not val
return val
Loading