Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33fb7f5
support segments
eli-darkly Jan 18, 2018
64b0051
Merge branch 'segments' into eb/segments
eli-darkly Feb 1, 2018
91cd013
genericized feature store + misc fixes
eli-darkly Feb 1, 2018
7ae9b3a
unit tests, misc cleanup
eli-darkly Feb 1, 2018
6aaa7e8
undo renaming of modules
eli-darkly Feb 1, 2018
55baede
more test coverage
eli-darkly Feb 6, 2018
983ae60
misc cleanup
eli-darkly Feb 6, 2018
21b07ba
cleaner path-parsing logic
eli-darkly Feb 6, 2018
796a1fc
InMemoryFeatureStore should implement FeatureStore
eli-darkly Feb 6, 2018
745b3b9
add more unit test coverage of flag evals
eli-darkly Feb 6, 2018
f03aaa1
fix bug in flag evals - putting wrong flag in "prereqOf"
eli-darkly Feb 6, 2018
d245ef2
use namedtuple
eli-darkly Feb 6, 2018
8fdfd40
use namedtuple again
eli-darkly Feb 6, 2018
51853eb
misc cleanup
eli-darkly Feb 7, 2018
21389b6
use defaultdict
eli-darkly Feb 7, 2018
74beca3
change class name
eli-darkly Feb 7, 2018
79376e4
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
3a691f6
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
7e02fa2
fix merge
eli-darkly Feb 7, 2018
1d7dd3e
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
830f2d1
Merge pull request #31 from launchdarkly/eb/segments
eli-darkly Feb 13, 2018
2018a25
fix & test edge case of weight=None
eli-darkly Feb 13, 2018
1602f10
Merge pull request #36 from launchdarkly/eb/more-segment-tests
eli-darkly Feb 13, 2018
29a05b6
remove all Twisted support
eli-darkly Feb 21, 2018
35c787a
update readme: we do support streaming for Python 2.6
eli-darkly Feb 21, 2018
c380f8a
Merge pull request #37 from launchdarkly/eb/remove-twisted
eli-darkly Feb 21, 2018
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
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
include requirements.txt
include README.txt
include test-requirements.txt
include twisted-requirements.txt
include redis-requirements.txt
include python2.6-requirements.txt
32 changes: 1 addition & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,40 +40,11 @@ Your first feature flag

Python 2.6
----------
Python 2.6 is supported for polling mode only and requires an extra dependency. Here's how to set it up:
Python 2.6 requires an extra dependency. Here's how to set it up:

1. Use the `python2.6` extra in your requirements.txt:
`ldclient-py[python2.6]`

1. Due to Python 2.6's lack of SNI support, LaunchDarkly's streaming flag updates are not available. Set the `stream=False` option in the client config to disable it. You'll still receive flag updates, but via a polling mechanism with efficient caching. Here's an example:
`config = ldclient.Config(stream=False, sdk_key="SDK_KEY")`


Twisted
-------
Twisted is supported for LDD mode only. To run in Twisted/LDD mode,

1. Use this dependency:

```
ldclient-py[twisted]>=3.0.1
```
2. Configure the client:

```
feature_store = TwistedRedisFeatureStore(url='YOUR_REDIS_URL', redis_prefix="ldd-restwrapper", expiration=0)
ldclient.config.feature_store = feature_store

ldclient.config = ldclient.Config(
use_ldd=use_ldd,
event_consumer_class=TwistedEventConsumer,
)
ldclient.sdk_key = 'YOUR_SDK_KEY'
```
3. Get the client:

```client = ldclient.get()```

Learn more
-----------

