├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── requirements.txt
├── screenshot.png
├── setup.py
├── version.py
└── wrapspawner
├── __init__.py
└── wrapspawner.py
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [released]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install build twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: '__token__'
28 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
29 | run: |
30 | python -m build
31 | python -m twine upload dist/*
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## unreleased changes
4 |
5 | Added (user)
6 |
7 | Added (developer)
8 |
9 | Changed
10 |
11 | Fixed
12 |
13 | ## v1.0.1
14 |
15 | Minor release to publish bugfix: #45 - Fix common_traits calculation
16 |
17 | ## v1.0.0
18 |
19 | Initial proper release published to PyPI. Believed to work with Jupyterhub versions >= 0.9, <= 1.1.
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html).
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2017, Project Jupyter Contributors
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | include LICENSE
3 | include version.py
4 | include requirements.txt
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wrapspawner for Jupyterhub
2 |
3 | This package includes **WrapSpawner** and **ProfilesSpawner**, which provide mechanisms for runtime configuration of spawners.
4 | The inspiration for their development was to allow users to select from a range of pre-defined batch job profiles, but
5 | their operation is completely generic.
6 |
7 | ## Installation
8 |
9 | 1. Most users can install via pip:
10 |
11 | `pip install wrapspawner`
12 |
13 | To install an editable copy for development, from root directory of this repo (where setup.py is), run `pip install -e .`
14 | See also [pip VCS support](https://pip.pypa.io/en/stable/reference/pip_install/#vcs-support) if you need a specific revision.
15 |
16 | 2. Add lines in `jupyterhub_config.py` for the spawner you intend to use, e.g.
17 |
18 | ```python
19 | c.JupyterHub.spawner_class = 'wrapspawner.ProfilesSpawner'
20 | ```
21 | 3. Depending on the spawner, additional configuration will likely be needed.
22 |
23 | ## Wrapper and Profile Spawners
24 |
25 | ### Overview
26 |
27 | [`WrapSpawner`](https://github.com/jupyterhub/wrapspawner/blob/master/wrapspawner/wrapspawner.py#L1)
28 | provides a mechanism to wrap the interface of a JupyterHub Spawner such that
29 | the Spawner class to use for single-user servers can be chosen dynamically.
30 | Subclasses may modify the class or properties of the child Spawner at any point
31 | before `start()` is called (e.g. from Authenticator `pre_spawn` hooks or options form
32 | processing) and that state will be preserved on restart. The `start/stop/poll`
33 | methods are not real coroutines, but simply pass through the Futures returned
34 | by the wrapped Spawner class.
35 |
36 | [`ProfilesSpawner`](https://github.com/jupyterhub/wrapspawner/blob/master/wrapspawner/wrapspawner.py#L120)
37 | leverages JupyterHub's `Spawner` "options form" feature to allow user-driven
38 | configuration of Spawner classes while permitting:
39 |
40 | * configuration of Spawner classes that don't natively implement `options_form`
41 | * administrator control of allowed configuration changes
42 | * runtime choice of which Spawner backend to launch
43 |
44 | ### Example
45 |
46 | Here is a screenshot of a typical dropdown menu letting the user choose between several SLURM instances:
47 | 
48 |
49 | The following configuration snippet lets the user choose between a Jupyter server
50 | running as a local process or one of two different Docker Images to run within `DockerSpawner`.
51 |
52 | ```python
53 | c.JupyterHub.spawner_class = 'wrapspawner.ProfilesSpawner'
54 | c.Spawner.http_timeout = 120
55 | #------------------------------------------------------------------------------
56 | # ProfilesSpawner configuration
57 | #------------------------------------------------------------------------------
58 | # List of profiles to offer for selection. Signature is:
59 | # List(Tuple( Unicode, Unicode, Type(Spawner), Dict ))
60 | # corresponding to profile display name, unique key, Spawner class,
61 | # dictionary of spawner config options.
62 | #
63 | # The first three values will be exposed in the input_template as {display},
64 | # {key}, and {type}
65 | #
66 | c.ProfilesSpawner.profiles = [
67 | ( "Host process", 'local', 'jupyterhub.spawner.LocalProcessSpawner', {'ip':'0.0.0.0'} ),
68 | ('Docker Python 3', 'singleuser', 'dockerspawner.SystemUserSpawner', dict(image="jupyterhub/singleuser")),
69 | ('Docker Python 3 Scipy', 'scipy-notebook', 'dockerspawner.SystemUserSpawner', dict(image="jupyter/scipy-notebook")),
70 | ]
71 | ```
72 |
73 | ## History
74 |
75 | These mechanisms originated as part of the [`batchspawner`](https://github.com/jupyterhub/batchspawner) package.
76 | The `batchspawner` README contains additional examples on the use of ProfilesSpawner.
77 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jupyterhub>=2.2
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterhub/wrapspawner/cb74046aa0217cf48c76944fae2d8c0cbae2a701/screenshot.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | # Copyright (c) Jupyter Development Team.
5 | # Distributed under the terms of the Modified BSD License.
6 |
7 | #-----------------------------------------------------------------------------
8 | # Minimal Python version sanity check (from IPython/Jupyterhub)
9 | #-----------------------------------------------------------------------------
10 |
11 | from __future__ import print_function
12 |
13 | import os
14 | import sys
15 |
16 | from distutils.core import setup
17 |
18 | pjoin = os.path.join
19 | here = os.path.abspath(os.path.dirname(__file__))
20 |
21 | # Get the current package version.
22 | version_ns = {}
23 | with open(pjoin(here, 'version.py')) as f:
24 | exec(f.read(), {}, version_ns)
25 |
26 | setup_args = dict(
27 | name = 'wrapspawner',
28 | packages = ['wrapspawner'],
29 | version = version_ns['__version__'],
30 | description = """Wrapspawner: A spawner for Jupyterhub to wrap other spawners and allow the user to choose among them.""",
31 | long_description = "",
32 | author = "Michael Milligan",
33 | author_email = "milligan@umn.edu",
34 | url = "http://jupyter.org",
35 | license = "BSD",
36 | platforms = "Linux, Mac OS X",
37 | keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
38 | classifiers = [
39 | 'Intended Audience :: Developers',
40 | 'Intended Audience :: System Administrators',
41 | 'Intended Audience :: Science/Research',
42 | 'License :: OSI Approved :: BSD License',
43 | 'Programming Language :: Python',
44 | 'Programming Language :: Python :: 3',
45 | ],
46 | )
47 |
48 | # setuptools requirements
49 | if 'setuptools' in sys.modules:
50 | setup_args['install_requires'] = install_requires = []
51 | with open('requirements.txt') as f:
52 | for line in f.readlines():
53 | req = line.strip()
54 | if not req or req.startswith(('-e', '#')):
55 | continue
56 | install_requires.append(req)
57 |
58 |
59 | def main():
60 | setup(**setup_args)
61 |
62 | if __name__ == '__main__':
63 | main()
64 |
--------------------------------------------------------------------------------
/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | version_info = (
5 | 1,
6 | 0,
7 | 2,
8 | 'dev', # comment-out this line for a release
9 | )
10 | __version__ = '.'.join(map(str, version_info[:3]))
11 |
12 | if len(version_info) > 3:
13 | __version__ = '%s-%s' % (__version__, version_info[3])
14 |
--------------------------------------------------------------------------------
/wrapspawner/__init__.py:
--------------------------------------------------------------------------------
1 | from .wrapspawner import *
2 |
--------------------------------------------------------------------------------
/wrapspawner/wrapspawner.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Regents of the University of Minnesota
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | """WrapSpawner classes
5 |
6 | WrapSpawner provides a mechanism to wrap the interface of a Spawner such that
7 | the Spawner class to use for single-user servers can be chosen dynamically.
8 | The child Spawner is created and started using the same logic as in User.spawn(),
9 | but not until start() or load_state() is called. Thus, subclasses may modify the
10 | class or properties of the child Spawner at any earlier point (e.g. from
11 | Authenticator pre_spawn hooks or options form processing).
12 |
13 | Note that there is no straightforward way to simply save the child_class Type
14 | value in the ORM, so a subclass that provides a way to change the child_class
15 | must also arrange to reproduce that change when state is reloaded.
16 |
17 | Provided as an initial application is ProfilesSpawner, which accepts a list
18 | of provided Spawner configurations, and generates an options form from that list.
19 | Using this mechanism, the administrator can provide users with a pre-approved
20 | selection of Spawner configurations.
21 | """
22 |
23 | import json
24 | import re
25 | import urllib.request
26 |
27 | from tornado import concurrent
28 |
29 | from jupyterhub.spawner import LocalProcessSpawner, Spawner
30 | from traitlets import (
31 | Instance, Type, Tuple, List, Dict, Unicode, Any
32 | )
33 | from traitlets import directional_link, validate, TraitError
34 |
35 | # Only needed for DockerProfilesSpawner
36 | try:
37 | import docker
38 | except ImportError:
39 | pass
40 |
41 | # Utility to create dummy Futures to return values through yields
42 | def _yield_val(x=None):
43 | f = concurrent.Future()
44 | f.set_result(x)
45 | return f
46 |
47 | class WrapSpawner(Spawner):
48 |
49 | # Grab this from constructor args in case some Spawner ever wants it
50 | config = Any()
51 |
52 | child_class = Type(LocalProcessSpawner, Spawner,
53 | config=True,
54 | help="""The class to wrap for spawning single-user servers.
55 | Should be a subclass of Spawner.
56 | """
57 | )
58 |
59 | child_config = Dict(default_value={},
60 | config=True,
61 | help="Dictionary of config values to apply to wrapped spawner class."
62 | )
63 |
64 | child_state = Dict(default_value={})
65 |
66 | child_spawner = Instance(Spawner, allow_none=True)
67 |
68 | def construct_child(self):
69 | if self.child_spawner is None:
70 | self.child_spawner = self.child_class(
71 | user = self.user,
72 | db = self.db,
73 | hub = self.hub,
74 | authenticator = self.authenticator,
75 | oauth_client_id = self.oauth_client_id,
76 | cookie_options = self.cookie_options,
77 | orm_spawner = self.orm_spawner,
78 | server = self._server,
79 | config = self.config,
80 | **self.child_config
81 | )
82 | # initial state will always be wrong since it will see *our* state
83 | self.child_spawner.clear_state()
84 | if self.child_state:
85 | self.child_spawner.load_state(self.child_state)
86 |
87 | # link traits common between self and child
88 | common_traits = (
89 | set(self.trait_names()) &
90 | set(self.child_spawner.trait_names()) -
91 | set(self.child_config.keys())
92 | )
93 | for trait in common_traits:
94 | directional_link((self, trait), (self.child_spawner, trait))
95 | return self.child_spawner
96 |
97 | def load_child_class(self, state):
98 | # Subclasses must arrange for correct child_class setting from load_state
99 | pass
100 |
101 | def load_state(self, state):
102 | super().load_state(state)
103 | self.load_child_class(state)
104 | self.child_config.update(state.get('child_conf', {}))
105 | self.child_state = state.get('child_state', {})
106 | self.construct_child()
107 |
108 | def get_state(self):
109 | state = super().get_state()
110 | state['child_conf'] = self.child_config
111 | if self.child_spawner:
112 | self.child_state = state['child_state'] = self.child_spawner.get_state()
113 | return state
114 |
115 | def clear_state(self):
116 | super().clear_state()
117 | if self.child_spawner:
118 | self.child_spawner.clear_state()
119 | self.child_state = {}
120 | self.child_config = {}
121 | self.child_spawner = None
122 |
123 | # proxy functions for start/poll/stop
124 | # pass back the child's Future, or create a dummy if needed
125 |
126 | def start(self):
127 | if not self.child_spawner:
128 | self.construct_child()
129 | return self.child_spawner.start()
130 |
131 | def stop(self, now=False):
132 | if self.child_spawner:
133 | return self.child_spawner.stop(now)
134 | else:
135 | return _yield_val()
136 |
137 | def poll(self):
138 | if self.child_spawner:
139 | return self.child_spawner.poll()
140 | else:
141 | return _yield_val(1)
142 |
143 | if hasattr(Spawner, 'progress'):
144 | @property
145 | def progress(self):
146 | if self.child_spawner:
147 | return self.child_spawner.progress
148 | else:
149 | raise RuntimeError("No child spawner yet exists - can not get progress yet")
150 |
151 |
152 | class ProfilesSpawner(WrapSpawner):
153 |
154 | """ProfilesSpawner - leverages the Spawner options form feature to allow user-driven
155 | configuration of Spawner classes while permitting:
156 | 1) configuration of Spawner classes that don't natively implement options_form
157 | 2) administrator control of allowed configuration changes
158 | 3) runtime choice of which Spawner backend to launch
159 | """
160 |
161 | profiles = List(
162 | trait = Tuple( Unicode(), Unicode(), Type(Spawner), Dict() ),
163 | default_value = [ ( 'Local Notebook Server', 'local', LocalProcessSpawner,
164 | {'start_timeout': 15, 'http_timeout': 10} ) ],
165 | minlen = 1,
166 | config = True,
167 | help = """List of profiles to offer for selection. Signature is:
168 | List(Tuple( Unicode, Unicode, Type(Spawner), Dict )) corresponding to
169 | profile display name, unique key, Spawner class, dictionary of spawner config options.
170 |
171 | The first three values will be exposed in the input_template as {display}, {key}, and {type}"""
172 | )
173 |
174 | @validate("profiles")
175 | def _validate_profiles(self, proposal):
176 | profiles = proposal.value
177 |
178 | seen = set()
179 | duplicated = {p[1] for p in profiles if p[1] in seen or seen.add(p[1])}
180 | if len(duplicated):
181 | raise TraitError(
182 | f"Invalid wrapspawner profiles, profiles keys are not unique : {duplicated}")
183 |
184 | return profiles
185 |
186 | child_profile = Unicode()
187 |
188 | form_template = Unicode(
189 | """
190 |
193 | """,
194 | config = True,
195 | help = """Template to use to construct options_form text. {input_template} is replaced with
196 | the result of formatting input_template against each item in the profiles list."""
197 | )
198 |
199 | first_template = Unicode('selected',
200 | config=True,
201 | help="Text to substitute as {first} in input_template"
202 | )
203 |
204 | input_template = Unicode("""
205 | """,
206 | config = True,
207 | help = """Template to construct {input_template} in form_template. This text will be formatted
208 | against each item in the profiles list, in order, using the following key names:
209 | ( display, key, type ) for the first three items in the tuple, and additionally
210 | first = "checked" (taken from first_template) for the first item in the list, so that
211 | the first item starts selected."""
212 | )
213 |
214 | def _options_form_default(self):
215 | temp_keys = [ dict(display=p[0], key=p[1], type=p[2], first='') for p in self.profiles ]
216 | temp_keys[0]['first'] = self.first_template
217 | text = ''.join([ self.input_template.format(**tk) for tk in temp_keys ])
218 | return self.form_template.format(input_template=text)
219 |
220 | def options_from_form(self, formdata):
221 | # Default to first profile if somehow none is provided
222 | return dict(profile=formdata.get('profile', [self.profiles[0][1]])[0])
223 |
224 | # load/get/clear : save/restore child_profile (and on load, use it to update child class/config)
225 |
226 | def select_profile(self, profile):
227 | # Select matching profile, or do nothing (leaving previous or default config in place)
228 | for p in self.profiles:
229 | if p[1] == profile:
230 | self.child_class = p[2]
231 | self.child_config = p[3]
232 | break
233 |
234 | def construct_child(self):
235 | self.child_profile = self.user_options.get('profile', "")
236 | self.select_profile(self.child_profile)
237 | super().construct_child()
238 |
239 | def load_child_class(self, state):
240 | try:
241 | self.child_profile = state['profile']
242 | except KeyError:
243 | self.child_profile = ''
244 | self.select_profile(self.child_profile)
245 |
246 | def get_state(self):
247 | state = super().get_state()
248 | state['profile'] = self.child_profile
249 | return state
250 |
251 | def clear_state(self):
252 | super().clear_state()
253 | self.child_profile = ''
254 |
255 | class DockerProfilesSpawner(ProfilesSpawner):
256 |
257 | """DockerProfilesSpawner - leverages ProfilesSpawner to dynamically create DockerSpawner
258 | profiles dynamically by looking for docker images that end with "jupyterhub". Due to the
259 | profiles being dynamic the "profiles" config item from the ProfilesSpawner is renamed as
260 | "default_profiles". Please note that the "docker" and DockerSpawner packages are required
261 | for this spawner to work.
262 | """
263 |
264 | default_profiles = List(
265 | trait = Tuple( Unicode(), Unicode(), Type(Spawner), Dict() ),
266 | default_value = [],
267 | config = True,
268 | help = """List of profiles to offer in addition to docker images for selection. Signature is:
269 | List(Tuple( Unicode, Unicode, Type(Spawner), Dict )) corresponding to
270 | profile display name, unique key, Spawner class, dictionary of spawner config options.
271 |
272 | The first three values will be exposed in the input_template as {display}, {key}, and {type}"""
273 | )
274 |
275 | docker_spawner_args = Dict(
276 | default_value = {},
277 | config = True,
278 | help = "Args to pass to DockerSpawner."
279 | )
280 |
281 | jupyterhub_docker_tag_re = re.compile('^.*jupyterhub$')
282 |
283 | def _nvidia_args(self):
284 | try:
285 | resp = urllib.request.urlopen('http://localhost:3476/v1.0/docker/cli/json')
286 | body = resp.read().decode('utf-8')
287 | args = json.loads(body)
288 | return dict(
289 | read_only_volumes={vol.split(':')[0]: vol.split(':')[1] for vol in args['Volumes']},
290 | extra_create_kwargs={"volume_driver": args['VolumeDriver']},
291 | extra_host_config={"devices": args['Devices']},
292 | )
293 | except urllib.error.URLError:
294 | return {}
295 |
296 |
297 | def _docker_profile(self, nvidia_args, image):
298 | spawner_args = dict(container_image=image, network_name=self.user.name)
299 | spawner_args.update(self.docker_spawner_args)
300 | spawner_args.update(nvidia_args)
301 | nvidia_enabled = "w/GPU" if len(nvidia_args) > 0 else "no GPU"
302 | return ("Docker: (%s): %s"%(nvidia_enabled, image), "docker-%s"%(image), "dockerspawner.SystemUserSpawner", spawner_args)
303 |
304 | def _jupyterhub_docker_tags(self):
305 | try:
306 | include_jh_tags = lambda tag: self.jupyterhub_docker_tag_re.match(tag)
307 | return filter(include_jh_tags, [tag for image in docker.from_env().images.list() for tag in image.tags])
308 | except NameError:
309 | raise Exception('The docker package is not installed and is a dependency for DockerProfilesSpawner')
310 |
311 | def _docker_profiles(self):
312 | return [self._docker_profile(self._nvidia_args(), tag) for tag in self._jupyterhub_docker_tags()]
313 |
314 | @property
315 | def profiles(self):
316 | return self.default_profiles + self._docker_profiles()
317 |
318 | @property
319 | def options_form(self):
320 | temp_keys = [ dict(display=p[0], key=p[1], type=p[2], first='') for p in self.profiles]
321 | temp_keys[0]['first'] = self.first_template
322 | text = ''.join([ self.input_template.format(**tk) for tk in temp_keys ])
323 | return self.form_template.format(input_template=text)
324 |
325 |
326 | # vim: set ai expandtab softtabstop=4:
327 |
328 |
--------------------------------------------------------------------------------