Creating a Jupyter Notebook App for OSX
Colin Bitterfield
Cybersecurity Leader | vCISO | Zero Trust Architect | GRC & Compliance Expert | Risk Mitigation & Cyber Transformation Specialist
Honestly, I like the ease of Apps on OSX and I got tired of loading Jupyter Notebook from the command-line. So I created a very basic Automator application so I can simply launch and go.
For those of you with little time, the application is downloadable from this link and is drop and go ready.
For everyone else, I will explain the fun issues of creating a drop and go application. Some of the issues are:
- Finding where "jupyter-notebook" is on the host system.
- Creating a working directory
- Creating a log file that can be viewed in real-time with the "Console App"
- Putting in a little safety logic
- And last but not least getting a decent icon for the application that is not the default.
1. Where is waldo
The binary file for jupyter-notebook can be in a few places and it will change if you update your python from 3.7 to 3.8 to 3.9 and so on.
If you are using Mac Ports, it will be in "/opt/local" somewhere, for HomeBrew it is /usr/local/Cellar, and if it is installed some other method, it could be in /usr/local somewhere. On my system it is located here:
/opt/local/Library/Frameworks/Python.framework/Versions/3.8/bin/jupyter-notebook
The fun part of this, is that "which" won't find it if the directory is not on the global path and OSX will use the sudo path for Automator tasks.
I came up with this logic to set an environment variable for the location.
# Script to run a jupyter notebook from a specific directory # We will check in /opt/local, /usr/local/Cellar and /usr/local if no file is found we exit with error if [ -f "$(eval "find /opt/local -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /opt/local -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /opt/local." >> "${JUPYTER_LOG}" elif [ -f "$(eval "find /usr/local/Cellar -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /usr/local/Cellar -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /usr/local/Cellar." >> "${JUPYTER_LOG}" elif [ -f "$(eval "find /usr/local/ -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /usr/local/ -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /usr/local/." >> "${JUPYTER_LOG}" else echo "$JUPYTER_NOTEBOOK not found in search directories, exiting" >> "${JUPYTER_LOG}" exit 1 fi # Test if Jupyter Notebook exists
It's not hugely sophisticated but it works. Using the find command with an exit after the first occurrence seemed to be the best choice. Actual mileage will vary and feedback is appreciated.
2. Working Directory
I am a bit "OCD" when it comes to finding files and making sure that they are not all over my system or created in the dark regions of my Mac.
# Create a working directory if it doesn't exist. export JUPYTER_HOME="$HOME/Documents/${JUPYTER_DIR}" [ ! -d "$JUPYTER_HOME" ] && mkdir "${JUPYTER_HOME}" cd "$JUPYTER_HOME" || exit # Create a subdirectory for the logs [ ! -d "$JUPYTER_HOME/logs" ] && mkdir "${JUPYTER_HOME}/logs"
I am using variables here and I create a sub-directory for logs. I have found being able to see the logs in real-time is very necessary when dealing with some of my projects; and I don't want log files mixed with my notebook files.
3. Creating a log file and viewing it in real-time in a "Mac-ish way"
# Log File name export JUPYTER_LOG="Jupyter-${USER}-$(date +'%Y-%M-%d-%s').log"
I set a log filename at the beginning of each run, so that it will change. This will allow me to keep or delete logs as I need to.
We start the console.app and load the log file so we can see it running.
# Create log file if missing [ ! -d "$JUPYTER_LOG" ] && touch "${JUPYTER_LOG}" # Open log file with Console so that messages are available for the user open -a console "${JUPYTER_LOG}"
Now I have a browser and a window for the logs
4. Putting in a little safety logic
If you look at the overall code, there are few checks for logic and exits when things don't work. And putting the whole script together.
## Written By Colin Bitterfield ## Version 1.0 # Note if you have weird startup issues look for the webbrowser.py file and change # if sys.platform[:3] == "win": (to) elif sys.platform[:3] == "win": # Around line 535 for python 3.8 # Document Directory export JUPYTER_DIR="Jupyter-Documents" # Log File name export JUPYTER_LOG="Jupyter-${USER}-$(date +'%Y-%M-%d-%s').log" # Change the PATH order to deal with MacPosts PHP # This should be set on the path correctly; however if not we fix it here # This is set "/etc/paths" (If you set in paths.d it will be after system directories) export PATH=/opt/local/bin:$PATH # Create a working directory if it doesn't exist. export JUPYTER_HOME="$HOME/Documents/${JUPYTER_DIR}" [ ! -d "$JUPYTER_HOME" ] && mkdir "${JUPYTER_HOME}" cd "$JUPYTER_HOME" || exit # Create a subdirectory for the logs [ ! -d "$JUPYTER_HOME/logs" ] && mkdir "${JUPYTER_HOME}/logs" # Create a log file in a variable export JUPYTER_LOG="$JUPYTER_HOME/logs/${JUPYTER_LOG}" echo "$(date) Starting Jupyter session for ${USER}" >> "${JUPYTER_LOG}" env >> "${JUPYTER_LOG}" # Script to run a jupyter notebook from a specific directory # We will check in /opt/local, /usr/local/Cellar and /usr/local if no file is found we exit with error if [ -f "$(eval "find /opt/local -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /opt/local -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /opt/local." >> "${JUPYTER_LOG}" elif [ -f "$(eval "find /usr/local/Cellar -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /usr/local/Cellar -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /usr/local/Cellar." >> "${JUPYTER_LOG}" elif [ -f "$(eval "find /usr/local/ -type f -name jupyter-notebook -print -quit 2>/dev/null")" ]; then export JUPYTER_NOTEBOOK="$(eval "find /usr/local/ -type f -name jupyter-notebook -print -quit 2>/dev/null")" echo "$JUPYTER_NOTEBOOK exists in /usr/local/." >> "${JUPYTER_LOG}" else echo "$JUPYTER_NOTEBOOK not found in search directories, exiting" >> "${JUPYTER_LOG}" exit 1 fi # Test if Jupyter Notebook exists # Create log file if missing [ ! -d "$JUPYTER_LOG" ] && touch "${JUPYTER_LOG}" # Open log file with Console so that messages are available for the user open -a console "${JUPYTER_LOG}" #Start Jupyter-notebook echo "CMD: ${JUPYTER_NOTEBOOK}" >> "${JUPYTER_LOG}" eval "${JUPYTER_NOTEBOOK}" >> "${JUPYTER_LOG}" 2>&1 || (echo "Failure to start" >> "${JUPYTER_LOG}"; exit) # Notice the log file echo "$(date) Ending Jupyter session for ${USER}" >> "${JUPYTER_LOG}"
5. Adding an icon, this turns out to be harder than it looks at first login.
Credit to this article for figuring it out.
Putting it all together as an application
Now we have our application in Automator.
I added a little voice to tell you it's working. Save it to the application directory. And we have a working application.
To stop the notebook, you need to click on the gear icon and the X to stop it.
Work left to do:
Currently, the application will fail to start if there is already one running. I should add some logic with a lock file to prevent a second version from running and let you know that's the problem. However, that will have to wait for another day
Comments and Suggestions are always welcome and I will put the code up in a repository on Github over the holidays. Feel free to fork and modify to as much as needed. Please provide a link to this article if you.