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 setup_ld_client(app): # define and set required env vars redis_prefix = app.config["LD_FRONTEND_KEY"] + "-featurestore" redis_conn = "redis://" + app.config["REDIS_HOST"] + ":6379" if os.environ.get("TESTING") is None or os.environ.get("TESTING") == False: store = Redis.new_feature_store(url=redis_conn, prefix=redis_prefix, caching=CacheConfig.disabled()) elif os.environ.get("FLASK_ENV") == "default": store = InMemoryFeatureStore() else: store = InMemoryFeatureStore() LD_CLIENT_KEY = app.config["LD_CLIENT_KEY"] LD_FRONTEND_KEY = app.config["LD_FRONTEND_KEY"] ld_config = LdConfig(sdk_key=LD_CLIENT_KEY, connect_timeout=30, read_timeout=30, feature_store=store, inline_users_in_events=True) # LaunchDarkly Config # If $LD_RELAY_URL is set, client will be pointed to a relay instance. if "LD_RELAY_URL" in os.environ: base_uri = os.environ.get("LD_RELAY_URL") ld_config = LdConfig( sdk_key=app.config["LD_CLIENT_KEY"], base_uri=base_uri, events_uri=os.environ.get("LD_RELAY_EVENTS_URL", base_uri), stream_uri=os.environ.get("LD_RELAY_STREAM_URL", base_uri), ) new_client = ldclient.LDClient(config=ld_config) return new_client
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 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 __init__(self, url='redis://localhost:6379/0', prefix='launchdarkly', max_connections=16, expiration=15, capacity=1000): self.core = _RedisFeatureStoreCore(url, prefix, max_connections) # exposed for testing self._wrapper = CachingStoreWrapper(self.core, CacheConfig(expiration=expiration, capacity=capacity))
def test_cached_initialized_can_cache_false_result(self): core = MockCore() wrapper = CachingStoreWrapper(core, CacheConfig(expiration=0.2)) # use a shorter cache TTL for this test assert wrapper.initialized is False assert core.inited_query_count == 1 core.inited = True assert wrapper.initialized is False assert core.inited_query_count == 1 sleep(0.5) assert wrapper.initialized is True assert core.inited_query_count == 2 # From this point on it should remain true and the method should not be called assert wrapper.initialized is True assert core.inited_query_count == 2
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}
def make_wrapper(core, cached): return CachingStoreWrapper(core, CacheConfig(expiration=30) if cached else CacheConfig.disabled())
ldclient.set_config(config) client = ldclient.get() elif settings.FEATURE_FLAGS_OFFLINE: # On-prem usage, without feature flags file ldclient.set_config( Config(settings.FEATURE_FLAGS_API_KEY or 'whatever', offline=True)) client = ldclient.get() else: # Production usage if hasattr(settings, 'REDIS_LOCATION'): logger.debug( f'Set LaunchDarkly config with Redis feature store at {settings.REDIS_LOCATION}' ) store = Redis.new_feature_store(url=settings.REDIS_LOCATION, prefix='feature-flags', caching=CacheConfig(expiration=30)) ldclient.set_config( Config(settings.FEATURE_FLAGS_API_KEY, feature_store=store, http=HTTPConfig(connect_timeout=5))) else: logger.debug('Set LaunchDarkly config without Redis...') ldclient.set_config( Config(settings.FEATURE_FLAGS_API_KEY, http=HTTPConfig(connect_timeout=5))) client = ldclient.get() def _get_user_repr(user): """Turn user object into dict with required properties""" from users.serializers import UserSerializer