Beispiel #1
0
    def new_feature_store(table_name,
                          prefix=None,
                          dynamodb_opts={},
                          caching=CacheConfig.default()):
        """Creates a DynamoDB-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``boto3`` package containing the AWS SDK gems.
        Then, put the object returned by this method into the ``feature_store`` property of your
        client configuration (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import DynamoDB
            store = DynamoDB.new_feature_store("my-table-name")
            config = Config(feature_store=store)

        Note that the DynamoDB table must already exist; the LaunchDarkly SDK does not create the table
        automatically, because it has no way of knowing what additional properties (such as permissions
        and throughput) you would want it to have. The table must have a partition key called
        "namespace" and a sort key called "key", both with a string type.

        By default, the DynamoDB client will try to get your AWS credentials and region name from
        environment variables and/or local configuration files, as described in the AWS SDK documentation.
        You may also pass configuration settings in ``dynamodb_opts``.

        :param string table_name: the name of an existing DynamoDB table
        :param string prefix: an optional namespace prefix to be prepended to all DynamoDB keys
        :param dict dynamodb_opts: optional parameters for configuring the DynamoDB client, as defined in
          the `boto3 API <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`_
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _DynamoDBFeatureStoreCore(table_name, prefix, dynamodb_opts)
        return CachingStoreWrapper(core, caching)
Beispiel #2
0
    def new_feature_store(host=None,
                          port=None,
                          prefix=None,
                          consul_opts=None,
                          caching=CacheConfig.default()):
        """Creates a Consul-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``python-consul`` package. Then, put the object
        returned by this method into the ``feature_store`` property of your client configuration
        (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import Consul
            store = Consul.new_feature_store()
            config = Config(feature_store=store)

        Note that ``python-consul`` is not available for Python 3.3 or 3.4, so this feature cannot be
        used in those Python versions.

        :param string host: hostname of the Consul server (uses ``localhost`` if omitted)
        :param int port: port of the Consul server (uses 8500 if omitted)
        :param string prefix: a namespace prefix to be prepended to all Consul keys
        :param dict consul_opts: optional parameters for configuring the Consul client, if you need
          to set any of them besides host and port, as defined in the
          `python-consul API <https://python-consul.readthedocs.io/en/latest/#consul>`_
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _ConsulFeatureStoreCore(host, port, prefix, consul_opts)
        return CachingStoreWrapper(core, caching)
Beispiel #3
0
    def new_feature_store(url='redis://localhost:6379/0',
                          prefix='launchdarkly',
                          max_connections=16,
                          caching=CacheConfig.default()):
        """Creates a Redis-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``redis`` package. Then, put the object
        returned by this method into the ``feature_store`` property of your client configuration
        (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import Redis
            store = Redis.new_feature_store()
            config = Config(feature_store=store)

        :param string url: the URL of the Redis host; defaults to ``DEFAULT_URL``
        :param string prefix: a namespace prefix to be prepended to all Redis keys; defaults to
          ``DEFAULT_PREFIX``
        :param int max_connections: the maximum number of Redis connections to keep in the
          connection pool; defaults to ``DEFAULT_MAX_CONNECTIONS``
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _RedisFeatureStoreCore(url, prefix, max_connections)
        wrapper = CachingStoreWrapper(core, caching)
        wrapper.core = core  # exposed for testing
        return wrapper
    def new_feature_store(table_name,
                          prefix=None,
                          dynamodb_opts={},
                          caching=CacheConfig.default()):
        """Creates a DynamoDB-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``boto3`` package containing the AWS SDK gems.
        Then, put the object returned by this method into the ``feature_store`` property of your
        client configuration (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import DynamoDB
            store = DynamoDB.new_feature_store("my-table-name")
            config = Config(feature_store=store)

        Note that the DynamoDB table must already exist; the LaunchDarkly SDK does not create the table
        automatically, because it has no way of knowing what additional properties (such as permissions
        and throughput) you would want it to have. The table must have a partition key called
        "namespace" and a sort key called "key", both with a string type.

        By default, the DynamoDB client will try to get your AWS credentials and region name from
        environment variables and/or local configuration files, as described in the AWS SDK documentation.
        You may also pass configuration settings in ``dynamodb_opts``.

        :param string table_name: the name of an existing DynamoDB table
        :param string prefix: an optional namespace prefix to be prepended to all DynamoDB keys
        :param dict dynamodb_opts: optional parameters for configuring the DynamoDB client, as defined in
          the `boto3 API <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`_
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _DynamoDBFeatureStoreCore(table_name, prefix, dynamodb_opts)
        return CachingStoreWrapper(core, caching)
    def new_feature_store(host=None,
                          port=None,
                          prefix=None,
                          consul_opts=None,
                          caching=CacheConfig.default()):
        """Creates a Consul-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``python-consul`` package. Then, put the object
        returned by this method into the ``feature_store`` property of your client configuration
        (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import Consul
            store = Consul.new_feature_store()
            config = Config(feature_store=store)

        Note that ``python-consul`` is not available for Python 3.3 or 3.4, so this feature cannot be
        used in those Python versions.

        :param string host: hostname of the Consul server (uses ``localhost`` if omitted)
        :param int port: port of the Consul server (uses 8500 if omitted)
        :param string prefix: a namespace prefix to be prepended to all Consul keys
        :param dict consul_opts: optional parameters for configuring the Consul client, if you need
          to set any of them besides host and port, as defined in the
          `python-consul API <https://python-consul.readthedocs.io/en/latest/#consul>`_
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _ConsulFeatureStoreCore(host, port, prefix, consul_opts)
        return CachingStoreWrapper(core, caching)
    def new_feature_store(url='redis://localhost:6379/0',
                          prefix='launchdarkly',
                          max_connections=16,
                          caching=CacheConfig.default()):
        """Creates a Redis-backed implementation of :class:`ldclient.interfaces.FeatureStore`.
        For more details about how and why you can use a persistent feature store, see the
        `SDK reference guide <https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store>`_.

        To use this method, you must first install the ``redis`` package. Then, put the object
        returned by this method into the ``feature_store`` property of your client configuration
        (:class:`ldclient.config.Config`).
        ::

            from ldclient.integrations import Redis
            store = Redis.new_feature_store()
            config = Config(feature_store=store)

        :param string url: the URL of the Redis host; defaults to ``DEFAULT_URL``
        :param string prefix: a namespace prefix to be prepended to all Redis keys; defaults to
          ``DEFAULT_PREFIX``
        :param int max_connections: the maximum number of Redis connections to keep in the
          connection pool; defaults to ``DEFAULT_MAX_CONNECTIONS``
        :param CacheConfig caching: specifies whether local caching should be enabled and if so,
          sets the cache properties; defaults to :func:`ldclient.feature_store.CacheConfig.default()`
        """
        core = _RedisFeatureStoreCore(url, prefix, max_connections)
        wrapper = CachingStoreWrapper(core, caching)
        wrapper.core = core  # exposed for testing
        return wrapper
def test_create_diagnostic_config_custom():
    test_store = CachingStoreWrapper(_TestStoreForDiagnostics(),
                                     CacheConfig.default())
    test_config = Config("SDK_KEY",
                         base_uri='https://test.com',
                         events_uri='https://test.com',
                         events_max_pending=10,
                         flush_interval=1,
                         stream_uri='https://test.com',
                         stream=False,
                         poll_interval=60,
                         use_ldd=True,
                         feature_store=test_store,
                         all_attributes_private=True,
                         user_keys_capacity=10,
                         user_keys_flush_interval=60,
                         inline_users_in_events=True,
                         http=HTTPConfig(http_proxy='proxy',
                                         read_timeout=1,
                                         connect_timeout=1),
                         diagnostic_recording_interval=60)
    diag_config = _create_diagnostic_config_object(test_config)

    assert len(diag_config) == 17
    assert diag_config['customBaseURI'] is True
    assert diag_config['customEventsURI'] is True
    assert diag_config['customStreamURI'] is True
    assert diag_config['eventsCapacity'] == 10
    assert diag_config['connectTimeoutMillis'] == 1000
    assert diag_config['socketTimeoutMillis'] == 1000
    assert diag_config['eventsFlushIntervalMillis'] == 1000
    assert diag_config['usingProxy'] is True
    assert diag_config['streamingDisabled'] is True
    assert diag_config['usingRelayDaemon'] is True
    assert diag_config['allAttributesPrivate'] is True
    assert diag_config['pollingIntervalMillis'] == 60000
    assert diag_config['userKeysCapacity'] == 10
    assert diag_config['userKeysFlushIntervalMillis'] == 60000
    assert diag_config['inlineUsersInEvents'] is True
    assert diag_config['diagnosticRecordingIntervalMillis'] == 60000
    assert diag_config['dataStoreType'] == 'MyFavoriteStore'
class TestFeatureStore:
    params = []  # type: List[Tester]
    if skip_db_tests:
        params += [InMemoryTester()]
    else:
        params += [
            InMemoryTester(),
            RedisTester(CacheConfig.default()),
            RedisTester(CacheConfig.disabled()),
            DynamoDBTester(CacheConfig.default()),
            DynamoDBTester(CacheConfig.disabled())
        ]
        if have_consul:
            params.append(ConsulTester(CacheConfig.default()))
            params.append(ConsulTester(CacheConfig.disabled()))

    @pytest.fixture(params=params)
    def tester(self, request):
        return request.param

    @pytest.fixture(params=params)
    def store(self, request):
        return request.param.init_store()

    @staticmethod
    def make_feature(key, ver):
        return {
            u'key':
            key,
            u'version':
            ver,
            u'salt':
            u'abc',
            u'on':
            True,
            u'variations': [{
                u'value': True,
                u'weight': 100,
                u'targets': []
            }, {
                u'value': False,
                u'weight': 0,
                u'targets': []
            }]
        }

    def base_initialized_store(self, store):
        store.init({
            FEATURES: {
                'foo': self.make_feature('foo', 10),
                'bar': self.make_feature('bar', 10),
            }
        })
        return store

    def test_not_initialized_before_init(self, store):
        assert store.initialized is False

    def test_initialized(self, store):
        store = self.base_initialized_store(store)
        assert store.initialized is True

    def test_get_existing_feature(self, store):
        store = self.base_initialized_store(store)
        expected = self.make_feature('foo', 10)
        assert store.get(FEATURES, 'foo', lambda x: x) == expected

    def test_get_nonexisting_feature(self, store):
        store = self.base_initialized_store(store)
        assert store.get(FEATURES, 'biz', lambda x: x) is None

    def test_get_all_versions(self, store):
        store = self.base_initialized_store(store)
        result = store.all(FEATURES, lambda x: x)
        assert len(result) == 2
        assert result.get('foo') == self.make_feature('foo', 10)
        assert result.get('bar') == self.make_feature('bar', 10)

    def test_upsert_with_newer_version(self, store):
        store = self.base_initialized_store(store)
        new_ver = self.make_feature('foo', 11)
        store.upsert(FEATURES, new_ver)
        assert store.get(FEATURES, 'foo', lambda x: x) == new_ver

    def test_upsert_with_older_version(self, store):
        store = self.base_initialized_store(store)
        new_ver = self.make_feature('foo', 9)
        expected = self.make_feature('foo', 10)
        store.upsert(FEATURES, new_ver)
        assert store.get(FEATURES, 'foo', lambda x: x) == expected

    def test_upsert_with_new_feature(self, store):
        store = self.base_initialized_store(store)
        new_ver = self.make_feature('biz', 1)
        store.upsert(FEATURES, new_ver)
        assert store.get(FEATURES, 'biz', lambda x: x) == new_ver

    def test_delete_with_newer_version(self, store):
        store = self.base_initialized_store(store)
        store.delete(FEATURES, 'foo', 11)
        assert store.get(FEATURES, 'foo', lambda x: x) is None

    def test_delete_unknown_feature(self, store):
        store = self.base_initialized_store(store)
        store.delete(FEATURES, 'biz', 11)
        assert store.get(FEATURES, 'biz', lambda x: x) is None

    def test_delete_with_older_version(self, store):
        store = self.base_initialized_store(store)
        store.delete(FEATURES, 'foo', 9)
        expected = self.make_feature('foo', 10)
        assert store.get(FEATURES, 'foo', lambda x: x) == expected

    def test_upsert_older_version_after_delete(self, store):
        store = self.base_initialized_store(store)
        store.delete(FEATURES, 'foo', 11)
        old_ver = self.make_feature('foo', 9)
        store.upsert(FEATURES, old_ver)
        assert store.get(FEATURES, 'foo', lambda x: x) is None

    def test_stores_with_different_prefixes_are_independent(self, tester):
        # This verifies that init(), get(), all(), and upsert() are all correctly using the specified key prefix.
        # The delete() method isn't tested separately because it's implemented as a variant of upsert().
        if not tester.supports_prefix:
            return

        flag_a1 = {'key': 'flagA1', 'version': 1}
        flag_a2 = {'key': 'flagA2', 'version': 1}
        flag_b1 = {'key': 'flagB1', 'version': 1}
        flag_b2 = {'key': 'flagB2', 'version': 1}
        store_a = tester.init_store('a')
        store_b = tester.init_store('b')

        store_a.init({FEATURES: {'flagA1': flag_a1}})
        store_a.upsert(FEATURES, flag_a2)

        store_b.init({FEATURES: {'flagB1': flag_b1}})
        store_b.upsert(FEATURES, flag_b2)

        item = store_a.get(FEATURES, 'flagA1', lambda x: x)
        assert item == flag_a1
        item = store_a.get(FEATURES, 'flagB1', lambda x: x)
        assert item is None
        items = store_a.all(FEATURES, lambda x: x)
        assert items == {'flagA1': flag_a1, 'flagA2': flag_a2}

        item = store_b.get(FEATURES, 'flagB1', lambda x: x)
        assert item == flag_b1
        item = store_b.get(FEATURES, 'flagA1', lambda x: x)
        assert item is None
        items = store_b.all(FEATURES, lambda x: x)
        assert items == {'flagB1': flag_b1, 'flagB2': flag_b2}