Expand Down Expand Up @@ -104,7 +75,6 @@ About LaunchDarkly
* [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK")
* [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK")
* [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK")
* [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK")
* [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK")
* [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK")
* [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK")
Expand Down
5 changes: 3 additions & 2 deletions ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ldclient.polling import PollingUpdateProcessor
from ldclient.streaming import StreamingUpdateProcessor
from ldclient.util import check_uwsgi, log
from ldclient.versioned_data_kind import FEATURES, SEGMENTS

# noinspection PyBroadException
try:
Expand Down Expand Up @@ -184,7 +185,7 @@ def cb(flag):

return default

return self._store.get(key, cb)
return self._store.get(FEATURES, key, cb)

def _evaluate(self, flag, user):
return evaluate(flag, user, self._store)
Expand Down Expand Up @@ -223,7 +224,7 @@ def cb(all_flags):
log.error("Exception caught in all_flags: " + e.message + " for user: " + str(user))
return {}

return self._store.all(cb)
return self._store.all(FEATURES, cb)

def _evaluate_multi(self, user, flags):
return dict([(k, self._evaluate(v, user)[0]) for k, v in flags.items() or {}])
Expand Down
8 changes: 8 additions & 0 deletions ldclient/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def get_default(self, key, default):
def sdk_key(self):
return self.__sdk_key

@property
def base_uri(self):
return self.__base_uri

@property
def get_latest_flags_uri(self):
return self.__base_uri + GET_LATEST_FEATURES_PATH
Expand All @@ -143,6 +147,10 @@ def get_latest_flags_uri(self):
def events_uri(self):
return self.__events_uri + '/bulk'

@property
def stream_base_uri(self):
return self.__stream_uri

@property
def stream_uri(self):
return self.__stream_uri + STREAM_FLAGS_PATH
Expand Down
35 changes: 21 additions & 14 deletions ldclient/feature_requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from ldclient.interfaces import FeatureRequester
from ldclient.util import _headers
from ldclient.util import log
from ldclient.versioned_data_kind import FEATURES, SEGMENTS


LATEST_ALL_URI = '/sdk/latest-all'


class FeatureRequesterImpl(FeatureRequester):
Expand All @@ -14,32 +18,35 @@ def __init__(self, config):
self._session_no_cache = requests.Session()
self._config = config

def get_all(self):
def get_all_data(self):
hdrs = _headers(self._config.sdk_key)
uri = self._config.get_latest_flags_uri
uri = self._config.base_uri + LATEST_ALL_URI
r = self._session_cache.get(uri,
headers=hdrs,
timeout=(
self._config.connect_timeout,
self._config.read_timeout))
r.raise_for_status()
flags = r.json()
versions_summary = list(map(lambda f: "{0}:{1}".format(f.get("key"), f.get("version")), flags.values()))
log.debug("Get All flags response status:[{0}] From cache?[{1}] ETag:[{2}] flag versions: {3}"
.format(r.status_code, r.from_cache, r.headers.get('ETag'), versions_summary))
return flags
all_data = r.json()
log.debug("Get All flags response status:[%d] From cache?[%s] ETag:[%s]",
r.status_code, r.from_cache, r.headers.get('ETag'))
return {
FEATURES: all_data['flags'],
SEGMENTS: all_data['segments']
}

def get_one(self, key):
def get_one(self, kind, key):
hdrs = _headers(self._config.sdk_key)
uri = self._config.get_latest_flags_uri + '/' + key
log.debug("Getting one feature flag using uri: " + uri)
path = kind.request_api_path + '/' + key
uri = config.base_uri + path
log.debug("Getting %s from %s using uri: %s", key, kind['namespace'], uri)
r = self._session_no_cache.get(uri,
headers=hdrs,
timeout=(
self._config.connect_timeout,
self._config.read_timeout))
r.raise_for_status()
flag = r.json()
log.debug("Get one flag response status:[{0}] Flag key:[{1}] version:[{2}]"
.format(r.status_code, key, flag.get("version")))
return flag
obj = r.json()
log.debug("%s response status:[%d] key:[%s] version:[%d]",
path, r.status_code, key, segment.get("version"))
return obj
72 changes: 40 additions & 32 deletions ldclient/feature_store.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,76 @@
from collections import defaultdict
from ldclient.util import log
from ldclient.interfaces import FeatureStore
from ldclient.rwlock import ReadWriteLock


class InMemoryFeatureStore(FeatureStore):
"""
In-memory implementation of a store that holds feature flags and related data received from the streaming API.
"""

def __init__(self):
self._lock = ReadWriteLock()
self._initialized = False
self._features = {}
self._items = defaultdict(dict)

def get(self, key, callback):
def get(self, kind, key, callback):
try:
self._lock.rlock()
f = self._features.get(key)
if f is None:
log.debug("Attempted to get missing feature: " + str(key) + " Returning None")
itemsOfKind = self._items[kind]
item = itemsOfKind.get(key)
if item is None:
log.debug("Attempted to get missing key %s in '%s', returning None", key, kind.namespace)
return callback(None)
if 'deleted' in f and f['deleted']:
log.debug("Attempted to get deleted feature: " + str(key) + " Returning None")
if 'deleted' in item and item['deleted']:
log.debug("Attempted to get deleted key %s in '%s', returning None", key, kind.namespace)
return callback(None)
return callback(f)
return callback(item)
finally:
self._lock.runlock()

def all(self, callback):
def all(self, kind, callback):
try:
self._lock.rlock()
return callback(dict((k, f) for k, f in self._features.items() if ('deleted' not in f) or not f['deleted']))
itemsOfKind = self._items[kind]
return callback(dict((k, i) for k, i in itemsOfKind.items() if ('deleted' not in i) or not i['deleted']))
finally:
self._lock.runlock()

def init(self, features):
def init(self, all_data):
try:
self._lock.lock()
self._features = dict(features)
self._lock.rlock()
self._items.clear()
self._items.update(all_data)
self._initialized = True
log.debug("Initialized feature store with " + str(len(features)) + " features")
for k in all_data:
log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k]))
finally:
self._lock.unlock()
self._lock.runlock()

# noinspection PyShadowingNames
def delete(self, key, version):
def delete(self, kind, key, version):
try:
self._lock.lock()
f = self._features.get(key)
if f is not None and f['version'] < version:
f['deleted'] = True
f['version'] = version
elif f is None:
f = {'deleted': True, 'version': version}
self._features[key] = f
self._lock.rlock()
itemsOfKind = self._items[kind]
i = itemsOfKind.get(key)
if i is None or i['version'] < version:
i = {'deleted': True, 'version': version}
itemsOfKind[key] = i
finally:
self._lock.unlock()
self._lock.runlock()

def upsert(self, key, feature):
def upsert(self, kind, item):
key = item['key']
try:
self._lock.lock()
f = self._features.get(key)
if f is None or f['version'] < feature['version']:
self._features[key] = feature
log.debug("Updated feature {0} to version {1}".format(key, feature['version']))
self._lock.rlock()
itemsOfKind = self._items[kind]
i = itemsOfKind.get(key)
if i is None or i['version'] < item['version']:
itemsOfKind[key] = item
log.debug("Updated %s in '%s' to version %d", key, kind.namespace, item['version'])
finally:
self._lock.unlock()
self._lock.runlock()

@property
def initialized(self):
Expand Down
Loading