diff --git a/CHANGELOG.md b/CHANGELOG.md index b6876b10..e9e7346a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly Python SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [4.0.2] - 2017-03-13 +### Added +- Support for Python 2.6. + ## [4.0.1] - 2017-01-10 ### Changed - RedisFeatureStore now returns default when Redis errors occur diff --git a/README.md b/README.md index 49158cb9..be51535a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,17 @@ Your first feature flag else: # the code to run if the feature is off +Python 2.6 +---------- +Python 2.6 is supported for polling mode only and 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, diff --git a/circle.yml b/circle.yml index eba43aed..d706cadf 100644 --- a/circle.yml +++ b/circle.yml @@ -3,20 +3,25 @@ machine: - redis dependencies: pre: + - pyenv shell 2.6.6; $(pyenv which pip) install --upgrade pip setuptools - pyenv shell 2.7.10; $(pyenv which pip) install --upgrade pip setuptools - pyenv shell 3.3.3; $(pyenv which pip) install --upgrade pip setuptools - pyenv shell 3.4.2; $(pyenv which pip) install --upgrade pip setuptools + - pyenv shell 2.6.6; $(pyenv which pip) install -r python2.6-requirements.txt + - pyenv shell 2.6.6; $(pyenv which pip) install -r test-requirements.txt - pyenv shell 2.7.10; $(pyenv which pip) install -r test-requirements.txt - pyenv shell 3.3.3; $(pyenv which pip) install -r test-requirements.txt - pyenv shell 3.4.2; $(pyenv which pip) install -r test-requirements.txt + - pyenv shell 2.6.6; $(pyenv which python) setup.py install - pyenv shell 2.7.10; $(pyenv which python) setup.py install - pyenv shell 3.3.3; $(pyenv which python) setup.py install - pyenv shell 3.4.2; $(pyenv which python) setup.py install test: override: + - pyenv shell 2.6.6; $(pyenv which py.test) testing - pyenv shell 2.7.10; $(pyenv which py.test) testing - pyenv shell 3.3.3; $(pyenv which py.test) -s testing - pyenv shell 3.4.2; $(pyenv which py.test) -s testing diff --git a/ldclient/client.py b/ldclient/client.py index d09b8618..0a31514a 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -217,7 +217,7 @@ def cb(all_flags): return self._store.all(cb) def _evaluate_multi(self, user, flags): - return {k: self._evaluate(v, user)[0] for k, v in flags.items() or {}} + return dict([(k, self._evaluate(v, user)[0]) for k, v in flags.items() or {}]) def secure_mode_hash(self, user): if user.get('key') is None or self._config.sdk_key is None: diff --git a/ldclient/feature_store.py b/ldclient/feature_store.py index e5a0f237..9daf5f9c 100644 --- a/ldclient/feature_store.py +++ b/ldclient/feature_store.py @@ -60,7 +60,7 @@ def upsert(self, key, feature): f = self._features.get(key) if f is None or f['version'] < feature['version']: self._features[key] = feature - log.debug("Updated feature {} to version {}".format(key, feature['version'])) + log.debug("Updated feature {0} to version {1}".format(key, feature['version'])) finally: self._lock.unlock() diff --git a/ldclient/redis_feature_store.py b/ldclient/redis_feature_store.py index 2299bf89..27578dcd 100644 --- a/ldclient/redis_feature_store.py +++ b/ldclient/redis_feature_store.py @@ -23,7 +23,7 @@ def __init__(self, expiration=15, capacity=1000): - self._features_key = "{}:features".format(prefix) + self._features_key = "{0}:features".format(prefix) self._cache = ForgetfulDict() if expiration == 0 else ExpiringDict(max_len=capacity, max_age_seconds=expiration) self._pool = redis.ConnectionPool.from_url(url=url, max_connections=max_connections) diff --git a/ldclient/streaming.py b/ldclient/streaming.py index f6ce61c2..018e6382 100644 --- a/ldclient/streaming.py +++ b/ldclient/streaming.py @@ -58,7 +58,7 @@ def initialized(self): @staticmethod def process_message(store, requester, msg, ready): - log.debug("Received stream event {} with data: {}".format(msg.event, msg.data)) + log.debug("Received stream event {0} with data: {1}".format(msg.event, msg.data)) if msg.event == 'put': payload = json.loads(msg.data) store.init(payload) diff --git a/ldclient/twisted_redis_feature_store.py b/ldclient/twisted_redis_feature_store.py index 2307a335..de2566ed 100644 --- a/ldclient/twisted_redis_feature_store.py +++ b/ldclient/twisted_redis_feature_store.py @@ -23,7 +23,7 @@ def __init__(self, parsed_url = urlparse.urlparse(url) self._redis_host = parsed_url.hostname self._redis_port = parsed_url.port - self._features_key = "{}:features".format(redis_prefix) + self._features_key = "{0}:features".format(redis_prefix) self._cache = ForgetfulDict() if expiration == 0 else ExpiringDict(max_len=capacity, max_age_seconds=expiration) log.info("Created TwistedRedisFeatureStore with url: " + url + " using key: " + self._features_key) diff --git a/ldclient/util.py b/ldclient/util.py index 6fd35201..9e461dce 100644 --- a/ldclient/util.py +++ b/ldclient/util.py @@ -38,7 +38,7 @@ def _headers(sdk_key): def _stream_headers(sdk_key, client="PythonClient"): return {'Authorization': sdk_key, - 'User-Agent': '{}/{}'.format(client, VERSION), + 'User-Agent': '{0}/{1}'.format(client, VERSION), 'Cache-Control': 'no-cache', 'Accept': "text/event-stream"} diff --git a/ldclient/version.py b/ldclient/version.py index 269d18fe..b8e20f02 100644 --- a/ldclient/version.py +++ b/ldclient/version.py @@ -1 +1 @@ -VERSION = "4.0.1" +VERSION = "4.0.2" diff --git a/python2.6-requirements.txt b/python2.6-requirements.txt new file mode 100644 index 00000000..d73f64f0 --- /dev/null +++ b/python2.6-requirements.txt @@ -0,0 +1 @@ +ordereddict>=1.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 6d08498e..fecbf237 100644 --- a/setup.py +++ b/setup.py @@ -3,12 +3,14 @@ except ImportError: from distutils.core import setup +import sys import uuid from pip.req import parse_requirements # parse_requirements() returns generator of pip.req.InstallRequirement objects install_reqs = parse_requirements('requirements.txt', session=uuid.uuid1()) +python26_reqs = parse_requirements('python2.6-requirements.txt', session=uuid.uuid1()) test_reqs = parse_requirements('test-requirements.txt', session=uuid.uuid1()) twisted_reqs = parse_requirements( 'twisted-requirements.txt', session=uuid.uuid1()) @@ -17,6 +19,7 @@ # reqs is a list of requirement # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] reqs = [str(ir.req) for ir in install_reqs] +python26reqs = [str(ir.req) for ir in python26_reqs] testreqs = [str(ir.req) for ir in test_reqs] txreqs = [str(ir.req) for ir in twisted_reqs] redisreqs = [str(ir.req) for ir in redis_reqs] @@ -39,7 +42,7 @@ def run(self): setup( name='ldclient-py', - version='4.0.1', + version='4.0.2', author='LaunchDarkly', author_email='team@launchdarkly.com', packages=['ldclient'], @@ -54,7 +57,8 @@ def run(self): ], extras_require={ "twisted": txreqs, - "redis": redisreqs + "redis": redisreqs, + "python2.6": python26reqs }, tests_require=testreqs, cmdclass={'test': PyTest}, diff --git a/testing/sync_util.py b/testing/sync_util.py index 955b7cc2..403fe7c4 100644 --- a/testing/sync_util.py +++ b/testing/sync_util.py @@ -9,7 +9,7 @@ def wait_until(condition, timeout=5): if result: return result elif time.time() > end_time: - raise Exception("Timeout waiting for {}".format( + raise Exception("Timeout waiting for {0}".format( condition.__name__)) # pragma: no cover else: time.sleep(.1) diff --git a/testing/test_integration_init.py b/testing/test_integration_init.py index 32c6749d..5665ac0b 100644 --- a/testing/test_integration_init.py +++ b/testing/test_integration_init.py @@ -1,4 +1,5 @@ import logging +import sys import pytest @@ -10,7 +11,9 @@ logging.basicConfig(level=logging.DEBUG) -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_set_sdk_key_before_init(): ldclient.set_config(Config.default()) @@ -20,7 +23,9 @@ def test_set_sdk_key_before_init(): ldclient.get().close() -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_set_sdk_key_after_init(): ldclient.set_config(Config.default()) assert ldclient.get().is_initialized() is False @@ -30,7 +35,9 @@ def test_set_sdk_key_after_init(): ldclient.get().close() -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_set_config(): offline_config = ldclient.Config(offline=True) online_config = ldclient.Config(sdk_key=sdk_key, offline=False) @@ -43,4 +50,3 @@ def test_set_config(): wait_until(ldclient.get().is_initialized, timeout=30) ldclient.get().close() - diff --git a/testing/test_integration_ldclient.py b/testing/test_integration_ldclient.py index 819ae152..7dc0ba74 100644 --- a/testing/test_integration_ldclient.py +++ b/testing/test_integration_ldclient.py @@ -1,4 +1,5 @@ import logging +import sys import pytest @@ -10,7 +11,9 @@ logging.basicConfig(level=logging.DEBUG) -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_ctor_with_sdk_key(): client = LDClient(sdk_key=sdk_key) wait_until(client.is_initialized, timeout=10) @@ -18,7 +21,9 @@ def test_ctor_with_sdk_key(): client.close() -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_ctor_with_sdk_key_and_config(): client = LDClient(sdk_key=sdk_key, config=Config.default()) wait_until(client.is_initialized, timeout=10) @@ -26,9 +31,21 @@ def test_ctor_with_sdk_key_and_config(): client.close() -@pytest.mark.skipif(sdk_key is None, reason="requires LD_SDK_KEY environment variable to be set") +# skipping for Python 2.6 since it is incompatible with LaunchDarkly's streaming connection due to SNI +@pytest.mark.skipif(sdk_key is None or sys.version_info < (2, 7), + reason="Requires Python >=2.7 and LD_SDK_KEY environment variable to be set") def test_ctor_with_config(): client = LDClient(config=Config(sdk_key=sdk_key)) wait_until(client.is_initialized, timeout=10) client.close() + + +#polling +@pytest.mark.skipif(sdk_key is None, + reason="requires LD_SDK_KEY environment variable to be set") +def test_ctor_with_config_polling(): + client = LDClient(config=Config(sdk_key=sdk_key, stream=False)) + wait_until(client.is_initialized, timeout=10) + + client.close()