Skip to content

Commit d5c61c9

Browse files
committed
add pypop-gui cli launcher
1 parent dc304c9 commit d5c61c9

File tree

3 files changed

+244
-1
lines changed

3 files changed

+244
-1
lines changed

pypop/cli.py

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@
55
"""CLI Analysis scripts"""
66

77
from os import getcwd
8-
from os.path import expanduser, join as path_join, normpath
8+
from os.path import (
9+
exists as path_exists,
10+
expanduser,
11+
isabs,
12+
join as path_join,
13+
normpath,
14+
realpath,
15+
relpath,
16+
)
17+
from pkg_resources import resource_filename
918
from shutil import copytree, Error as shutil_error
19+
from webbrowser import open_new_tab
20+
import sys
1021

1122
from matplotlib import use
1223

@@ -17,10 +28,31 @@
1728
from pypop.dimemas import dimemas_idealise
1829
from pypop.config import set_dimemas_path, set_paramedir_path, set_tmpdir_path
1930
from pypop.examples import examples_directory
31+
from pypop.server import get_notebook_server_instance, construct_nb_url
2032

2133
from argparse import ArgumentParser
2234
from tqdm import tqdm
2335

36+
import nbformat
37+
38+
latest_nbformat = getattr(nbformat, "v{}".format(nbformat.current_nbformat))
39+
new_nb = latest_nbformat.new_notebook
40+
code_cell = latest_nbformat.new_code_cell
41+
md_cell = latest_nbformat.new_markdown_cell
42+
43+
GUI_MSG = """
44+
PyPOP GUI Ready:
45+
If the gui does not open automatically, please go to the following url
46+
in your web browser:
47+
48+
{}
49+
"""
50+
51+
OWN_SERVER_MSG = """
52+
A new notebook server was started for this PyPOP session. When you are finished,
53+
press CTRL-C in this window to shut down the server.
54+
"""
55+
2456

2557
def _dimemas_idealise_parse_args():
2658

@@ -327,3 +359,127 @@ def copy_examples():
327359
except shutil_error as err:
328360
print("Copy failed: {}".format(str(err)))
329361
return -1
362+
363+
364+
def _gui_launcher_parse_args():
365+
366+
# make an argument parser
367+
parser = ArgumentParser(description="Launch the PyPOP GUI and Notebook server")
368+
369+
# First define collection of traces
370+
parser.add_argument(
371+
"nb_path",
372+
type=str,
373+
nargs="?",
374+
metavar="NB_PATH",
375+
help="GUI Notebook name/path default is $PWD/pypop_gui.ipynb",
376+
)
377+
parser.add_argument(
378+
"-n",
379+
"--notebookdir",
380+
type=str,
381+
metavar="NB_DIR",
382+
help="Notebook server root directory (default is $PWD or inferred from "
383+
"NB_PATH), ignored if an existing server is used",
384+
)
385+
parser.add_argument(
386+
"-f",
387+
"--force-overwrite",
388+
action="store_true",
389+
help="Overwrite existing file when creating GUI notebook.",
390+
)
391+
392+
return parser.parse_args()
393+
394+
395+
def _gui_exit(msg, own_server):
396+
print(msg, file=sys.stderr)
397+
if own_server:
398+
own_server.kill()
399+
sys.exit(-1)
400+
401+
402+
def pypop_gui():
403+
"""Entrypoint for launching Jupyter Notebook GUI
404+
"""
405+
406+
config = _gui_launcher_parse_args()
407+
408+
notebookdir = getcwd()
409+
nb_name = "pypop_gui.ipynb"
410+
if config.notebookdir:
411+
notebookdir = realpath(notebookdir)
412+
413+
nb_path = realpath(path_join(notebookdir, nb_name))
414+
if config.nb_path:
415+
if isabs(config.nb_path):
416+
if relpath(realpath(config.nb_path), notebookdir):
417+
gui_exit(
418+
"Requested gui notebook file path is not in the notebook server "
419+
"directory ({}).".format(config.nb_path, notebookdir),
420+
None,
421+
)
422+
nb_path = os.realpath(config.nb_path)
423+
else:
424+
nb_name = os.realpath(join(notebookdir, config.nb_path))
425+
426+
server_info, own_server = get_notebook_server_instance()
427+
428+
real_nbdir = realpath(server_info["notebook_dir"])
429+
if relpath(nb_path, real_nbdir).startswith(".."):
430+
_gui_exit(
431+
"Requested gui notebook file path {} is not in the root of the "
432+
"notebook server ({}). You may need to specify a different working "
433+
"directory, change the server config, or allow PyPOP to start its own "
434+
"server.".format(nb_path, real_nbdir),
435+
own_server,
436+
)
437+
438+
if not path_exists(nb_path) or config.force_overwrite:
439+
try:
440+
write_gui_nb(nb_path)
441+
except:
442+
_gui_exit("Failed to create gui notebook", own_server)
443+
444+
nb_url = construct_nb_url(server_info, nb_path)
445+
446+
open_new_tab(nb_url)
447+
448+
print(GUI_MSG.format(nb_url))
449+
450+
if own_server:
451+
print(OWN_SERVER_MSG)
452+
try:
453+
own_server.wait()
454+
except KeyboardInterrupt:
455+
own_server.terminate()
456+
457+
458+
def hidden_code_cell(*args, **kwargs):
459+
460+
hidden_cell = {"hide_input": True}
461+
462+
if "metadata" in kwargs:
463+
kwargs["metadata"].update(hidden_cell)
464+
else:
465+
kwargs["metadata"] = hidden_cell
466+
467+
return code_cell(*args, **kwargs)
468+
469+
470+
def write_gui_nb(gui_nb_path):
471+
472+
gui_nb = new_nb(metadata={})
473+
474+
gui_code = """\
475+
from pypop.notebook_interface import MetricsWizard
476+
from pypop.metrics import MPI_OpenMP_Metrics
477+
gui = MetricsWizard(MPI_OpenMP_Metrics)
478+
display(gui)\
479+
"""
480+
gui_cells = [(gui_code, hidden_code_cell)]
481+
482+
for cell_text, cell_ctr in gui_cells:
483+
gui_nb.cells.append(cell_ctr(cell_text))
484+
485+
nbformat.write(gui_nb, gui_nb_path)

