diff --git a/CHANGELOG.md b/CHANGELOG.md index f00262c9b56..1388cca8197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [1.9.12] - 2016-05-16 +### Added +SSL support for streaming. + ## [1.9.11] - 2016-05-02 ### Added - The FigureFactory can now create scatter plot matrices with `.create_scatterplotmatrix`. Check it out with: diff --git a/plotly/plotly/chunked_requests/chunked_request.py b/plotly/plotly/chunked_requests/chunked_request.py index c154ec8635d..c0be184a7a8 100644 --- a/plotly/plotly/chunked_requests/chunked_request.py +++ b/plotly/plotly/chunked_requests/chunked_request.py @@ -3,10 +3,12 @@ import os from six.moves import http_client from six.moves.urllib.parse import urlparse +from ssl import SSLError + class Stream: - def __init__(self, server, port=80, headers={}, url='/'): - ''' Initialize a stream object and an HTTP Connection + def __init__(self, server, port=80, headers={}, url='/', ssl_enabled=False): + ''' Initialize a stream object and an HTTP or HTTPS connection with chunked Transfer-Encoding to server:port with optional headers. ''' self.maxtries = 5 @@ -17,6 +19,7 @@ def __init__(self, server, port=80, headers={}, url='/'): self._port = port self._headers = headers self._url = url + self._ssl_enabled = ssl_enabled self._connect() def write(self, data, reconnect_on=('', 200, )): @@ -73,17 +76,21 @@ def write(self, data, reconnect_on=('', 200, )): def _get_proxy_config(self): """ Determine if self._url should be passed through a proxy. If so, return - the appropriate proxy_server and proxy_port + the appropriate proxy_server and proxy_port. Assumes https_proxy is used + when ssl_enabled=True. """ proxy_server = None proxy_port = None + ssl_enabled = self._ssl_enabled - ## only doing HTTPConnection, so only use http_proxy - proxy = os.environ.get("http_proxy") + if ssl_enabled: + proxy = os.environ.get("https_proxy") + else: + proxy = os.environ.get("http_proxy") no_proxy = os.environ.get("no_proxy") - no_proxy_url = no_proxy and self._url in no_proxy + no_proxy_url = no_proxy and self._server in no_proxy if proxy and not no_proxy_url: p = urlparse(proxy) @@ -93,19 +100,30 @@ def _get_proxy_config(self): return proxy_server, proxy_port def _connect(self): - ''' Initialize an HTTP connection with chunked Transfer-Encoding + ''' Initialize an HTTP/HTTPS connection with chunked Transfer-Encoding to server:port with optional headers. ''' server = self._server port = self._port headers = self._headers + ssl_enabled = self._ssl_enabled proxy_server, proxy_port = self._get_proxy_config() if (proxy_server and proxy_port): - self._conn = http_client.HTTPConnection(proxy_server, proxy_port) + if ssl_enabled: + self._conn = http_client.HTTPSConnection( + proxy_server, proxy_port + ) + else: + self._conn = http_client.HTTPConnection( + proxy_server, proxy_port + ) self._conn.set_tunnel(server, port) else: - self._conn = http_client.HTTPConnection(server, port) + if ssl_enabled: + self._conn = http_client.HTTPSConnection(server, port) + else: + self._conn = http_client.HTTPConnection(server, port) self._conn.putrequest('POST', self._url) self._conn.putheader('Transfer-Encoding', 'chunked') @@ -236,6 +254,16 @@ def _isconnected(self): # let's just assume that we're still connected and # hopefully recieve some data on the next try. return True + elif isinstance(e, SSLError): + if e.errno == 2: + # errno 2 occurs when trying to read or write data, but more + # data needs to be received on the underlying TCP transport + # before the request can be fulfilled. + # + # Python 2.7.9+ and Python 3.3+ give this its own exception, + # SSLWantReadError + return True + raise e else: # Unknown scenario raise e diff --git a/plotly/plotly/plotly.py b/plotly/plotly/plotly.py index c175e7c7d7a..15b2e3f011d 100644 --- a/plotly/plotly/plotly.py +++ b/plotly/plotly/plotly.py @@ -439,6 +439,9 @@ class Stream: """ + HTTP_PORT = 80 + HTTPS_PORT = 443 + @utils.template_doc(**tools.get_config_file()) def __init__(self, stream_id): """ @@ -454,6 +457,30 @@ def __init__(self, stream_id): self.connected = False self._stream = None + def get_streaming_specs(self): + """ + Returns the streaming server, port, ssl_enabled flag, and headers. + + """ + streaming_url = get_config()['plotly_streaming_domain'] + ssl_enabled = 'https' in streaming_url + port = self.HTTPS_PORT if ssl_enabled else self.HTTP_PORT + + # If no scheme (https/https) is included in the streaming_url, the + # host will be None. Use streaming_url in this case. + host = (six.moves.urllib.parse.urlparse(streaming_url).hostname or + streaming_url) + + headers = {'Host': host, 'plotly-streamtoken': self.stream_id} + streaming_specs = { + 'server': host, + 'port': port, + 'ssl_enabled': ssl_enabled, + 'headers': headers + } + + return streaming_specs + def heartbeat(self, reconnect_on=(200, '', 408)): """ Keep stream alive. Streams will close after ~1 min of inactivity. @@ -481,10 +508,8 @@ def open(self): https://plot.ly/python/streaming/ """ - streaming_url = get_config()['plotly_streaming_domain'] - self._stream = chunked_requests.Stream( - streaming_url, 80, {'Host': streaming_url, - 'plotly-streamtoken': self.stream_id}) + streaming_specs = self.get_streaming_specs() + self._stream = chunked_requests.Stream(**streaming_specs) def write(self, trace, layout=None, validate=True, reconnect_on=(200, '', 408)): diff --git a/plotly/tests/test_core/test_stream/test_stream.py b/plotly/tests/test_core/test_stream/test_stream.py index c36c91599f8..2e0a1131010 100644 --- a/plotly/tests/test_core/test_stream/test_stream.py +++ b/plotly/tests/test_core/test_stream/test_stream.py @@ -1,16 +1,13 @@ """ -test_get_figure: -================= - -A module intended for use with Nose. +Streaming tests. """ from __future__ import absolute_import import time +from unittest import TestCase from nose.plugins.attrib import attr -from nose.tools import raises import plotly.plotly as py from plotly.graph_objs import (Layout, Scatter, Stream) @@ -24,102 +21,156 @@ 'plotly_api_domain': 'https://api.plot.ly'} -def setUp(): - py.sign_in(un, ak, **config) - - -@attr('slow') -def test_initialize_stream_plot(): - py.sign_in(un, ak) - stream = Stream(token=tk, maxpoints=50) - url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], - auto_open=False, - world_readable=True, - filename='stream-test') - assert url == 'https://plot.ly/~PythonAPI/461' - time.sleep(.5) - - -@attr('slow') -def test_stream_single_points(): - py.sign_in(un, ak) - stream = Stream(token=tk, maxpoints=50) - res = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], - auto_open=False, - world_readable=True, - filename='stream-test') - time.sleep(.5) - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(Scatter(x=1, y=10)) - time.sleep(.5) - my_stream.close() - - -@attr('slow') -def test_stream_multiple_points(): - py.sign_in(un, ak) - stream = Stream(token=tk, maxpoints=50) - url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], - auto_open=False, - world_readable=True, - filename='stream-test') - time.sleep(.5) - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(Scatter(x=[1, 2, 3, 4], y=[2, 1, 2, 5])) - time.sleep(.5) - my_stream.close() - - -@attr('slow') -def test_stream_layout(): - py.sign_in(un, ak) - stream = Stream(token=tk, maxpoints=50) - url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], - auto_open=False, - world_readable=True, - filename='stream-test') - time.sleep(.5) - title_0 = "some title i picked first" - title_1 = "this other title i picked second" - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(Scatter(x=1, y=10), layout=Layout(title=title_0)) - time.sleep(.5) - my_stream.close() - my_stream.open() - my_stream.write(Scatter(x=1, y=10), layout=Layout(title=title_1)) - my_stream.close() - - -@attr('slow') -@raises(exceptions.PlotlyError) -def test_stream_validate_data(): - py.sign_in(un, ak) - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(dict(x=1, y=10, z=[1])) # assumes scatter... - my_stream.close() - - -@attr('slow') -@raises(exceptions.PlotlyError) -def test_stream_validate_layout(): - py.sign_in(un, ak) - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(Scatter(x=1, y=10), layout=Layout(legend=True)) - my_stream.close() - - -@attr('slow') -def test_stream_unstreamable(): - - # even though `name` isn't streamable, we don't validate it --> should pass - - py.sign_in(un, ak) - my_stream = py.Stream(tk) - my_stream.open() - my_stream.write(Scatter(x=1, y=10, name='nope')) - my_stream.close() +class TestStreaming(TestCase): + + def setUp(self): + py.sign_in(un, ak, **config) + + @attr('slow') + def test_initialize_stream_plot(self): + py.sign_in(un, ak) + stream = Stream(token=tk, maxpoints=50) + url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], + auto_open=False, + world_readable=True, + filename='stream-test') + assert url == 'https://plot.ly/~PythonAPI/461' + time.sleep(.5) + + @attr('slow') + def test_stream_single_points(self): + py.sign_in(un, ak) + stream = Stream(token=tk, maxpoints=50) + res = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], + auto_open=False, + world_readable=True, + filename='stream-test') + time.sleep(.5) + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(Scatter(x=1, y=10)) + time.sleep(.5) + my_stream.close() + + @attr('slow') + def test_stream_multiple_points(self): + py.sign_in(un, ak) + stream = Stream(token=tk, maxpoints=50) + url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], + auto_open=False, + world_readable=True, + filename='stream-test') + time.sleep(.5) + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(Scatter(x=[1, 2, 3, 4], y=[2, 1, 2, 5])) + time.sleep(.5) + my_stream.close() + + @attr('slow') + def test_stream_layout(self): + py.sign_in(un, ak) + stream = Stream(token=tk, maxpoints=50) + url = py.plot([Scatter(x=[], y=[], mode='markers', stream=stream)], + auto_open=False, + world_readable=True, + filename='stream-test') + time.sleep(.5) + title_0 = "some title i picked first" + title_1 = "this other title i picked second" + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(Scatter(x=1, y=10), layout=Layout(title=title_0)) + time.sleep(.5) + my_stream.close() + my_stream.open() + my_stream.write(Scatter(x=1, y=10), layout=Layout(title=title_1)) + my_stream.close() + + @attr('slow') + def test_stream_validate_data(self): + with self.assertRaises(exceptions.PlotlyError): + py.sign_in(un, ak) + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(dict(x=1, y=10, z=[1])) # assumes scatter... + my_stream.close() + + @attr('slow') + def test_stream_validate_layout(self): + with self.assertRaises(exceptions.PlotlyError): + py.sign_in(un, ak) + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(Scatter(x=1, y=10), layout=Layout(legend=True)) + my_stream.close() + + @attr('slow') + def test_stream_unstreamable(self): + + # even though `name` isn't streamable, we don't validate it --> pass + + py.sign_in(un, ak) + my_stream = py.Stream(tk) + my_stream.open() + my_stream.write(Scatter(x=1, y=10, name='nope')) + my_stream.close() + + def test_stream_no_scheme(self): + + # If no scheme is used in the plotly_streaming_domain, port 80 + # should be used for streaming and ssl_enabled should be False + + py.sign_in(un, ak, **{'plotly_streaming_domain': 'stream.plot.ly'}) + my_stream = py.Stream(tk) + expected_streaming_specs = { + 'server': 'stream.plot.ly', + 'port': 80, + 'ssl_enabled': False, + 'headers': { + 'Host': 'stream.plot.ly', + 'plotly-streamtoken': tk + } + } + actual_streaming_specs = my_stream.get_streaming_specs() + self.assertEqual(expected_streaming_specs, actual_streaming_specs) + + def test_stream_http(self): + + # If the http scheme is used in the plotly_streaming_domain, port 80 + # should be used for streaming and ssl_enabled should be False + + py.sign_in(un, ak, + **{'plotly_streaming_domain': 'http://stream.plot.ly'}) + my_stream = py.Stream(tk) + expected_streaming_specs = { + 'server': 'stream.plot.ly', + 'port': 80, + 'ssl_enabled': False, + 'headers': { + 'Host': 'stream.plot.ly', + 'plotly-streamtoken': tk + } + } + actual_streaming_specs = my_stream.get_streaming_specs() + self.assertEqual(expected_streaming_specs, actual_streaming_specs) + + def test_stream_https(self): + + # If the https scheme is used in the plotly_streaming_domain, port 443 + # should be used for streaming and ssl_enabled should be True + + py.sign_in(un, ak, + **{'plotly_streaming_domain': 'https://stream.plot.ly'}) + my_stream = py.Stream(tk) + expected_streaming_specs = { + 'server': 'stream.plot.ly', + 'port': 443, + 'ssl_enabled': True, + 'headers': { + 'Host': 'stream.plot.ly', + 'plotly-streamtoken': tk + } + } + actual_streaming_specs = my_stream.get_streaming_specs() + self.assertEqual(expected_streaming_specs, actual_streaming_specs) diff --git a/plotly/version.py b/plotly/version.py index ad78fc163b8..b8f569b218e 100644 --- a/plotly/version.py +++ b/plotly/version.py @@ -1,2 +1,2 @@ -__version__ = '1.9.11' +__version__ = '1.9.12' diff --git a/submodules/chunked_requests b/submodules/chunked_requests index f940b023110..42799319b8f 160000 --- a/submodules/chunked_requests +++ b/submodules/chunked_requests @@ -1 +1 @@ -Subproject commit f940b023110a77bbaf79f9dbccca701cf756335f +Subproject commit 42799319b8f06ce89a3831b3ff911207ca0b6a02