Skip to content

Commit af35a1b

Browse files
prepare 6.13.0 release (#142)
1 parent f432fdb commit af35a1b

25 files changed

+1082
-375
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ jobs:
171171
pip install -r consul-requirements.txt
172172
python setup.py install
173173
- run:
174-
name: run tests (2.7)
174+
name: run tests
175175
command: |
176176
mkdir test-reports
177177
$env:Path += ";C:\Python27\;C:\Python27\Scripts\" # has no effect if 2.7 isn't installed

ldclient/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ def get():
122122
__lock.unlock()
123123

124124

125+
# for testing only
126+
def _reset_client():
127+
global __client
128+
global __lock
129+
try:
130+
__lock.lock()
131+
c = __client
132+
__client = None
133+
finally:
134+
__lock.unlock()
135+
if c:
136+
c.close()
137+
138+
125139
# currently hidden from documentation - see docs/README.md
126140
class NullHandler(logging.Handler):
127141
"""A :class:`logging.Handler` implementation that does nothing.

ldclient/config.py

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,71 @@
1111
STREAM_FLAGS_PATH = '/flags'
1212

1313

14+
class HTTPConfig(object):
15+
"""Advanced HTTP configuration options for the SDK client.
16+
17+
This class groups together HTTP/HTTPS-related configuration properties that rarely need to be changed.
18+
If you need to set these, construct an `HTTPConfig` instance and pass it as the `http` parameter when
19+
you construct the main :class:`Config` for the SDK client.
20+
21+
For some of these properties, :class:`Config` also has properties with the same names; the latter are
22+
deprecated and will be removed in the future, and if you specify an `HTTPConfig` instance then the
23+
corresponding `Config` properties will be ignored.
24+
"""
25+
def __init__(self,
26+
connect_timeout=10,
27+
read_timeout=15,
28+
http_proxy=None,
29+
ca_certs=None,
30+
cert_file=None,
31+
disable_ssl_verification=False):
32+
"""
33+
:param float connect_timeout: The connect timeout for network connections in seconds.
34+
:param float read_timeout: The read timeout for network connections in seconds.
35+
:param http_proxy: Use a proxy when connecting to LaunchDarkly. This is the full URI of the
36+
proxy; for example: http://my-proxy.com:1234. Note that unlike the standard `http_proxy` environment
37+
variable, this is used regardless of whether the target URI is HTTP or HTTPS (the actual LaunchDarkly
38+
service uses HTTPS, but a Relay Proxy instance could use HTTP). Setting this Config parameter will
39+
override any proxy specified by an environment variable, but only for LaunchDarkly SDK connections.
40+
:param string ca_certs: If using a custom certificate authority, set this to the file path of the
41+
certificate bundle.
42+
:param string cert_file: If using a custom client certificate, set this to the file path of the
43+
certificate.
44+
:param bool disable_ssl_verification: If true, completely disables SSL verification and certificate
45+
verification for secure requests. This is unsafe and should not be used in a production environment;
46+
instead, use a self-signed certificate and set `ca_certs`.
47+
"""
48+
self.__connect_timeout = connect_timeout
49+
self.__read_timeout = read_timeout
50+
self.__http_proxy = http_proxy
51+
self.__ca_certs = ca_certs
52+
self.__cert_file = cert_file
53+
self.__disable_ssl_verification = disable_ssl_verification
54+
55+
@property
56+
def connect_timeout(self):
57+
return self.__connect_timeout
58+
59+
@property
60+
def read_timeout(self):
61+
return self.__read_timeout
62+
63+
@property
64+
def http_proxy(self):
65+
return self.__http_proxy
66+
67+
@property
68+
def ca_certs(self):
69+
return self.__ca_certs
70+
71+
@property
72+
def cert_file(self):
73+
return self.__cert_file
74+
75+
@property
76+
def disable_ssl_verification(self):
77+
return self.__disable_ssl_verification
78+
1479
class Config(object):
1580
"""Advanced configuration options for the SDK client.
1681
@@ -27,6 +92,7 @@ def __init__(self,
2792
flush_interval=5,
2893
stream_uri='https://stream.launchdarkly.com',
2994
stream=True,
95+
initial_reconnect_delay=1,
3096
verify_ssl=True,
3197
defaults=None,
3298
send_events=None,
@@ -47,15 +113,18 @@ def __init__(self,
47113
diagnostic_opt_out=False,
48114
diagnostic_recording_interval=900,
49115
wrapper_name=None,
50-
wrapper_version=None):
116+
wrapper_version=None,
117+
http=None):
51118
"""
52119
:param string sdk_key: The SDK key for your LaunchDarkly account.
53120
:param string base_uri: The base URL for the LaunchDarkly server. Most users should use the default
54121
value.
55122
:param string events_uri: The URL for the LaunchDarkly events server. Most users should use the
56123
default value.
57-
:param float connect_timeout: The connect timeout for network connections in seconds.
58-
:param float read_timeout: The read timeout for network connections in seconds.
124+
:param float connect_timeout: Deprecated; use `http` instead and specify the `connect_timeout` as
125+
part of :class:`HTTPConfig`.
126+
:param float read_timeout: Deprecated; use `http` instead and specify the `read_timeout` as
127+
part of :class:`HTTPConfig`.
59128
:param int events_upload_max_batch_size: The maximum number of analytics events that the client will
60129
send at once.
61130
:param int events_max_pending: The capacity of the events buffer. The client buffers up to this many
@@ -67,6 +136,12 @@ def __init__(self,
67136
use the default value.
68137
:param bool stream: Whether or not the streaming API should be used to receive flag updates. By
69138
default, it is enabled. Streaming should only be disabled on the advice of LaunchDarkly support.
139+
:param float initial_reconnect_delay: The initial reconnect delay (in seconds) for the streaming
140+
connection. The streaming service uses a backoff algorithm (with jitter) every time the connection needs
141+
to be reestablished. The delay for the first reconnection will start near this value, and then
142+
increase exponentially for any subsequent connection failures.
143+
:param bool verify_ssl: Deprecated; use `http` instead and specify `disable_ssl_verification` as
144+
part of :class:`HTTPConfig` if you want to turn off SSL verification (not recommended).
70145
:param bool send_events: Whether or not to send events back to LaunchDarkly. This differs from
71146
`offline` in that it affects only the sending of client-side events, not streaming or polling for
72147
events from the server. By default, events will be sent.
@@ -99,11 +174,8 @@ def __init__(self,
99174
:type event_processor_class: (ldclient.config.Config) -> EventProcessor
100175
:param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key,
101176
config, and FeatureStore implementation
102-
:param http_proxy: Use a proxy when connecting to LaunchDarkly. This is the full URI of the
103-
proxy; for example: http://my-proxy.com:1234. Note that unlike the standard `http_proxy` environment
104-
variable, this is used regardless of whether the target URI is HTTP or HTTPS (the actual LaunchDarkly
105-
service uses HTTPS, but a Relay Proxy instance could use HTTP). Setting this Config parameter will
106-
override any proxy specified by an environment variable, but only for LaunchDarkly SDK connections.
177+
:param http_proxy: Deprecated; use `http` instead and specify the `http_proxy` as part of
178+
:class:`HTTPConfig`.
107179
:param bool diagnostic_opt_out: Unless this field is set to True, the client will send
108180
some diagnostics data to the LaunchDarkly servers in order to assist in the development of future SDK
109181
improvements. These diagnostics consist of an initial payload containing some details of SDK in use,
@@ -118,6 +190,8 @@ def __init__(self,
118190
use. If `wrapper_name` is not set, this field will be ignored. Otherwise the version string will
119191
be included in the HTTP headers along with the `wrapper_name` during requests to the LaunchDarkly
120192
servers.
193+
:param HTTPConfig http: Optional properties for customizing the client's HTTP/HTTPS behavior. See
194+
:class:`HTTPConfig`.
121195
"""
122196
self.__sdk_key = sdk_key
123197

@@ -129,6 +203,7 @@ def __init__(self,
129203
self.__stream_uri = stream_uri.rstrip('\\')
130204
self.__update_processor_class = update_processor_class
131205
self.__stream = stream
206+
self.__initial_reconnect_delay = initial_reconnect_delay
132207
self.__poll_interval = max(poll_interval, 30)
133208
self.__use_ldd = use_ldd
134209
self.__feature_store = InMemoryFeatureStore() if not feature_store else feature_store
@@ -154,6 +229,7 @@ def __init__(self,
154229
self.__diagnostic_recording_interval = max(diagnostic_recording_interval, 60)
155230
self.__wrapper_name = wrapper_name
156231
self.__wrapper_version = wrapper_version
232+
self.__http = http
157233

158234
@classmethod
159235
def default(cls):
@@ -178,6 +254,7 @@ def copy_with_new_sdk_key(self, new_sdk_key):
178254
flush_interval=self.__flush_interval,
179255
stream_uri=self.__stream_uri,
180256
stream=self.__stream,
257+
initial_reconnect_delay=self.__initial_reconnect_delay,
181258
verify_ssl=self.__verify_ssl,
182259
defaults=self.__defaults,
183260
send_events=self.__send_events,
@@ -196,7 +273,8 @@ def copy_with_new_sdk_key(self, new_sdk_key):
196273
diagnostic_opt_out=self.__diagnostic_opt_out,
197274
diagnostic_recording_interval=self.__diagnostic_recording_interval,
198275
wrapper_name=self.__wrapper_name,
199-
wrapper_version=self.__wrapper_version)
276+
wrapper_version=self.__wrapper_version,
277+
http=self.__http)
200278

201279
# for internal use only - probably should be part of the client logic
202280
def get_default(self, key, default):
@@ -244,6 +322,9 @@ def stream(self):
244322
return self.__stream
245323

246324
@property
325+
def initial_reconnect_delay(self):
326+
return self.__initial_reconnect_delay
327+
@property
247328
def poll_interval(self):
248329
return self.__poll_interval
249330

@@ -335,6 +416,19 @@ def wrapper_name(self):
335416
def wrapper_version(self):
336417
return self.__wrapper_version
337418

419+
@property
420+
def http(self):
421+
if self.__http is None:
422+
return HTTPConfig(
423+
connect_timeout=self.__connect_timeout,
424+
read_timeout=self.__read_timeout,
425+
http_proxy=self.__http_proxy,
426+
ca_certs=None,
427+
cert_file=None,
428+
disable_ssl_verification=not self.__verify_ssl
429+
)
430+
return self.__http
431+
338432
def _validate(self):
339433
if self.offline is False and self.sdk_key is None or self.sdk_key == '':
340434
log.warning("Missing or blank sdk_key.")

ldclient/event_processor.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222

2323
from ldclient.event_summarizer import EventSummarizer
2424
from ldclient.fixed_thread_pool import FixedThreadPool
25+
from ldclient.impl.http import _http_factory
2526
from ldclient.lru_cache import SimpleLRUCache
2627
from ldclient.user_filter import UserFilter
2728
from ldclient.interfaces import EventProcessor
2829
from ldclient.repeating_timer import RepeatingTimer
2930
from ldclient.util import UnsuccessfulResponseException
3031
from ldclient.util import _headers, _retryable_statuses
31-
from ldclient.util import create_http_pool_manager
3232
from ldclient.util import log
3333
from ldclient.util import http_error_message, is_http_error_recoverable, stringify_attrs, throw_if_unsuccessful_response
3434
from ldclient.diagnostics import create_diagnostic_init
@@ -255,8 +255,7 @@ class EventDispatcher(object):
255255
def __init__(self, inbox, config, http_client, diagnostic_accumulator=None):
256256
self._inbox = inbox
257257
self._config = config
258-
self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl,
259-
target_base_uri=config.events_uri, force_proxy=config.http_proxy) if http_client is None else http_client
258+
self._http = _http_factory(config).create_pool_manager(1, config.events_uri) if http_client is None else http_client
260259
self._close_http = (http_client is None) # so we know whether to close it later
261260
self._disabled = False
262261
self._outbox = EventBuffer(config.events_max_pending)