pypop/server.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/enb python3
2+
3+
"""A minimal interface for managing IPython servers
4+
"""
5+
6+
import os
7+
import subprocess
8+
from time import sleep
9+
10+
from notebook.notebookapp import list_running_servers
11+
12+
13+
def get_cache_pid():
14+
15+
try:
16+
with open(os.path.expanduser("~/.cache/pypopserver.pid")) as fh:
17+
pid = int(fh.readline().strip())
18+
return pid
19+
except (FileNotFoundError, ValueError):
20+
return None
21+
22+
23+
def get_notebook_server_instance(try_use_existing=False):
24+
"""Create a notebook server instance to use. Optionally attempting to re-use existing
25+
instances.
26+
"""
27+
28+
pid = get_cache_pid()
29+
30+
servers = list_running_servers()
31+
32+
# If we already have a server, use that
33+
for server in servers:
34+
if server["pid"] == pid:
35+
return (server, None)
36+
37+
# Otherwise, if we are allowed, try to piggyback on another session
38+
if try_use_existing and servers:
39+
return (servers[0], None)
40+
41+
# Fine, I'll make my own server, with blackjack, and userhooks!
42+
try:
43+
server_process = subprocess.Popen(["jupyter", "notebook", "--no-browser"])
44+
except OSError as err:
45+
raise RuntimeError("Failed to start server: {}".format(err))
46+
print("Started Jupyter Notebook server pid {}".format(server_process.pid))
47+
# wait for 1 second for server to come up
48+
sleep(1)
49+
50+
server = None
51+
for retry in range(5):
52+
try:
53+
server = {s["pid"]: s for s in list_running_servers()}[server_process.pid]
54+
break
55+
except KeyError:
56+
# Sleep for increasing times to give server a chance to come up
57+
sleep(5)
58+
59+
if server:
60+
return (server, server_process)
61+
62+
# Don't leave orphans!
63+
server_process.kill()
64+
raise RuntimeError("Failed to acquire server instance after 25s")
65+
66+
67+
def construct_nb_url(server_info, nb_path):
68+
69+
nb_dir = server_info["notebook_dir"]
70+
71+
if os.path.isabs(nb_dir):
72+
nb_relpath = os.path.relpath(nb_path, nb_dir)
73+
if nb_relpath.startswith(".."):
74+
raise ValueError(
75+
"Requested notebook file path is not in the notebook server "
76+
"directory ({}).".format(nb_path, nb_dir)
77+
)
78+
else:
79+
nb_relpath = nb_path.lstrip(".").lstrip("/")
80+
81+
if server_info["token"]:
82+
return "{}notebooks/{}?token={}".format(
83+
server_info["url"], nb_relpath, server_info["token"]
84+
)
85+
else:
86+
return "{}notebooks/{}".format(server_info["url"], nb_relpath)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"pypop-preprocess = pypop.cli:preprocess_traces",
3737
"pypop-idealise-prv = pypop.cli:dimemas_idealise_cli",
3838
"pypop-copy-examples = pypop.cli:copy_examples",
39+
"pypop-gui = pypop.cli:pypop_gui",
3940
]
4041
},
4142
)

0 commit comments

Comments
 (0)