├── .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 |
--------------------------------------------------------------------------------