From 324a68f62fafd2db30965cff2a7a2464ffc8484b Mon Sep 17 00:00:00 2001 From: Zack Cornelius Date: Mon, 28 Oct 2019 13:56:22 -0500 Subject: [PATCH 1/5] Add server_url to plotly.io.orca to allow for external orca server --- packages/python/plotly/plotly/io/_orca.py | 226 ++++++++++++++-------- 1 file changed, 143 insertions(+), 83 deletions(-) diff --git a/packages/python/plotly/plotly/io/_orca.py b/packages/python/plotly/plotly/io/_orca.py index ae4c25494f5..ebc9ab9f9ce 100644 --- a/packages/python/plotly/plotly/io/_orca.py +++ b/packages/python/plotly/plotly/io/_orca.py @@ -380,6 +380,48 @@ def save(self): ) ) + @property + def server_url(self): + """ + The server URL to use for an external orca server, or None if orca + should be managed locally + + Overrides executable, port, timeout, mathjax, topojson, + and mapbox_access_token + + Returns + ------- + str or None + """ + return self._props.get("server_url", None) + + @server_url.setter + def server_url(self, val): + + if val is None: + self._props.pop("server_url", None) + return + if not isinstance(val, str): + raise ValueError( + """ +The server_url property must be a string, but received value of type {typ}. + Received value: {val}""".format( + typ=type(val), val=val + ) + ) + + if not val.startswith("http://") and not val.startswith("https://"): + val = "http://" + val + + shutdown_server() + self.executable = None + self.port = None + self.timeout = None + self.mathjax = None + self.topojson = None + self.mapbox_access_token = None + self._props["server_url"] = val + @property def port(self): """ @@ -777,6 +819,7 @@ def __repr__(self): return """\ orca configuration ------------------ + server_url: {server_url} executable: {executable} port: {port} timeout: {timeout} @@ -795,6 +838,7 @@ def __repr__(self): config_file: {config_file} """.format( + server_url=self.server_url, port=self.port, executable=self.executable, timeout=self.timeout, @@ -1343,62 +1387,65 @@ def ensure_server(): if status.state == "unvalidated": validate_executable() - # Acquire lock to make sure that we keep the properties of orca_state - # consistent across threads - with orca_lock: - # Cancel the current shutdown timer, if any - if orca_state["shutdown_timer"] is not None: - orca_state["shutdown_timer"].cancel() + if not config.server_url: + # Acquire lock to make sure that we keep the properties of orca_state + # consistent across threads + with orca_lock: + # Cancel the current shutdown timer, if any + if orca_state["shutdown_timer"] is not None: + orca_state["shutdown_timer"].cancel() + + # Start a new server process if none is active + if orca_state["proc"] is None: + + # Determine server port + if config.port is None: + orca_state["port"] = find_open_port() + else: + orca_state["port"] = config.port + + # Build orca command list + cmd_list = status._props["executable_list"] + [ + "serve", + "-p", + str(orca_state["port"]), + "--plotly", + config.plotlyjs, + "--graph-only", + ] - # Start a new server process if none is active - if orca_state["proc"] is None: + if config.topojson: + cmd_list.extend(["--topojson", config.topojson]) - # Determine server port - if config.port is None: - orca_state["port"] = find_open_port() - else: - orca_state["port"] = config.port - - # Build orca command list - cmd_list = status._props["executable_list"] + [ - "serve", - "-p", - str(orca_state["port"]), - "--plotly", - config.plotlyjs, - "--graph-only", - ] - - if config.topojson: - cmd_list.extend(["--topojson", config.topojson]) - - if config.mathjax: - cmd_list.extend(["--mathjax", config.mathjax]) - - if config.mapbox_access_token: - cmd_list.extend(["--mapbox-access-token", config.mapbox_access_token]) - - # Create subprocess that launches the orca server on the - # specified port. - DEVNULL = open(os.devnull, "wb") - with orca_env(): - orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL) - - # Update orca.status so the user has an accurate view - # of the state of the orca server - status._props["state"] = "running" - status._props["pid"] = orca_state["proc"].pid - status._props["port"] = orca_state["port"] - status._props["command"] = cmd_list - - # Create new shutdown timer if a timeout was specified - if config.timeout is not None: - t = threading.Timer(config.timeout, shutdown_server) - # Make it a daemon thread so that exit won't wait for timer to - # complete - t.daemon = True - t.start() - orca_state["shutdown_timer"] = t + if config.mathjax: + cmd_list.extend(["--mathjax", config.mathjax]) + + if config.mapbox_access_token: + cmd_list.extend( + ["--mapbox-access-token", config.mapbox_access_token] + ) + + # Create subprocess that launches the orca server on the + # specified port. + DEVNULL = open(os.devnull, "wb") + with orca_env(): + orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL) + + # Update orca.status so the user has an accurate view + # of the state of the orca server + status._props["state"] = "running" + status._props["pid"] = orca_state["proc"].pid + status._props["port"] = orca_state["port"] + status._props["command"] = cmd_list + + # Create new shutdown timer if a timeout was specified + if config.timeout is not None: + t = threading.Timer(config.timeout, shutdown_server) + # Make it a daemon thread so that exit won't wait for timer to + # complete + t.daemon = True + t.start() + orca_state["shutdown_timer"] = t @retrying.retry(wait_random_min=5, wait_random_max=10, stop_max_delay=60000) @@ -1409,9 +1456,12 @@ def request_image_with_retrying(**kwargs): """ from requests import post - server_url = "http://{hostname}:{port}".format( - hostname="localhost", port=orca_state["port"] - ) + if config.server_url: + server_url = config.server_url + else: + server_url = "http://{hostname}:{port}".format( + hostname="localhost", port=orca_state["port"] + ) request_params = {k: v for k, v, in kwargs.items() if v is not None} json_str = json.dumps(request_params, cls=_plotly_utils.utils.PlotlyJSONEncoder) @@ -1512,40 +1562,50 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru # Get current status string status_str = repr(status) - # Check if the orca server process exists - pid_exists = psutil.pid_exists(status.pid) - - # Raise error message based on whether the server process existed - if pid_exists: + if config.server_url: raise ValueError( """ -For some reason plotly.py was unable to communicate with the -local orca server process, even though the server process seems to be running. + Plotly.py was unable to communicate with the orca server at {server_url} -Please review the process and connection information below: - -{info} -""".format( - info=status_str - ) + Please check that the server is running and accessible. + """ ) + else: - # Reset the status so that if the user tries again, we'll try to - # start the server again - reset_status() - raise ValueError( - """ -For some reason the orca server process is no longer running. + # Check if the orca server process exists + pid_exists = psutil.pid_exists(status.pid) -Please review the process and connection information below: + # Raise error message based on whether the server process existed + if pid_exists: + raise ValueError( + """ + For some reason plotly.py was unable to communicate with the + local orca server process, even though the server process seems to be running. -{info} -plotly.py will attempt to start the local server process again the next time -an image export operation is performed. -""".format( - info=status_str + Please review the process and connection information below: + + {info} + """.format( + info=status_str + ) + ) + else: + # Reset the status so that if the user tries again, we'll try to + # start the server again + reset_status() + raise ValueError( + """ + For some reason the orca server process is no longer running. + + Please review the process and connection information below: + + {info} + plotly.py will attempt to start the local server process again the next time + an image export operation is performed. + """.format( + info=status_str + ) ) - ) # Check response # -------------- From 38a1052ebb92b67d998b34f3b7c3e6b0c35918e3 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 11 Nov 2019 07:59:47 -0500 Subject: [PATCH 2/5] Dedent error messages --- packages/python/plotly/plotly/io/_orca.py | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/python/plotly/plotly/io/_orca.py b/packages/python/plotly/plotly/io/_orca.py index ebc9ab9f9ce..b8b24a7e7ee 100644 --- a/packages/python/plotly/plotly/io/_orca.py +++ b/packages/python/plotly/plotly/io/_orca.py @@ -1565,10 +1565,10 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru if config.server_url: raise ValueError( """ - Plotly.py was unable to communicate with the orca server at {server_url} +Plotly.py was unable to communicate with the orca server at {server_url} - Please check that the server is running and accessible. - """ +Please check that the server is running and accessible. +""" ) else: @@ -1579,13 +1579,13 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru if pid_exists: raise ValueError( """ - For some reason plotly.py was unable to communicate with the - local orca server process, even though the server process seems to be running. +For some reason plotly.py was unable to communicate with the +local orca server process, even though the server process seems to be running. - Please review the process and connection information below: +Please review the process and connection information below: - {info} - """.format( +{info} +""".format( info=status_str ) ) @@ -1595,14 +1595,14 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru reset_status() raise ValueError( """ - For some reason the orca server process is no longer running. +For some reason the orca server process is no longer running. - Please review the process and connection information below: +Please review the process and connection information below: - {info} - plotly.py will attempt to start the local server process again the next time - an image export operation is performed. - """.format( +{info} +plotly.py will attempt to start the local server process again the next time +an image export operation is performed. +""".format( info=status_str ) ) From 84e2b5d9d96748eeeb19d82b71223c5cdd65b965 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 11 Nov 2019 09:11:32 -0500 Subject: [PATCH 3/5] Add server_url format string --- packages/python/plotly/plotly/io/_orca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/io/_orca.py b/packages/python/plotly/plotly/io/_orca.py index b8b24a7e7ee..1fb95faf599 100644 --- a/packages/python/plotly/plotly/io/_orca.py +++ b/packages/python/plotly/plotly/io/_orca.py @@ -1568,7 +1568,7 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru Plotly.py was unable to communicate with the orca server at {server_url} Please check that the server is running and accessible. -""" +""".format(server_url=config.server_url) ) else: From e4383257f441ae6f933d805346bec8353bef5543 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 11 Nov 2019 09:14:58 -0500 Subject: [PATCH 4/5] Add orca external server test --- .../tests/test_orca/test_orca_server.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/python/plotly/plotly/tests/test_orca/test_orca_server.py b/packages/python/plotly/plotly/tests/test_orca/test_orca_server.py index b8e70e153ca..cd6a31653a5 100644 --- a/packages/python/plotly/plotly/tests/test_orca/test_orca_server.py +++ b/packages/python/plotly/plotly/tests/test_orca/test_orca_server.py @@ -7,10 +7,14 @@ import time import psutil import pytest +import plotly.graph_objects as go # Fixtures # -------- +from plotly.io._orca import find_open_port, which, orca_env + + @pytest.fixture() def setup(): # Set problematic environment variables @@ -150,3 +154,45 @@ def test_server_timeout_shutdown(): # Check that ping is no longer answered assert not ping_pongs(server_url) + + +def test_external_server_url(): + # Build server url + port = find_open_port() + server_url = "http://{hostname}:{port}".format(hostname="localhost", port=port) + + # Build external orca command + orca_path = which("orca") + cmd_list = [orca_path] + [ + "serve", + "-p", + str(port), + "--plotly", + pio.orca.config.plotlyjs, + "--graph-only", + ] + + # Run orca as subprocess to simulate external orca server + DEVNULL = open(os.devnull, "wb") + with orca_env(): + proc = subprocess.Popen(cmd_list, stdout=DEVNULL) + + # Start plotly managed orca server so we can ensure it gets shut down properly + pio.orca.config.port = port + pio.orca.ensure_server() + assert pio.orca.status.state == "running" + + # Configure orca to use external server + pio.orca.config.server_url = server_url + + # Make sure that the locally managed orca server has been shutdown and the local + # config options have been cleared + assert pio.orca.status.state == "unvalidated" + assert pio.orca.config.port is None + + fig = go.Figure() + img_bytes = pio.to_image(fig, format="svg") + assert img_bytes.startswith(b" Date: Mon, 11 Nov 2019 09:17:13 -0500 Subject: [PATCH 5/5] blacken --- packages/python/plotly/plotly/io/_orca.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/io/_orca.py b/packages/python/plotly/plotly/io/_orca.py index 1fb95faf599..d6dad0c3954 100644 --- a/packages/python/plotly/plotly/io/_orca.py +++ b/packages/python/plotly/plotly/io/_orca.py @@ -1568,7 +1568,9 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru Plotly.py was unable to communicate with the orca server at {server_url} Please check that the server is running and accessible. -""".format(server_url=config.server_url) +""".format( + server_url=config.server_url + ) ) else: