Skip to content

Commit 085372a

Browse files
miriadjonmmease
authored andcommitted
Add server_url to plotly.io.orca to allow for external orca server (#1850)
* Add server_url to plotly.io.orca to allow for external orca server * Add orca external server test
1 parent 4731367 commit 085372a

File tree

2 files changed

+180
-72
lines changed

2 files changed

+180
-72
lines changed

Diff for: packages/python/plotly/plotly/io/_orca.py

+134-72
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,48 @@ def save(self):
380380
)
381381
)
382382

383+
@property
384+
def server_url(self):
385+
"""
386+
The server URL to use for an external orca server, or None if orca
387+
should be managed locally
388+
389+
Overrides executable, port, timeout, mathjax, topojson,
390+
and mapbox_access_token
391+
392+
Returns
393+
-------
394+
str or None
395+
"""
396+
return self._props.get("server_url", None)
397+
398+
@server_url.setter
399+
def server_url(self, val):
400+
401+
if val is None:
402+
self._props.pop("server_url", None)
403+
return
404+
if not isinstance(val, str):
405+
raise ValueError(
406+
"""
407+
The server_url property must be a string, but received value of type {typ}.
408+
Received value: {val}""".format(
409+
typ=type(val), val=val
410+
)
411+
)
412+
413+
if not val.startswith("http://") and not val.startswith("https://"):
414+
val = "http://" + val
415+
416+
shutdown_server()
417+
self.executable = None
418+
self.port = None
419+
self.timeout = None
420+
self.mathjax = None
421+
self.topojson = None
422+
self.mapbox_access_token = None
423+
self._props["server_url"] = val
424+
383425
@property
384426
def port(self):
385427
"""
@@ -777,6 +819,7 @@ def __repr__(self):
777819
return """\
778820
orca configuration
779821
------------------
822+
server_url: {server_url}
780823
executable: {executable}
781824
port: {port}
782825
timeout: {timeout}
@@ -795,6 +838,7 @@ def __repr__(self):
795838
config_file: {config_file}
796839
797840
""".format(
841+
server_url=self.server_url,
798842
port=self.port,
799843
executable=self.executable,
800844
timeout=self.timeout,
@@ -1344,62 +1388,65 @@ def ensure_server():
13441388
if status.state == "unvalidated":
13451389
validate_executable()
13461390

1347-
# Acquire lock to make sure that we keep the properties of orca_state
1348-
# consistent across threads
1349-
with orca_lock:
1350-
# Cancel the current shutdown timer, if any
1351-
if orca_state["shutdown_timer"] is not None:
1352-
orca_state["shutdown_timer"].cancel()
1391+
if not config.server_url:
1392+
# Acquire lock to make sure that we keep the properties of orca_state
1393+
# consistent across threads
1394+
with orca_lock:
1395+
# Cancel the current shutdown timer, if any
1396+
if orca_state["shutdown_timer"] is not None:
1397+
orca_state["shutdown_timer"].cancel()
1398+
1399+
# Start a new server process if none is active
1400+
if orca_state["proc"] is None:
1401+
1402+
# Determine server port
1403+
if config.port is None:
1404+
orca_state["port"] = find_open_port()
1405+
else:
1406+
orca_state["port"] = config.port
1407+
1408+
# Build orca command list
1409+
cmd_list = status._props["executable_list"] + [
1410+
"serve",
1411+
"-p",
1412+
str(orca_state["port"]),
1413+
"--plotly",
1414+
config.plotlyjs,
1415+
"--graph-only",
1416+
]
13531417

1354-
# Start a new server process if none is active
1355-
if orca_state["proc"] is None:
1418+
if config.topojson:
1419+
cmd_list.extend(["--topojson", config.topojson])
13561420

1357-
# Determine server port
1358-
if config.port is None:
1359-
orca_state["port"] = find_open_port()
1360-
else:
1361-
orca_state["port"] = config.port
1362-
1363-
# Build orca command list
1364-
cmd_list = status._props["executable_list"] + [
1365-
"serve",
1366-
"-p",
1367-
str(orca_state["port"]),
1368-
"--plotly",
1369-
config.plotlyjs,
1370-
"--graph-only",
1371-
]
1372-
1373-
if config.topojson:
1374-
cmd_list.extend(["--topojson", config.topojson])
1375-
1376-
if config.mathjax:
1377-
cmd_list.extend(["--mathjax", config.mathjax])
1378-
1379-
if config.mapbox_access_token:
1380-
cmd_list.extend(["--mapbox-access-token", config.mapbox_access_token])
1381-
1382-
# Create subprocess that launches the orca server on the
1383-
# specified port.
1384-
DEVNULL = open(os.devnull, "wb")
1385-
with orca_env():
1386-
orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL)
1387-
1388-
# Update orca.status so the user has an accurate view
1389-
# of the state of the orca server
1390-
status._props["state"] = "running"
1391-
status._props["pid"] = orca_state["proc"].pid
1392-
status._props["port"] = orca_state["port"]
1393-
status._props["command"] = cmd_list
1394-
1395-
# Create new shutdown timer if a timeout was specified
1396-
if config.timeout is not None:
1397-
t = threading.Timer(config.timeout, shutdown_server)
1398-
# Make it a daemon thread so that exit won't wait for timer to
1399-
# complete
1400-
t.daemon = True
1401-
t.start()
1402-
orca_state["shutdown_timer"] = t
1421+
if config.mathjax:
1422+
cmd_list.extend(["--mathjax", config.mathjax])
1423+
1424+
if config.mapbox_access_token:
1425+
cmd_list.extend(
1426+
["--mapbox-access-token", config.mapbox_access_token]
1427+
)
1428+
1429+
# Create subprocess that launches the orca server on the
1430+
# specified port.
1431+
DEVNULL = open(os.devnull, "wb")
1432+
with orca_env():
1433+
orca_state["proc"] = subprocess.Popen(cmd_list, stdout=DEVNULL)
1434+
1435+
# Update orca.status so the user has an accurate view
1436+
# of the state of the orca server
1437+
status._props["state"] = "running"
1438+
status._props["pid"] = orca_state["proc"].pid
1439+
status._props["port"] = orca_state["port"]
1440+
status._props["command"] = cmd_list
1441+
1442+
# Create new shutdown timer if a timeout was specified
1443+
if config.timeout is not None:
1444+
t = threading.Timer(config.timeout, shutdown_server)
1445+
# Make it a daemon thread so that exit won't wait for timer to
1446+
# complete
1447+
t.daemon = True
1448+
t.start()
1449+
orca_state["shutdown_timer"] = t
14031450

14041451

14051452
@retrying.retry(wait_random_min=5, wait_random_max=10, stop_max_delay=60000)
@@ -1410,9 +1457,12 @@ def request_image_with_retrying(**kwargs):
14101457
"""
14111458
from requests import post
14121459

