Skip to content

Commit 4500f55

Browse files
authored
Merge pull request #25 from configcat/sdk-improvements
Sdk improvements
2 parents 7c38d38 + 3241212 commit 4500f55

19 files changed

+594
-178
lines changed

configcatclient/__init__.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ def create_client(sdk_key, data_governance=DataGovernance.Global):
1818

1919
def create_client_with_auto_poll(sdk_key, poll_interval_seconds=60, max_init_wait_time_seconds=5,
2020
on_configuration_changed_callback=None, config_cache_class=None,
21-
base_url=None, proxies=None, proxy_auth=None, data_governance=DataGovernance.Global):
21+
base_url=None, proxies=None, proxy_auth=None, connect_timeout=10, read_timeout=30,
22+
flag_overrides=None,
23+
data_governance=DataGovernance.Global):
2224
"""
2325
Create an instance of ConfigCatClient and setup Auto Poll mode with custom options
2426
@@ -31,6 +33,10 @@ def create_client_with_auto_poll(sdk_key, poll_interval_seconds=60, max_init_wai
3133
:param base_url: You can set a base_url if you want to use a proxy server between your application and ConfigCat
3234
:param proxies: Proxy addresses. e.g. { 'https': 'your_proxy_ip:your_proxy_port' }
3335
:param proxy_auth: Proxy authentication. e.g. HTTPProxyAuth('username', 'password')
36+
:param connect_timeout: The number of seconds to wait for the server to make the initial connection
37+
(i.e. completing the TCP connection handshake). Default: 10 seconds.
38+
:param read_timeout: The number of seconds to wait for the server to respond before giving up. Default: 30 seconds.
39+
:param flag_overrides: An OverrideDataSource implementation used to override feature flags & settings.
3440
:param data_governance:
3541
Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: \n
3642
https://app.configcat.com/organization/data-governance \n
@@ -46,13 +52,25 @@ def create_client_with_auto_poll(sdk_key, poll_interval_seconds=60, max_init_wai
4652
if max_init_wait_time_seconds < 0:
4753
max_init_wait_time_seconds = 0
4854

49-
return ConfigCatClient(sdk_key, poll_interval_seconds, max_init_wait_time_seconds,
50-
on_configuration_changed_callback, 0, config_cache_class, base_url, proxies, proxy_auth,
51-
data_governance)
55+
return ConfigCatClient(sdk_key=sdk_key,
56+
poll_interval_seconds=poll_interval_seconds,
57+
max_init_wait_time_seconds=max_init_wait_time_seconds,
58+
on_configuration_changed_callback=on_configuration_changed_callback,
59+
cache_time_to_live_seconds=0,
60+
config_cache_class=config_cache_class,
61+
base_url=base_url,
62+
proxies=proxies,
63+
proxy_auth=proxy_auth,
64+
connect_timeout=connect_timeout,
65+
read_timeout=read_timeout,
66+
flag_overrides=flag_overrides,
67+
data_governance=data_governance)
5268

5369

5470
def create_client_with_lazy_load(sdk_key, cache_time_to_live_seconds=60, config_cache_class=None,
55-
base_url=None, proxies=None, proxy_auth=None, data_governance=DataGovernance.Global):
71+
base_url=None, proxies=None, proxy_auth=None, connect_timeout=10, read_timeout=30,
72+
flag_overrides=None,
73+
data_governance=DataGovernance.Global):
5674
"""
5775
Create an instance of ConfigCatClient and setup Lazy Load mode with custom options
5876
@@ -63,6 +81,10 @@ def create_client_with_lazy_load(sdk_key, cache_time_to_live_seconds=60, config_
6381
:param base_url: You can set a base_url if you want to use a proxy server between your application and ConfigCat
6482
:param proxies: Proxy addresses. e.g. { "https": "your_proxy_ip:your_proxy_port" }
6583
:param proxy_auth: Proxy authentication. e.g. HTTPProxyAuth('username', 'password')
84+
:param connect_timeout: The number of seconds to wait for the server to make the initial connection
85+
(i.e. completing the TCP connection handshake). Default: 10 seconds.
86+
:param read_timeout: The number of seconds to wait for the server to respond before giving up. Default: 30 seconds.
87+
:param flag_overrides: An OverrideDataSource implementation used to override feature flags & settings.
6688
:param data_governance:
6789
Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: \n
6890
https://app.configcat.com/organization/data-governance \n
@@ -75,12 +97,25 @@ def create_client_with_lazy_load(sdk_key, cache_time_to_live_seconds=60, config_
7597
if cache_time_to_live_seconds < 1:
7698
cache_time_to_live_seconds = 1
7799

78-
return ConfigCatClient(sdk_key, 0, 0, None, cache_time_to_live_seconds, config_cache_class, base_url,
79-
proxies, proxy_auth, data_governance)
100+
return ConfigCatClient(sdk_key=sdk_key,
101+
poll_interval_seconds=0,
102+
max_init_wait_time_seconds=0,
103+
on_configuration_changed_callback=None,
104+
cache_time_to_live_seconds=cache_time_to_live_seconds,
105+
config_cache_class=config_cache_class,
106+
base_url=base_url,
107+
proxies=proxies,
108+
proxy_auth=proxy_auth,
109+
connect_timeout=connect_timeout,
110+
read_timeout=read_timeout,
111+
flag_overrides=flag_overrides,
112+
data_governance=data_governance)
80113

81114

82115
def create_client_with_manual_poll(sdk_key, config_cache_class=None,
83-
base_url=None, proxies=None, proxy_auth=None, data_governance=DataGovernance.Global):
116+
base_url=None, proxies=None, proxy_auth=None, connect_timeout=10, read_timeout=30,
117+
flag_overrides=None,
118+
data_governance=DataGovernance.Global):
84119
"""
85120
Create an instance of ConfigCatClient and setup Manual Poll mode with custom options
86121
@@ -90,6 +125,10 @@ def create_client_with_manual_poll(sdk_key, config_cache_class=None,
90125
:param base_url: You can set a base_url if you want to use a proxy server between your application and ConfigCat
91126
:param proxies: Proxy addresses. e.g. { "https": "your_proxy_ip:your_proxy_port" }
92127
:param proxy_auth: Proxy authentication. e.g. HTTPProxyAuth('username', 'password')
128+
:param connect_timeout: The number of seconds to wait for the server to make the initial connection
129+
(i.e. completing the TCP connection handshake). Default: 10 seconds.
130+
:param read_timeout: The number of seconds to wait for the server to respond before giving up. Default: 30 seconds.
131+
:param flag_overrides: An OverrideDataSource implementation used to override feature flags & settings.
93132
:param data_governance:
94133
Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: \n
95134
https://app.configcat.com/organization/data-governance \n
@@ -99,4 +138,16 @@ def create_client_with_manual_poll(sdk_key, config_cache_class=None,
99138
if sdk_key is None:
100139
raise ConfigCatClientException('SDK Key is required.')
101140

102-
return ConfigCatClient(sdk_key, 0, 0, None, 0, config_cache_class, base_url, proxies, proxy_auth, data_governance)
141+
return ConfigCatClient(sdk_key=sdk_key,
142+
poll_interval_seconds=0,
143+
max_init_wait_time_seconds=0,
144+
on_configuration_changed_callback=None,
145+
cache_time_to_live_seconds=0,
146+
config_cache_class=config_cache_class,
147+
base_url=base_url,
148+
proxies=proxies,
149+
proxy_auth=proxy_auth,
150+
connect_timeout=connect_timeout,
151+
read_timeout=read_timeout,
152+
flag_overrides=flag_overrides,
153+
data_governance=data_governance)

configcatclient/autopollingcachepolicy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import time
55
from threading import Thread, Event
66
from requests import HTTPError
7+
from requests import Timeout
78

89
from .readwritelock import ReadWriteLock
910
from .interfaces import CachePolicy
@@ -93,6 +94,9 @@ def force_refresh(self):
9394
except HTTPError as e:
9495
log.error('Double-check your SDK Key at https://app.configcat.com/sdkkey.'
9596
' Received unexpected response: %s' % str(e.response))
97+
except Timeout:
98+
log.exception('Request timed out. Timeout values: [connect: {}s, read: {}s]'.format(
99+
self._config_fetcher.get_connect_timeout(), self._config_fetcher.get_read_timeout()))
96100
except Exception:
97101
log.exception(sys.exc_info()[0])
98102

configcatclient/configcatclient.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
from .configfetcher import ConfigFetcher
77
from .configcache import InMemoryConfigCache
88
from .datagovernance import DataGovernance
9+
from .overridedatasource import OverrideBehaviour
910
from .rolloutevaluator import RolloutEvaluator
1011
import logging
1112
import sys
1213
import hashlib
1314
from collections import namedtuple
15+
import copy
1416

1517
log = logging.getLogger(sys.modules[__name__].__name__)
1618

1719
KeyValue = namedtuple('KeyValue', 'key value')
1820

1921

2022
class ConfigCatClient(object):
23+
sdk_keys = []
2124

2225
def __init__(self,
2326
sdk_key,
@@ -29,37 +32,52 @@ def __init__(self,
2932
base_url=None,
3033
proxies=None,
3134
proxy_auth=None,
35+
connect_timeout=10,
36+
read_timeout=30,
37+
flag_overrides=None,
3238
data_governance=DataGovernance.Global):
3339

3440
if sdk_key is None:
3541
raise ConfigCatClientException('SDK Key is required.')
3642

43+
if sdk_key in ConfigCatClient.sdk_keys:
44+
log.warning('A ConfigCat Client is already initialized with sdk_key %s. '
45+
'We strongly recommend you to use the ConfigCat Client as '
46+
'a Singleton object in your application.' % sdk_key)
47+
else:
48+
ConfigCatClient.sdk_keys.append(sdk_key)
49+
3750
self._sdk_key = sdk_key
51+
self._override_data_source = flag_overrides
3852
self._rollout_evaluator = RolloutEvaluator()
3953

4054
if config_cache_class:
4155
self._config_cache = config_cache_class()
4256
else:
4357
self._config_cache = InMemoryConfigCache()
4458

45-
if poll_interval_seconds > 0:
46-
self._config_fetcher = ConfigFetcher(sdk_key, 'p', base_url, proxies, proxy_auth, data_governance)
47-
self._cache_policy = AutoPollingCachePolicy(self._config_fetcher, self._config_cache,
48-
self.__get_cache_key(),
49-
poll_interval_seconds, max_init_wait_time_seconds,
50-
on_configuration_changed_callback)
51-
elif cache_time_to_live_seconds > 0:
52-
self._config_fetcher = ConfigFetcher(sdk_key, 'l', base_url, proxies, proxy_auth, data_governance)
53-
self._cache_policy = LazyLoadingCachePolicy(self._config_fetcher, self._config_cache,
54-
self.__get_cache_key(),
55-
cache_time_to_live_seconds)
59+
if self._override_data_source and self._override_data_source.get_behaviour() == OverrideBehaviour.LocalOnly:
60+
self._config_fetcher = None
61+
self._cache_policy = None
5662
else:
57-
self._config_fetcher = ConfigFetcher(sdk_key, 'm', base_url, proxies, proxy_auth, data_governance)
58-
self._cache_policy = ManualPollingCachePolicy(self._config_fetcher, self._config_cache,
59-
self.__get_cache_key())
63+
if poll_interval_seconds > 0:
64+
self._config_fetcher = ConfigFetcher(sdk_key, 'p', base_url, proxies, proxy_auth, connect_timeout, read_timeout, data_governance)
65+
self._cache_policy = AutoPollingCachePolicy(self._config_fetcher, self._config_cache,
66+
self.__get_cache_key(),
67+
poll_interval_seconds, max_init_wait_time_seconds,
68+
on_configuration_changed_callback)
69+
elif cache_time_to_live_seconds > 0:
70+
self._config_fetcher = ConfigFetcher(sdk_key, 'l', base_url, proxies, proxy_auth, connect_timeout, read_timeout, data_governance)
71+
self._cache_policy = LazyLoadingCachePolicy(self._config_fetcher, self._config_cache,
72+
self.__get_cache_key(),
73+
cache_time_to_live_seconds)
74+
else:
75+
self._config_fetcher = ConfigFetcher(sdk_key, 'm', base_url, proxies, proxy_auth, connect_timeout, read_timeout, data_governance)
76+
self._cache_policy = ManualPollingCachePolicy(self._config_fetcher, self._config_cache,
77+
self.__get_cache_key())
6078

6179
def get_value(self, key, default_value, user=None):
62-
config = self._cache_policy.get()
80+
config = self.__get_settings()
6381
if config is None:
6482
log.warning('Evaluating get_value(\'%s\') failed. Cache is empty. '
6583
'Returning default_value in your get_value call: [%s].' %
@@ -70,7 +88,7 @@ def get_value(self, key, default_value, user=None):
7088
return value
7189

7290
def get_all_keys(self):
73-
config = self._cache_policy.get()
91+
config = self.__get_settings()
7492
if config is None:
7593
return []
7694

@@ -81,7 +99,7 @@ def get_all_keys(self):
8199
return list(feature_flags)
82100

83101
def get_variation_id(self, key, default_variation_id, user=None):
84-
config = self._cache_policy.get()
102+
config = self.__get_settings()
85103
if config is None:
86104
log.warning('Evaluating get_variation_id(\'%s\') failed. Cache is empty. '
87105
'Returning default_variation_id in your get_variation_id call: [%s].' %
@@ -102,7 +120,7 @@ def get_all_variation_ids(self, user=None):
102120
return variation_ids
103121

104122
def get_key_and_value(self, variation_id):
105-
config = self._cache_policy.get()
123+
config = self.__get_settings()
106124
if config is None:
107125
log.warning('Evaluating get_key_and_value(\'%s\') failed. Cache is empty. '
108126
'Returning None.' % variation_id)
@@ -131,11 +149,47 @@ def get_key_and_value(self, variation_id):
131149
log.error('Could not find the setting for the given variation_id: ' + variation_id)
132150
return None
133151

152+
def get_all_values(self, user=None):
153+
keys = self.get_all_keys()
154+
all_values = {}
155+
for key in keys:
156+
value = self.get_value(key, None, user)
157+
if value is not None:
158+
all_values[key] = value
159+
160+
return all_values
161+
134162
def force_refresh(self):
135-
self._cache_policy.force_refresh()
163+
if self._cache_policy:
164+
self._cache_policy.force_refresh()
136165

137166
def stop(self):
138-
self._cache_policy.stop()
167+
if self._cache_policy:
168+
self._cache_policy.stop()
169+
ConfigCatClient.sdk_keys.remove(self._sdk_key)
170+
171+
def __get_settings(self):
172+
if self._override_data_source:
173+
behaviour = self._override_data_source.get_behaviour()
174+
175+
if behaviour == OverrideBehaviour.LocalOnly:
176+
return self._override_data_source.get_overrides()
177+
elif behaviour == OverrideBehaviour.RemoteOverLocal:
178+
remote_settings = self._cache_policy.get()
179+
local_settings = self._override_data_source.get_overrides()
180+
result = copy.deepcopy(local_settings)
181+
if FEATURE_FLAGS in remote_settings and FEATURE_FLAGS in local_settings:
182+
result[FEATURE_FLAGS].update(remote_settings[FEATURE_FLAGS])
183+
return result
184+
elif behaviour == OverrideBehaviour.LocalOverRemote:
185+
remote_settings = self._cache_policy.get()
186+
local_settings = self._override_data_source.get_overrides()
187+
result = copy.deepcopy(remote_settings)
188+
if FEATURE_FLAGS in remote_settings and FEATURE_FLAGS in local_settings:
189+
result[FEATURE_FLAGS].update(local_settings[FEATURE_FLAGS])
190+
return result
191+
192+
return self._cache_policy.get()
139193

140194
def __get_cache_key(self):
141195
return hashlib.sha1(('python_' + CONFIG_FILE_NAME + '_' + self._sdk_key).encode('utf-8'))

configcatclient/configfetcher.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ def is_not_modified(self):
4949

5050
class ConfigFetcher(object):
5151
def __init__(self, sdk_key, mode, base_url=None, proxies=None, proxy_auth=None,
52-
data_governance=DataGovernance.Global):
52+
connect_timeout=10, read_timeout=30, data_governance=DataGovernance.Global):
5353
self._sdk_key = sdk_key
5454
self._proxies = proxies
5555
self._proxy_auth = proxy_auth
56+
self._connect_timeout = connect_timeout
57+
self._read_timeout = read_timeout
5658
self._etag = ''
5759
self._headers = {'User-Agent': 'ConfigCat-Python/' + mode + '-' + CONFIGCATCLIENT_VERSION,
5860
'X-ConfigCat-UserAgent': 'ConfigCat-Python/' + mode + '-' + CONFIGCATCLIENT_VERSION,
@@ -67,6 +69,12 @@ def __init__(self, sdk_key, mode, base_url=None, proxies=None, proxy_auth=None,
6769
else:
6870
self._base_url = BASE_URL_GLOBAL
6971

72+
def get_connect_timeout(self):
73+
return self._connect_timeout
74+
75+
def get_read_timeout(self):
76+
return self._read_timeout
77+
7078
def get_configuration_json(self, force_fetch=False, retries=0):
7179
"""
7280
:return: Returns the FetchResponse object contains configuration json Dictionary
@@ -78,7 +86,7 @@ def get_configuration_json(self, force_fetch=False, retries=0):
7886
else:
7987
headers['If-None-Match'] = None
8088

81-
response = requests.get(uri, headers=headers, timeout=(10, 30),
89+
response = requests.get(uri, headers=headers, timeout=(self._connect_timeout, self._read_timeout),
8290
proxies=self._proxies, auth=self._proxy_auth)
8391
response.raise_for_status()
8492
etag = response.headers.get('Etag')
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from .constants import VALUE, FEATURE_FLAGS
2+
from .overridedatasource import OverrideDataSource
3+
4+
5+
class LocalDictionaryDataSource(OverrideDataSource):
6+
def __init__(self, source, override_behaviour):
7+
OverrideDataSource.__init__(self, override_behaviour=override_behaviour)
8+
dictionary = {}
9+
for key, value in source.items():
10+
dictionary[key] = {VALUE: value}
11+
self._settings = {FEATURE_FLAGS: dictionary}
12+
13+
def get_overrides(self):
14+
return self._settings

0 commit comments

Comments
 (0)