├── .github └── workflows │ ├── ci.yml │ └── pypi.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── configs ├── example-config.yaml └── lsst-example-config.yaml ├── doc ├── Makefile └── source │ ├── conf.py │ ├── index.rst │ └── readme.rst ├── environment.yml ├── pyproject.toml └── src └── stackyter ├── __init__.py └── stackyter.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "**" 9 | pull_request: 10 | 11 | env: 12 | GITHUB_PR_NUMBER: ${{ github.event.number }} 13 | 14 | jobs: 15 | user-install: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10", "3.11"] 20 | os: [ubuntu-latest, macos-latest] 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | - name: Python setup 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | check-latest: true 30 | - if: ${{ runner.os == 'macOS' }} 31 | name: Fix Python PATH on macOS 32 | # See https://github.com/actions/setup-python/issues/132 and 33 | # https://github.com/actions/setup-python/issues/132#issuecomment-779406058 34 | # Login shells on macOS prepend system paths, so we need to 35 | # prepend the python path from actions/setup-python. 36 | run: | 37 | tee -a ~/.bash_profile <<<'export PATH="$pythonLocation/bin:$PATH"' 38 | - name: Install dependencies 39 | run: | 40 | python --version 41 | pip install . 42 | pip freeze 43 | python -c 'import stackyter; print(stackyter.__version__)' 44 | stackyter -h 45 | 46 | developer-install: 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | matrix: 50 | python-version: ["3.8", "3.9", "3.10", "3.11"] 51 | os: [ubuntu-latest, macos-latest] 52 | steps: 53 | - uses: actions/checkout@v3 54 | with: 55 | fetch-depth: 0 56 | - name: Python setup 57 | uses: actions/setup-python@v4 58 | with: 59 | python-version: ${{ matrix.python-version }} 60 | check-latest: true 61 | - if: ${{ runner.os == 'macOS' }} 62 | name: Fix Python PATH on macOS 63 | # See https://github.com/actions/setup-python/issues/132 and 64 | # https://github.com/actions/setup-python/issues/132#issuecomment-779406058 65 | # Login shells on macOS prepend system paths, so we need to 66 | # prepend the python path from actions/setup-python. 67 | run: | 68 | tee -a ~/.bash_profile <<<'export PATH="$pythonLocation/bin:$PATH"' 69 | - name: Install dependencies 70 | run: | 71 | python --version 72 | pip install '.[all]' 73 | pip freeze 74 | python -c 'import stackyter; print(stackyter.__version__)' 75 | stackyter -h 76 | 77 | docs: 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | with: 82 | fetch-depth: 0 83 | 84 | - name: Set up Python 85 | uses: actions/setup-python@v4 86 | with: 87 | python-version: "3.8" 88 | 89 | - name: Install doc dependencies 90 | run: | 91 | sudo apt update --yes && sudo apt install --yes git build-essential 92 | pip install -e .[docs] 93 | git describe --tags 94 | python -c 'import stackyter; print(stackyter.__version__)' 95 | 96 | - name: Build docs 97 | run: | 98 | cd doc 99 | make html 100 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: pypi_publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | # make sure we have version info 14 | - run: git fetch --tags 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.8 20 | - name: build package 21 | run: | 22 | python --version 23 | pip install -U build 24 | python -m build 25 | - name: Publish a Python distribution to PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | with: 28 | user: __token__ 29 | password: ${{ secrets.PYPI_API_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.py[co] 3 | *.a 4 | *.o 5 | *.so 6 | __pycache__ 7 | 8 | # Packages/installer info 9 | *.egg 10 | *.eggs 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | distribute-*.tar.gz 22 | 23 | # Other 24 | .*.swp 25 | *~ 26 | \#* 27 | .\#* 28 | 29 | # Mac OSX 30 | .DS_Store 31 | 32 | # ipython notebook checkpoints 33 | .ipynb_checkpoints 34 | .cache* 35 | .cache 36 | 37 | # Sphinx 38 | doc/_build -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: doc/source/conf.py 17 | 18 | # Optionally declare the Python requirements required to build your docs 19 | python: 20 | install: 21 | - method: pip 22 | path: . 23 | extra_requirements: 24 | - docs 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nicolas Chotard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: http://readthedocs.org/projects/stackyter/badge/?version=latest 2 | :target: http://stackyter.readthedocs.io/en/latest/?badge=latest 3 | :alt: Documentation Status 4 | 5 | .. image:: https://landscape.io/github/nicolaschotard/stackyter/master/landscape.svg?style=flat 6 | :target: https://landscape.io/github/nicolaschotard/stackyter/master 7 | :alt: Code Health 8 | 9 | .. image:: https://badge.fury.io/py/stackyter.svg 10 | :target: https://badge.fury.io/py/stackyter 11 | 12 | .. inclusion-marker-do-not-remove 13 | 14 | Quick install and how-to 15 | ======================== 16 | 17 | Local display of a Jupyter notebook running on a distant server 18 | 19 | #. Install the latest version of ``stackyter`` on you local machine:: 20 | 21 | pip install stackyter 22 | 23 | #. Install `Jupyter `_ on your distant host if not done yet 24 | #. Create a file with instructions to make Jupyter (and anything else 25 | you need) available (e.g, ``mysetup.sh``) 26 | #. Run ``stackyter`` on your local machine:: 27 | 28 | stackyter --host thehost --user myusername --mysetup /path/on/the/host/mysetup.sh 29 | 30 | #. Copy/paste the given URL into your local browser to display Jupyter 31 | 32 | Purpose 33 | ======= 34 | 35 | This script allow you to run a jupyter notebook (or lab) on a distant 36 | server while displaying it localy in your local brower. It can be used 37 | by anyone and on any host using the ``--host`` and ``--mysetup`` 38 | options. The only prerequisite is that **Jupyter must be available on 39 | the distant host for this script to work.** 40 | 41 | Installation 42 | ============ 43 | 44 | Latest stable version can be installed with ``pip``:: 45 | 46 | pip install stackyter 47 | 48 | To upgrade to a newer version:: 49 | 50 | pip install --upgrade stackyter 51 | 52 | To install in a local directory:: 53 | 54 | pip install --user stackyter # in your home directory 55 | pip install --prefix mypath stackyter # in 'mypath' 56 | 57 | 58 | Usage 59 | ===== 60 | 61 | .. code-block:: shell 62 | 63 | stackyter [options] 64 | 65 | Then click on the green link given by ``stackyter``, as followed:: 66 | 67 | Copy/paste this URL into your browser to run the notebook localy 68 | http://localhost:20001/?token=38924c48136091ade71a597218f2722dc49c669d1430db41 69 | 70 | ``Ctrl-C`` will stop the Jupyter server and close the connection. 71 | 72 | You can use the following set of options to adapt ``stackyter`` to 73 | your personal case. 74 | 75 | 76 | Optional arguments 77 | ================== 78 | 79 | An option used on the command line will always overwrite the content 80 | of the configuration file for the same option, if defined. See the 81 | next section for a description on how to use the configuration 82 | file. Available options are:: 83 | 84 | -h, --help show this help message and exit 85 | -c CONFIG, --config CONFIG 86 | Name of the configuration to use, taken from your 87 | default configuration file (~/.stackyter-config.yaml 88 | or $STACKYTERCONFIG). Default if to use the 89 | 'default_config' defined in this file. The content of 90 | the configuration file will be overwritten by any 91 | given command line options. (default: None) 92 | -f CONFIGFILE, --configfile CONFIGFILE 93 | Configuration file containing a set of option values. 94 | The content of this file will be overwritten by any 95 | given command line options. (default: None) 96 | -H HOST, --host HOST Name of the target host. Allows you to connect to any 97 | host on which Jupyter is available, or to avoid 98 | conflit with the content of your $HOME/.ssh/config. 99 | (default: None) 100 | -u USERNAME, --username USERNAME 101 | Your user name on the host. If not given, ssh will try 102 | to figure it out from you ~/.ssh/config or will use 103 | your local user name. (default: None) 104 | -w WORKDIR, --workdir WORKDIR 105 | Your working directory on the host (default: None) 106 | -j JUPYTER, --jupyter JUPYTER 107 | Either launch a jupiter notebook or a jupyter lab. 108 | (default: notebook) 109 | --mysetup MYSETUP Path to a setup file (on the host) that will be used 110 | to set up the working environment. A Python 111 | installation with Jupyter must be available to make 112 | this work. (default: None) 113 | --runbefore RUNBEFORE 114 | A list of extra commands to run BEFORE sourcing your 115 | setup file. Coma separated for more than one commands, 116 | or a list in the config file. (default: None) 117 | --runafter RUNAFTER A list of extra commands to run AFTER sourcing your 118 | setup file. Coma separated for more than one commands, 119 | or a list in the config file. (default: None) 120 | -C, --compression Activate ssh compression option (-C). (default: False) 121 | -S, --showconfig Show all available configurations from your default 122 | file and exit. (default: False) 123 | 124 | 125 | Configuration file 126 | ================== 127 | 128 | A configuration dictionnary can contain any options available through 129 | the command line. The options found in the configuration file will 130 | always be overwritten by the command line. 131 | 132 | The configuration file can be given in different ways, and can 133 | contains from a single configuration dictionnary to several 134 | configuration dictionnaries: 135 | 136 | - The **configuration file** can either be a default file located 137 | under ``~/stackyter-config.yaml`` or defined by the 138 | ``STACKYTERCONFIG``, or given in command line using the 139 | ``--configfile`` option. 140 | 141 | - The **configuration name**, which should be defined in your 142 | configuration file, must be given using the command line option 143 | ``--config``. If not given, a ``default_config``, which should be 144 | defined in your configration file, will be used by default. 145 | 146 | Here are a few example on how to use it:: 147 | 148 | stackyter # 'default_config' in default file if it exists, default option values used otherwise 149 | stackyter --config config1 # 'config1' in default file which must exist 150 | stackyter --config config2 --configfile myfile.yaml # 'config2' in 'myfile.yaml' 151 | stackyter --configfile myfile.yaml # 'default_config' in 'myfile.yaml' 152 | 153 | In principal, your default configuration file should look like that:: 154 | 155 | { 156 | 'default_config': 'host1', 157 | 158 | 'host1': { 159 | 'host': 'myhost.domain.fr', # 'myhost' if you have configured your ~/.ssh/config 160 | 'jupyter': 'lab', # if installed 161 | 'username': 'myusername', 162 | 'mysetup': '/path/to/my/setup/file.sh', 163 | 'workdir': '/path/to/my/directory/' 164 | }, 165 | 166 | 'host2': { 167 | 'host': 'otherhost.fr', 168 | 'username': 'otherusername', 169 | 'mysetup': '/path/to/my/setup' 170 | }, 171 | 172 | 'host3': { 173 | 'host': 'somewhere.edu', 174 | 'username': 'ausername', 175 | # Jupyter is available by default on this host, 'mysetup' is not needed 176 | }, 177 | } 178 | 179 | or simply as followed if only one configuration is defined:: 180 | 181 | { 182 | 'host1': { 183 | 'host': 'myhost.domain.fr', # or 'myhost' if you have configured your ~/.ssh/config file 184 | 'jupyter': 'lab', # if installed 185 | 'username': 'myusername', 186 | 'mysetup': '/path/to/my/setup/file.sh', 187 | 'workdir': '/path/to/my/directory/' 188 | }, 189 | } 190 | 191 | You can use the `example 192 | `_ 193 | configuration file as a template to create your own. You can also find 194 | several example configuration files in the `configs 195 | `_ 196 | directory for different user cases. 197 | 198 | 199 | Distant host configuration 200 | ========================== 201 | 202 | The ``--host`` option allows you to connect to any distant host. The 203 | default option used to create the ``ssh`` tunnel are ``-X -Y -tt 204 | -L``. If you want to configure your ``ssh`` connection, edit your 205 | ``~/.ssh/config`` file using, for instance, the following template:: 206 | 207 | Host myjupyter 208 | Hostname thehostname 209 | User myusername 210 | GSSAPIClientIdentity myusername@HOST 211 | GSSAPIAuthentication yes 212 | GSSAPIDelegateCredentials yes 213 | GSSAPITrustDns yes 214 | 215 | You only need to replace ``thehostname``, ``myusername``, and 216 | ``myusername@HOST`` by the appropriate values. You can then use the 217 | ``stackyter`` script as follows:: 218 | 219 | stackyter --host myjupyter 220 | 221 | Or put the value for that option (along with others) in your 222 | ``config.yaml`` file. 223 | 224 | Working environment 225 | =================== 226 | 227 | There are several ways to setup your personnal working environment, 228 | using the ``--mysetup``, ``--runbefore``, and ``runafter`` 229 | options. Given a setup file located on your distant host, you can 230 | simply do:: 231 | 232 | stackyter --mysetup /path/to/my/setup.sh (--username myusername) 233 | 234 | Your local setup file will be sourced at connection as followed:: 235 | 236 | source /path/to/my/setup.sh 237 | 238 | The ``runbefore`` and ``runafter`` options allow you to respectively 239 | run command lines before or after your setup file is sourced. It can 240 | be useful if you need to pass argument to your setup file through 241 | environment variables, or add extra command after the sourcing. 242 | 243 | Your setup must **at least** contains what is needed to make 244 | Jupyter available. If Jupyter is available by default on the distant 245 | host (it might be set up on connection), you only need to use the 246 | ``--host`` and ``--username`` option to run. 247 | 248 | You can of course add any kind of personal setups with these three 249 | options, related or not to Jupyter. 250 | 251 | Help 252 | ==== 253 | 254 | - If you have any comments or suggestions, or if you find a bug, 255 | please use the dedicated github `issue tracker 256 | `_. 257 | - Why ``stakyter``? For historical reason: ``stackyter`` = LSST 258 | ``stack`` + ``Jupyter``. It was initially intended for LSST members 259 | to easily use the LSST software stack and interact with data sets. 260 | 261 | Development contributions 262 | ========================= 263 | 264 | Is it recommended to work from a virtual environment. 265 | 266 | The root of the repository hosts an ``environment.yml`` file to produce a 267 | ``stackyter-dev`` conda environment, 268 | 269 | .. code-block:: 270 | 271 | conda env create -f environment.yml 272 | 273 | This environment comes with `Ruff `_ installed, 274 | which you can use to check for syntax, style and general linting. 275 | 276 | Build of the documentation 277 | -------------------------- 278 | 279 | You can build the documentation locally by doing, 280 | 281 | .. code-block:: 282 | 283 | cd doc 284 | make html 285 | 286 | and check how it looks by opening ``docs/_build/html/index.html`` 287 | with your favourite browser. -------------------------------------------------------------------------------- /configs/example-config.yaml: -------------------------------------------------------------------------------- 1 | { 2 | 'default_config': 'host1', 3 | 4 | 'host1': { 5 | 'host': 'myhost.domain.fr', # or 'myhost' if you have configured your ~/.ssh/config file 6 | 'jupyter': 'lab', # if installed 7 | 'username': 'myusername', 8 | 'mysetup': '/path/to/my/setup/file.sh', 9 | 'workdir': '/path/to/my/directory/' 10 | }, 11 | 12 | 'host2': { 13 | 'host': 'otherhost.fr', 14 | 'username': 'otherusername', 15 | 'jump': 'username1@hophost1,username2@hophost2', 16 | 'mysetup': '/path/to/my/setup' 17 | }, 18 | } -------------------------------------------------------------------------------- /configs/lsst-example-config.yaml: -------------------------------------------------------------------------------- 1 | { 2 | 'default_config': 'stackatcc', 3 | 4 | 'stackatcc': { 5 | 'host': 'cca7.in2p3.fr', 6 | 'username': 'MYUSERNAME', 7 | 'runbefore': ["source /sps/lsst/software/lsst_distrib/w_2018_08/loadLSST.bash", 8 | "setup lsst_distrib", 9 | "export PATH=$PATH:/pbs/throng/lsst/users/nchotard/local/bin" 10 | ], 11 | }, 12 | 13 | 'lsstdesc': { 14 | 'host': 'cca7.in2p3.fr', 15 | 'username': 'MYUSERNAME', 16 | 'runbefore': "source /pbs/throng/lsst/software/desc/setup.sh", 17 | }, 18 | 19 | 'descdc2': { 20 | 'host': 'cca7.in2p3.fr', 21 | 'username': 'MYUSERNAME', 22 | 'runbefore': ["source /sps/lsst/software/lsst_distrib/w_2018_15/loadLSST.bash", 23 | "setup lsst_distrib", 24 | "export DC2SOFT=/pbs/throng/lsst/software/desc/DC2/Run1.1-test2/", 25 | "eups declare -r $DC2SOFT/obs_lsstSim obs_lsstSim w201815_descdc2", 26 | "setup obs_lsstSim w201815_descdc2", 27 | "export PATH=$PATH:/pbs/throng/lsst/users/nchotard/local/bin" 28 | ], 29 | 'workdir': '/pbs/throng/lsst/users/MYUSERNAME/', # just an example 30 | }, 31 | 32 | } -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W --keep-going -n --color 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = stackyter 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | clean: 16 | rm -rf "$(BUILDDIR)" 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # stackyter documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Nov 27 14:38:09 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | import stackyter 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ["sphinx.ext.autodoc"] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = ".rst" 45 | 46 | # The master toctree document. 47 | master_doc = "index" 48 | 49 | # General information about the project. 50 | project = "stackyter" 51 | copyright = "2017, N. Chotard" 52 | author = "N. Chotard" 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | # The full version, including alpha/beta/rc tags. 60 | # release = '0.12' 61 | version = stackyter.__version__ 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | # language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = "sphinx" 77 | 78 | # If true, `todo` and `todoList` produce output, else they produce nothing. 79 | todo_include_todos = False 80 | 81 | 82 | # -- Options for HTML output ---------------------------------------------- 83 | 84 | # The theme to use for HTML and HTML Help pages. See the documentation for 85 | # a list of builtin themes. 86 | # 87 | html_theme = "sphinx_rtd_theme" 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | # html_static_path = ["_static"] 99 | 100 | # Custom sidebar templates, must be a dictionary that maps document names 101 | # to template names. 102 | # 103 | # This is required for the alabaster theme 104 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 105 | html_sidebars = { 106 | "**": [ 107 | "about.html", 108 | "navigation.html", 109 | "relations.html", # needs 'show_related': True theme option to display 110 | "searchbox.html", 111 | "donate.html", 112 | ] 113 | } 114 | 115 | 116 | # -- Options for HTMLHelp output ------------------------------------------ 117 | 118 | # Output file base name for HTML help builder. 119 | htmlhelp_basename = "stackyterdoc" 120 | 121 | 122 | # -- Options for LaTeX output --------------------------------------------- 123 | 124 | latex_elements = { 125 | # The paper size ('letterpaper' or 'a4paper'). 126 | # 127 | # 'papersize': 'letterpaper', 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | # Additional stuff for the LaTeX preamble. 132 | # 133 | # 'preamble': '', 134 | # Latex figure (float) alignment 135 | # 136 | # 'figure_align': 'htbp', 137 | } 138 | 139 | # Grouping the document tree into LaTeX files. List of tuples 140 | # (source start file, target name, title, 141 | # author, documentclass [howto, manual, or own class]). 142 | latex_documents = [ 143 | (master_doc, "stackyter.tex", "stackyter Documentation", "N. Chotard", "manual"), 144 | ] 145 | 146 | 147 | # -- Options for manual page output --------------------------------------- 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [(master_doc, "stackyter", "stackyter Documentation", [author], 1)] 152 | 153 | 154 | # -- Options for Texinfo output ------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | ( 161 | master_doc, 162 | "stackyter", 163 | "stackyter Documentation", 164 | author, 165 | "stackyter", 166 | "One line description of project.", 167 | "Miscellaneous", 168 | ), 169 | ] 170 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. stackyter documentation master file, created by 2 | sphinx-quickstart on Mon Nov 27 14:38:09 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to stackyter's documentation! 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | readme 13 | -------------------------------------------------------------------------------- /doc/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | :start-after: inclusion-marker-do-not-remove 3 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # Notes for developers: 2 | # - maintain readability by adding each new entry according to which context it belongs 3 | # - unless it's not possible, privilege conda over pip 4 | # - add a new channel only if the package you want to add cannot be found in the ones currently used 5 | # - specify versions only if necessary (e.g. upstream bug) otherwise modernize the code 6 | name: stackyter-dev 7 | channels: 8 | - conda-forge 9 | dependencies: 10 | # Basic dependencies 11 | - python 12 | - pip 13 | - pyyaml 14 | # Development 15 | - ruff 16 | - setuptools_scm 17 | # Documentation 18 | - make 19 | - sphinx 20 | - sphinx_rtd_theme 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools_scm[toml]"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = 'stackyter' 7 | authors = [{ name = "Nicolas Chotard", email = "nchotard@in2p3.fr" }] 8 | description = "Local display of a jupyter notebook running on a remote server" 9 | keywords = ["jupyter", "remote"] 10 | readme = "README.rst" 11 | requires-python = ">=3.8" 12 | license = { file = "LICENSE" } 13 | classifiers = [ 14 | 'Development Status :: 3 - Alpha', 15 | 'Intended Audience :: Science/Research', 16 | 'Topic :: Software Development :: Build Tools', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Programming Language :: Python :: 3', 19 | 'Topic :: Scientific/Engineering :: Astronomy', 20 | ] 21 | dependencies = ["pyyaml"] 22 | dynamic = ["version"] 23 | 24 | [project.optional-dependencies] 25 | dev = ["setuptools_scm[toml]", "ruff"] 26 | docs = ["sphinx", "sphinx_rtd_theme"] 27 | all = ["stackyter[dev]", "stackyter[docs]"] 28 | 29 | [tool.setuptools_scm] 30 | 31 | [project.urls] 32 | documentation = "https://stackyter.readthedocs.io/en/latest/" 33 | repository = "https://github.com/nicolaschotard/stackyter" 34 | 35 | [project.scripts] 36 | stackyter = "stackyter.stackyter:main" 37 | -------------------------------------------------------------------------------- /src/stackyter/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | stackyter - Local display of a jupyter notebook running on a remote server 3 | Licensed under a MIT style license - see LICENSE file 4 | """ 5 | 6 | from importlib.metadata import version, PackageNotFoundError 7 | 8 | try: 9 | __version__ = version("stackyter") 10 | except PackageNotFoundError: 11 | raise PackageNotFoundError from None 12 | -------------------------------------------------------------------------------- /src/stackyter/stackyter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Run jupyter on a given host and display it localy.""" 3 | 4 | import os 5 | import sys 6 | import subprocess 7 | from argparse import ArgumentParser 8 | from argparse import ArgumentDefaultsHelpFormatter 9 | import yaml 10 | import random 11 | 12 | 13 | DEFAULT_CONFIG = os.getenv("HOME") + "/.stackyter-config.yaml" 14 | 15 | 16 | def string_to_list(a): 17 | """Transform a string with coma separated values to a list of values.""" 18 | return a if isinstance(a, list) or a is None else a.split(",") 19 | 20 | 21 | def get_default_config(only_path=False): 22 | """Get the stackyter default configuration file if it exists.""" 23 | if os.getenv("STACKYTERCONFIG") is not None: # set up by the user 24 | config = os.getenv("STACKYTERCONFIG") 25 | if not os.path.exist(config): 26 | raise IOError("$STACKYTERCONFIG is defined but the file does not exist.") 27 | elif os.path.exists(DEFAULT_CONFIG): # default location 28 | config = DEFAULT_CONFIG 29 | else: 30 | return None 31 | return ( 32 | yaml.load(open(config, "r"), Loader=yaml.SafeLoader) 33 | if not only_path 34 | else config 35 | ) 36 | 37 | 38 | def read_config(config, key=None): 39 | """Read a config file and return the right configuration.""" 40 | print("INFO: Loading configuration from", config) 41 | config = yaml.load(open(config, "r"), Loader=yaml.SafeLoader) 42 | if key is not None: 43 | if key in config: 44 | print("INFO: Using the '%s' configuration" % key) 45 | config = config[key] 46 | else: 47 | raise IOError( 48 | "Configuration `%s` does not exist. Check your default file." % key 49 | ) 50 | elif len(config) > 1: 51 | if "default_config" in config: 52 | print("INFO: Using default configuration '%s'" % config["default_config"]) 53 | config = config[config["default_config"]] 54 | else: 55 | raise IOError( 56 | "You must define a 'default_config' in you configuration file." 57 | ) 58 | else: 59 | config = config[list(config)[0]] 60 | return config 61 | 62 | 63 | def get_config(config, configfile): 64 | """Get the configuration for stackyter is any.""" 65 | configfile = ( 66 | get_default_config(only_path=True) if configfile is None else configfile 67 | ) 68 | if config is not None: 69 | # Is there a configuration file? 70 | if configfile is None: 71 | raise IOError( 72 | "No (default) configuration file found or given. Check the doc." 73 | ) 74 | config = read_config(configfile, key=config) 75 | elif configfile is not None: 76 | config = read_config(configfile) 77 | return config 78 | 79 | 80 | def main(): 81 | 82 | description = """Run Jupyter on a distant host and display it localy.""" 83 | prog = "stackyter.py" 84 | usage = """%s [options]""" % prog 85 | 86 | parser = ArgumentParser( 87 | prog=prog, 88 | usage=usage, 89 | description=description, 90 | formatter_class=ArgumentDefaultsHelpFormatter, 91 | ) 92 | 93 | # General options 94 | parser.add_argument( 95 | "-c", 96 | "--config", 97 | default=None, 98 | help="Name of the configuration to use, taken from your default " 99 | "configuration file (~/.stackyter-config.yaml or $STACKYTERCONFIG). " 100 | "Default if to use the 'default_config' defined in this file. " 101 | "The content of the configuration file will be overwritten by any " 102 | "given command line options.", 103 | ) 104 | parser.add_argument( 105 | "-f", 106 | "--configfile", 107 | default=None, 108 | help="Configuration file containing a set of option values. The content " 109 | "of this file will be overwritten by any given command line options.", 110 | ) 111 | parser.add_argument( 112 | "-H", 113 | "--host", 114 | default=None, 115 | help="Name of the target host. Allows you to connect to any host " 116 | "on which Jupyter is available, or to avoid conflit with the " 117 | "content of your $HOME/.ssh/config.", 118 | ) 119 | parser.add_argument( 120 | "-u", 121 | "--username", 122 | help="Your user name on the host. If not given, ssh will try to " 123 | "figure it out from you ~/.ssh/config or will use your local user name.", 124 | ) 125 | parser.add_argument( 126 | "-J", 127 | "--jump", 128 | default=None, 129 | help="jump hosts or gateways in the form username@host. For serveral hops," 130 | " give them ordered and separated by a coma.", 131 | ) 132 | parser.add_argument( 133 | "-w", 134 | "--workdir", 135 | default="$HOME", 136 | help="Your working directory on the remote host", 137 | ) 138 | parser.add_argument( 139 | "-j", 140 | "--jupyter", 141 | default="notebook", 142 | help="Either launch Jupyter notebook or Jupyter lab.", 143 | ) 144 | parser.add_argument( 145 | "--mysetup", 146 | default=None, 147 | help="Path to a setup file (on the host) that will be used to set up the " 148 | "working environment. A Python installation with Jupyter must be " 149 | "available to make this work.", 150 | ) 151 | parser.add_argument( 152 | "--runbefore", 153 | default=None, 154 | help="A list of extra commands to run BEFORE sourcing your setup file." 155 | " Coma separated for more than one commands, or a list in the config file.", 156 | ) 157 | parser.add_argument( 158 | "--runafter", 159 | default=None, 160 | help="A list of extra commands to run AFTER sourcing your setup file." 161 | " Coma separated for more than one commands, or a list in the config file.", 162 | ) 163 | parser.add_argument( 164 | "-C", 165 | "--compression", 166 | action="store_true", 167 | default=False, 168 | help="Activate ssh compression option (-C).", 169 | ) 170 | parser.add_argument( 171 | "-S", 172 | "--showconfig", 173 | action="store_true", 174 | default=False, 175 | help="Show all available configurations from your default file and exit.", 176 | ) 177 | parser.add_argument( 178 | "--localport", 179 | default=20001, 180 | type=int, 181 | help="Local port to use to connect to the distant machine." 182 | "The default value is 20001.", 183 | ) 184 | 185 | args = parser.parse_args() 186 | default_args = parser.parse_args(args=[]) 187 | 188 | # Show available configuration(s) is any and exit 189 | if args.showconfig: 190 | config = get_default_config(only_path=True) 191 | if config is not None: 192 | config = open(config, "r") 193 | print( 194 | "Your default configuration file contains the following configuration(s)." 195 | ) 196 | print(config.read()) 197 | config.close() 198 | else: 199 | print("Error: No default configuration file found.") 200 | sys.exit(0) 201 | 202 | # Do we have a configuration file 203 | config = get_config(args.config, args.configfile) 204 | if config is not None: 205 | for opt, val in args._get_kwargs(): 206 | # only keep option value from the config file 207 | # if the user has not set it up from command line 208 | if opt in config and args.__dict__[opt] == default_args.__dict__[opt]: 209 | setattr(args, opt, config[opt]) 210 | 211 | # Do we have a valid host name 212 | if args.host is None: 213 | raise ValueError("You must give a valid host name (--host)") 214 | 215 | # Do we have a valid username 216 | args.username = "" if args.username is None else args.username + "@" 217 | 218 | # Do we have a valid Jupyter flavor 219 | if args.jupyter not in ("notebook", "lab"): 220 | raise ValueError( 221 | f"Invalid Jupyter flavor '{args.jupyter}': expecting either 'notebook' or 'lab'" 222 | ) 223 | 224 | # Make sure that we have a list (even empty) for extra commands to run 225 | args.runbefore = string_to_list(args.runbefore) 226 | args.runafter = string_to_list(args.runafter) 227 | 228 | # A random port number is selected between 1025 and 65635 (included) for server side to 229 | # prevent from conflict between users. 230 | port = random.randint(1025, 65635) 231 | 232 | # Should we use a jump host? 233 | jumphost = f"-J {args.jump}" if args.jump else "" 234 | 235 | # Do we have to run something before sourcing the setup file ? 236 | run_before = ( 237 | "".join([run.replace("$", "\$") + "; " for run in args.runbefore]) 238 | if args.runbefore 239 | else "" 240 | ) 241 | 242 | # Do we have to run something after sourcing the setup file ? 243 | run_after = ( 244 | "".join([run.replace("$", "\$") + "; " for run in args.runafter]) 245 | if args.runafter 246 | else "" 247 | ) 248 | 249 | # Use the setup file given by the user to set up the working environment 250 | user_setup = f"source {args.mysetup}" if args.mysetup else "" 251 | 252 | script = f""" 253 | #!/bin/bash 254 | if [[ ! -d {args.workdir} ]]; then 255 | echo 'Error: directory {args.workdir} does not exist' 256 | exit 1 257 | fi 258 | cd {args.workdir} 259 | {run_before} 260 | {user_setup} 261 | {run_after} 262 | jupyter_version=$(jupyter --version | grep '{args.jupyter}' | awk '{{print $NF}}' | awk -F '.' '{{print $1}}') 263 | function get_servers() {{ 264 | local version=$1 265 | local flavor="{args.jupyter}" 266 | local servers="" 267 | local cmd="echo" 268 | case ${{flavor}} in 269 | notebook) 270 | cmd="jupyter notebook list" 271 | ;; 272 | lab) 273 | if [[ $version -le 2 ]]; then 274 | cmd="jupyter notebook list" 275 | else 276 | cmd="jupyter server list" 277 | fi 278 | ;; 279 | esac 280 | echo `$cmd 2> /dev/null | grep '127.0.0.1:{port}' ` 281 | }} 282 | set -m # For job control 283 | jupyter {args.jupyter} --no-browser --port={port} --ip=127.0.0.1 & 284 | jupyter_pid=$! 285 | for i in $(seq 1 10); do 286 | sleep 2s 287 | servers=$(get_servers $jupyter_version) 288 | if [[ $servers == *"127.0.0.1:{port}"* ]]; then 289 | break 290 | fi 291 | echo 'waiting...' 292 | done 293 | if [[ -z ${{servers}} ]]; then 294 | echo 'could not determine the URL of the Jupyter server' 295 | kill -TERM ${{jupyter_pid}} &> /dev/null 296 | exit 1 297 | fi 298 | token=$(echo $servers | grep token | sed 's|^http.*?token=||g' | awk '{{print $1}}') 299 | printf "\nCopy/paste the URL below into your browser to open your notebook \n\n\\x1B[01;92m http://localhost:{args.localport}/?token=%s \\x1B[0m\\n\\n" $token 300 | fg 301 | kill -TERM ${{jupyter_pid}} &> /dev/null 302 | exit 0 303 | """ 304 | 305 | # Establish the SSH tunnel and run the shell script 306 | cmd = f"ssh {jumphost} -X -Y {'-C' if args.compression else ''} -tt -L {args.localport}:localhost:{port} {args.username}{args.host}" 307 | proc = subprocess.run( 308 | cmd, input=script.encode(), stderr=subprocess.STDOUT, shell=True 309 | ) 310 | sys.exit(proc.returncode) 311 | 312 | 313 | if __name__ == "__main__": 314 | main() 315 | --------------------------------------------------------------------------------