1413-
server_url = "http://{hostname}:{port}".format(
1414-
hostname="localhost", port=orca_state["port"]
1415-
)
1460+
if config.server_url:
1461+
server_url = config.server_url
1462+
else:
1463+
server_url = "http://{hostname}:{port}".format(
1464+
hostname="localhost", port=orca_state["port"]
1465+
)
14161466

14171467
request_params = {k: v for k, v, in kwargs.items() if v is not None}
14181468
json_str = json.dumps(request_params, cls=_plotly_utils.utils.PlotlyJSONEncoder)
@@ -1513,29 +1563,41 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru
15131563
# Get current status string
15141564
status_str = repr(status)
15151565

1516-
# Check if the orca server process exists
1517-
pid_exists = psutil.pid_exists(status.pid)
1518-
1519-
# Raise error message based on whether the server process existed
1520-
if pid_exists:
1566+
if config.server_url:
15211567
raise ValueError(
15221568
"""
1569+
Plotly.py was unable to communicate with the orca server at {server_url}
1570+
1571+
Please check that the server is running and accessible.
1572+
""".format(
1573+
server_url=config.server_url
1574+
)
1575+
)
1576+
1577+
else:
1578+
# Check if the orca server process exists
1579+
pid_exists = psutil.pid_exists(status.pid)
1580+
1581+
# Raise error message based on whether the server process existed
1582+
if pid_exists:
1583+
raise ValueError(
1584+
"""
15231585
For some reason plotly.py was unable to communicate with the
15241586
local orca server process, even though the server process seems to be running.
15251587
15261588
Please review the process and connection information below:
15271589
15281590
{info}
15291591
""".format(
1530-
info=status_str
1592+
info=status_str
1593+
)
15311594
)
1532-
)
1533-
else:
1534-
# Reset the status so that if the user tries again, we'll try to
1535-
# start the server again
1536-
reset_status()
1537-
raise ValueError(
1538-
"""
1595+
else:
1596+
# Reset the status so that if the user tries again, we'll try to
1597+
# start the server again
1598+
reset_status()
1599+
raise ValueError(
1600+
"""
15391601
For some reason the orca server process is no longer running.
15401602
15411603
Please review the process and connection information below:
@@ -1544,9 +1606,9 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru
15441606
plotly.py will attempt to start the local server process again the next time
15451607
an image export operation is performed.
15461608
""".format(
1547-
info=status_str
1609+
info=status_str
1610+
)
15481611
)
1549-
)
15501612

15511613
# Check response
15521614
# --------------

Diff for: packages/python/plotly/plotly/tests/test_orca/test_orca_server.py

+46
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
import time
88
import psutil
99
import pytest
10+
import plotly.graph_objects as go
1011

1112

1213
# Fixtures
1314
# --------
15+
from plotly.io._orca import find_open_port, which, orca_env
16+
17+
1418
@pytest.fixture()
1519
def setup():
1620
# Set problematic environment variables
@@ -150,3 +154,45 @@ def test_server_timeout_shutdown():
150154

151155
# Check that ping is no longer answered
152156
assert not ping_pongs(server_url)
157+
158+
159+
def test_external_server_url():
160+
# Build server url
161+
port = find_open_port()
162+
server_url = "http://{hostname}:{port}".format(hostname="localhost", port=port)
163+
164+
# Build external orca command
165+
orca_path = which("orca")
166+
cmd_list = [orca_path] + [
167+
"serve",
168+
"-p",
169+
str(port),
170+
"--plotly",
171+
pio.orca.config.plotlyjs,
172+
"--graph-only",
173+
]
174+
175+
# Run orca as subprocess to simulate external orca server
176+
DEVNULL = open(os.devnull, "wb")
177+
with orca_env():
178+
proc = subprocess.Popen(cmd_list, stdout=DEVNULL)
179+
180+
# Start plotly managed orca server so we can ensure it gets shut down properly
181+
pio.orca.config.port = port
182+
pio.orca.ensure_server()
183+
assert pio.orca.status.state == "running"
184+
185+
# Configure orca to use external server
186+
pio.orca.config.server_url = server_url
187+
188+
# Make sure that the locally managed orca server has been shutdown and the local
189+
# config options have been cleared
190+
assert pio.orca.status.state == "unvalidated"
191+
assert pio.orca.config.port is None
192+
193+
fig = go.Figure()
194+
img_bytes = pio.to_image(fig, format="svg")
195+
assert img_bytes.startswith(b"<svg class")
196+
197+
# Kill server orca process
198+
proc.terminate()

0 commit comments

Comments
 (0)