Skip to content

Commit

Permalink
prepare 5.0.0 release (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly authored Feb 21, 2018
1 parent 3cde7db commit c1bc4ad
Show file tree
Hide file tree
Showing 20 changed files with 537 additions and 538 deletions.
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

0 comments on commit c1bc4ad

Please sign in to comment.