Skip to content

Commit c1bc4ad

Browse files
authored
prepare 5.0.0 release (#76)
1 parent 3cde7db commit c1bc4ad

20 files changed

+537
-538
lines changed

MANIFEST.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
include requirements.txt
22
include README.txt
33
include test-requirements.txt
4-
include twisted-requirements.txt
54
include redis-requirements.txt
65
include python2.6-requirements.txt

README.md

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,40 +40,11 @@ Your first feature flag
4040

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

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

48-
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:
49-
`config = ldclient.Config(stream=False, sdk_key="SDK_KEY")`
50-
51-
52-
Twisted
53-
-------
54-
Twisted is supported for LDD mode only. To run in Twisted/LDD mode,
55-
56-
1. Use this dependency:
57-
58-
```
59-
ldclient-py[twisted]>=3.0.1
60-
```
61-
2. Configure the client:
62-
63-
```
64-
feature_store = TwistedRedisFeatureStore(url='YOUR_REDIS_URL', redis_prefix="ldd-restwrapper", expiration=0)
65-
ldclient.config.feature_store = feature_store
66-
67-
ldclient.config = ldclient.Config(
68-
use_ldd=use_ldd,
69-
event_consumer_class=TwistedEventConsumer,
70-
)
71-
ldclient.sdk_key = 'YOUR_SDK_KEY'
72-
```
73-
3. Get the client:
74-
75-
```client = ldclient.get()```
76-
7748
Learn more
7849
-----------
7950

@@ -104,7 +75,6 @@ About LaunchDarkly
10475
* [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK")
10576
* [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK")
10677
* [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK")
107-
* [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK")
10878
* [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK")
10979
* [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK")
11080
* [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK")

ldclient/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ldclient.polling import PollingUpdateProcessor
1515
from ldclient.streaming import StreamingUpdateProcessor
1616
from ldclient.util import check_uwsgi, log
17+
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
1718

1819
# noinspection PyBroadException
1920
try:
@@ -184,7 +185,7 @@ def cb(flag):
184185

185186
return default
186187

187-
return self._store.get(key, cb)
188+
return self._store.get(FEATURES, key, cb)
188189

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

226-
return self._store.all(cb)
227+
return self._store.all(FEATURES, cb)
227228

228229
def _evaluate_multi(self, user, flags):
229230
return dict([(k, self._evaluate(v, user)[0]) for k, v in flags.items() or {}])

ldclient/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ def get_default(self, key, default):
135135
def sdk_key(self):
136136
return self.__sdk_key
137137

138+
@property
139+
def base_uri(self):
140+
return self.__base_uri
141+
138142
@property
139143
def get_latest_flags_uri(self):
140144
return self.__base_uri + GET_LATEST_FEATURES_PATH
@@ -143,6 +147,10 @@ def get_latest_flags_uri(self):
143147
def events_uri(self):
144148
return self.__events_uri + '/bulk'
145149

150+
@property
151+
def stream_base_uri(self):
152+
return self.__stream_uri
153+
146154
@property
147155
def stream_uri(self):
148156
return self.__stream_uri + STREAM_FLAGS_PATH

ldclient/feature_requester.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from ldclient.interfaces import FeatureRequester
77
from ldclient.util import _headers
88
from ldclient.util import log
9+
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
10+
11+
12+
LATEST_ALL_URI = '/sdk/latest-all'
913

1014

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

17-
def get_all(self):
21+
def get_all_data(self):
1822
hdrs = _headers(self._config.sdk_key)
19-
uri = self._config.get_latest_flags_uri
23+
uri = self._config.base_uri + LATEST_ALL_URI
2024
r = self._session_cache.get(uri,
2125
headers=hdrs,
2226
timeout=(
2327
self._config.connect_timeout,
2428
self._config.read_timeout))
2529
r.raise_for_status()
26-
flags = r.json()
27-
versions_summary = list(map(lambda f: "{0}:{1}".format(f.get("key"), f.get("version")), flags.values()))
28-
log.debug("Get All flags response status:[{0}] From cache?[{1}] ETag:[{2}] flag versions: {3}"
29-
.format(r.status_code, r.from_cache, r.headers.get('ETag'), versions_summary))
30-
return flags
30+
all_data = r.json()
31+
log.debug("Get All flags response status:[%d] From cache?[%s] ETag:[%s]",
32+
r.status_code, r.from_cache, r.headers.get('ETag'))
33+
return {
34+
FEATURES: all_data['flags'],
35+
SEGMENTS: all_data['segments']
36+
}
3137

32-
def get_one(self, key):
38+
def get_one(self, kind, key):
3339
hdrs = _headers(self._config.sdk_key)
34-
uri = self._config.get_latest_flags_uri + '/' + key
35-
log.debug("Getting one feature flag using uri: " + uri)
40+
path = kind.request_api_path + '/' + key
41+
uri = config.base_uri + path
42+
log.debug("Getting %s from %s using uri: %s", key, kind['namespace'], uri)
3643
r = self._session_no_cache.get(uri,
3744
headers=hdrs,
3845
timeout=(
3946
self._config.connect_timeout,
4047
self._config.read_timeout))
4148
r.raise_for_status()
42-
flag = r.json()
43-
log.debug("Get one flag response status:[{0}] Flag key:[{1}] version:[{2}]"
44-
.format(r.status_code, key, flag.get("version")))
45-
return flag
49+
obj = r.json()
50+
log.debug("%s response status:[%d] key:[%s] version:[%d]",
51+
path, r.status_code, key, segment.get("version"))
52+
return obj

ldclient/feature_store.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,76 @@
1+
from collections import defaultdict
12
from ldclient.util import log
23
from ldclient.interfaces import FeatureStore
34
from ldclient.rwlock import ReadWriteLock
45

56

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

812
def __init__(self):
913
self._lock = ReadWriteLock()
1014
self._initialized = False
11-
self._features = {}
15+
self._items = defaultdict(dict)
1216

13-
def get(self, key, callback):
17+
def get(self, kind, key, callback):
1418
try:
1519
self._lock.rlock()
16-
f = self._features.get(key)
17-
if f is None:
18-
log.debug("Attempted to get missing feature: " + str(key) + " Returning None")
20+
itemsOfKind = self._items[kind]
21+
item = itemsOfKind.get(key)
22+
if item is None:
23+
log.debug("Attempted to get missing key %s in '%s', returning None", key, kind.namespace)
1924
return callback(None)
20-
if 'deleted' in f and f['deleted']:
21-
log.debug("Attempted to get deleted feature: " + str(key) + " Returning None")
25+
if 'deleted' in item and item['deleted']:
26+
log.debug("Attempted to get deleted key %s in '%s', returning None", key, kind.namespace)
2227
return callback(None)
23-
return callback(f)
28+
return callback(item)
2429
finally:
2530
self._lock.runlock()
2631

27-
def all(self, callback):
32+
def all(self, kind, callback):
2833
try:
2934
self._lock.rlock()
30-
return callback(dict((k, f) for k, f in self._features.items() if ('deleted' not in f) or not f['deleted']))
35+
itemsOfKind = self._items[kind]
36+
return callback(dict((k, i) for k, i in itemsOfKind.items() if ('deleted' not in i) or not i['deleted']))
3137
finally:
3238
self._lock.runlock()
3339

34-
def init(self, features):
40+
def init(self, all_data):
3541
try:
36-
self._lock.lock()
37-
self._features = dict(features)
42+
self._lock.rlock()
43+
self._items.clear()
44+
self._items.update(all_data)
3845
self._initialized = True
39-
log.debug("Initialized feature store with " + str(len(features)) + " features")
46+
for k in all_data:
47+
log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k]))
4048
finally:
41-
self._lock.unlock()
49+
self._lock.runlock()
4250

4351
# noinspection PyShadowingNames
44-
def delete(self, key, version):
52+
def delete(self, kind, key, version):
4553
try:
46-
self._lock.lock()
47-
f = self._features.get(key)
48-
if f is not None and f['version'] < version:
49-
f['deleted'] = True
50-
f['version'] = version
51-
elif f is None:
52-
f = {'deleted': True, 'version': version}
53-
self._features[key] = f
54+
self._lock.rlock()
55+
itemsOfKind = self._items[kind]
56+
i = itemsOfKind.get(key)
57+
if i is None or i['version'] < version:
58+
i = {'deleted': True, 'version': version}
59+
itemsOfKind[key] = i
5460
finally:
55-
self._lock.unlock()
61+
self._lock.runlock()
5662

57-
def upsert(self, key, feature):
63+
def upsert(self, kind, item):
64+
key = item['key']
5865
try:
59-
self._lock.lock()
60-
f = self._features.get(key)
61-
if f is None or f['version'] < feature['version']:
62-
self._features[key] = feature
63-
log.debug("Updated feature {0} to version {1}".format(key, feature['version']))
66+
self._lock.rlock()
67+
itemsOfKind = self._items[kind]
68+
i = itemsOfKind.get(key)
69+
if i is None or i['version'] < item['version']:
70+
itemsOfKind[key] = item
71+
log.debug("Updated %s in '%s' to version %d", key, kind.namespace, item['version'])
6472
finally:
65-
self._lock.unlock()
73+
self._lock.runlock()
6674

6775
@property
6876
def initialized(self):

0 commit comments

Comments
 (0)