ldclient/feature_requester.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import json
88
import urllib3
99

10+
from ldclient.impl.http import _http_factory
1011
from ldclient.interfaces import FeatureRequester
1112
from ldclient.util import UnsuccessfulResponseException
1213
from ldclient.util import _headers
13-
from ldclient.util import create_http_pool_manager
1414
from ldclient.util import log
1515
from ldclient.util import throw_if_unsuccessful_response
1616
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
@@ -25,8 +25,7 @@
2525
class FeatureRequesterImpl(FeatureRequester):
2626
def __init__(self, config):
2727
self._cache = dict()
28-
self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl,
29-
target_base_uri=config.base_uri, force_proxy=config.http_proxy)
28+
self._http = _http_factory(config).create_pool_manager(1, config.base_uri)
3029
self._config = config
3130

3231
def get_all_data(self):

ldclient/impl/http.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from ldclient.version import VERSION
2+
import certifi
3+
from os import environ
4+
import urllib3
5+
6+
def _base_headers(config):
7+
headers = {'Authorization': config.sdk_key or '',
8+
'User-Agent': 'PythonClient/' + VERSION}
9+
if isinstance(config.wrapper_name, str) and config.wrapper_name != "":
10+
wrapper_version = ""
11+
if isinstance(config.wrapper_version, str) and config.wrapper_version != "":
12+
wrapper_version = "/" + config.wrapper_version
13+
headers.update({'X-LaunchDarkly-Wrapper': config.wrapper_name + wrapper_version})
14+
return headers
15+
16+
def _http_factory(config):
17+
return HTTPFactory(_base_headers(config), config.http)
18+
19+
class HTTPFactory(object):
20+
def __init__(self, base_headers, http_config, override_read_timeout=None):
21+
self.__base_headers = base_headers
22+
self.__http_config = http_config
23+
self.__timeout = urllib3.Timeout(
24+
connect=http_config.connect_timeout,
25+
read=http_config.read_timeout if override_read_timeout is None else override_read_timeout
26+
)
27+
28+
@property
29+
def base_headers(self):
30+
return self.__base_headers
31+
32+
@property
33+
def http_config(self):
34+
return self.__http_config
35+
36+
@property
37+
def timeout(self):
38+
return self.__timeout
39+
40+
def create_pool_manager(self, num_pools, target_base_uri):
41+
proxy_url = self.__http_config.http_proxy or _get_proxy_url(target_base_uri)
42+
43+
if self.__http_config.disable_ssl_verification:
44+
cert_reqs = 'CERT_NONE'
45+
ca_certs = None
46+
else:
47+
cert_reqs = 'CERT_REQUIRED'
48+
ca_certs = self.__http_config.ca_certs or certifi.where()
49+
50+
if proxy_url is None:
51+
return urllib3.PoolManager(
52+
num_pools=num_pools,
53+
cert_reqs=cert_reqs,
54+
ca_certs=ca_certs
55+
)
56+
else:
57+
return urllib3.ProxyManager(
58+
proxy_url,
59+
num_pools=num_pools,
60+
cert_reqs=cert_reqs,
61+
ca_certs = ca_certs
62+
)
63+
64+
def _get_proxy_url(target_base_uri):
65+
if target_base_uri is None:
66+
return None
67+
is_https = target_base_uri.startswith('https:')
68+
if is_https:
69+
return environ.get('https_proxy')
70+
return environ.get('http_proxy')

0 commit comments

Comments
 (0)