mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 07:23:00 +00:00
Merge pull request #2203 from minrk/entrypoints
allow spawners and authenticators to register via entry points
This commit is contained in:
@@ -75,7 +75,6 @@ Writing an Authenticator that looks up passwords in a dictionary
|
|||||||
requires only overriding this one method:
|
requires only overriding this one method:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from tornado import gen
|
|
||||||
from IPython.utils.traitlets import Dict
|
from IPython.utils.traitlets import Dict
|
||||||
from jupyterhub.auth import Authenticator
|
from jupyterhub.auth import Authenticator
|
||||||
|
|
||||||
@@ -85,8 +84,7 @@ class DictionaryAuthenticator(Authenticator):
|
|||||||
help="""dict of username:password for authentication"""
|
help="""dict of username:password for authentication"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@gen.coroutine
|
async def authenticate(self, handler, data):
|
||||||
def authenticate(self, handler, data):
|
|
||||||
if self.passwords.get(data['username']) == data['password']:
|
if self.passwords.get(data['username']) == data['password']:
|
||||||
return data['username']
|
return data['username']
|
||||||
```
|
```
|
||||||
@@ -143,6 +141,41 @@ See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/
|
|||||||
If you are interested in writing a custom authenticator, you can read
|
If you are interested in writing a custom authenticator, you can read
|
||||||
[this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
|
[this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
|
||||||
|
|
||||||
|
### Registering custom Authenticators via entry points
|
||||||
|
|
||||||
|
As of JupyterHub 1.0, custom authenticators can register themselves via
|
||||||
|
the `jupyterhub.authenticators` entry point metadata.
|
||||||
|
To do this, in your `setup.py` add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
setup(
|
||||||
|
...
|
||||||
|
entry_points={
|
||||||
|
'jupyterhub.authenticators': [
|
||||||
|
'myservice = mypackage:MyAuthenticator',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have added this metadata to your package,
|
||||||
|
users can select your authenticator with the configuration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.authenticator_class = 'myservice'
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of the full
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.authenticator_class = 'mypackage:MyAuthenticator'
|
||||||
|
```
|
||||||
|
|
||||||
|
previously required.
|
||||||
|
Additionally, configurable attributes for your spawner will
|
||||||
|
appear in jupyterhub help output and auto-generated configuration files
|
||||||
|
via `jupyterhub --generate-config`.
|
||||||
|
|
||||||
|
|
||||||
### Authentication state
|
### Authentication state
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ and a custom Spawner needs to be able to take three actions:
|
|||||||
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
|
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
|
||||||
Some examples include:
|
Some examples include:
|
||||||
|
|
||||||
@@ -174,6 +175,42 @@ When `Spawner.start` is called, this dictionary is accessible as `self.user_opti
|
|||||||
|
|
||||||
If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html).
|
If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html).
|
||||||
|
|
||||||
|
### Registering custom Spawners via entry points
|
||||||
|
|
||||||
|
As of JupyterHub 1.0, custom Spawners can register themselves via
|
||||||
|
the `jupyterhub.spawners` entry point metadata.
|
||||||
|
To do this, in your `setup.py` add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
setup(
|
||||||
|
...
|
||||||
|
entry_points={
|
||||||
|
'jupyterhub.spawners': [
|
||||||
|
'myservice = mypackage:MySpawner',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have added this metadata to your package,
|
||||||
|
users can select your authenticator with the configuration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.spawner_class = 'myservice'
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of the full
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.spawner_class = 'mypackage:MySpawner'
|
||||||
|
```
|
||||||
|
|
||||||
|
previously required.
|
||||||
|
Additionally, configurable attributes for your spawner will
|
||||||
|
appear in jupyterhub help output and auto-generated configuration files
|
||||||
|
via `jupyterhub --generate-config`.
|
||||||
|
|
||||||
|
|
||||||
## Spawners, resource limits, and guarantees (Optional)
|
## Spawners, resource limits, and guarantees (Optional)
|
||||||
|
|
||||||
Some spawners of the single-user notebook servers allow setting limits or
|
Some spawners of the single-user notebook servers allow setting limits or
|
||||||
|
@@ -42,7 +42,7 @@ from traitlets import (
|
|||||||
Tuple, Type, Set, Instance, Bytes, Float,
|
Tuple, Type, Set, Instance, Bytes, Float,
|
||||||
observe, default,
|
observe, default,
|
||||||
)
|
)
|
||||||
from traitlets.config import Application, catch_config_error
|
from traitlets.config import Application, Configurable, catch_config_error
|
||||||
|
|
||||||
here = os.path.dirname(__file__)
|
here = os.path.dirname(__file__)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ from .oauth.provider import make_provider
|
|||||||
from ._data import DATA_FILES_PATH
|
from ._data import DATA_FILES_PATH
|
||||||
from .log import CoroutineLogFormatter, log_request
|
from .log import CoroutineLogFormatter, log_request
|
||||||
from .proxy import Proxy, ConfigurableHTTPProxy
|
from .proxy import Proxy, ConfigurableHTTPProxy
|
||||||
from .traitlets import URLPrefix, Command
|
from .traitlets import URLPrefix, Command, EntryPointType
|
||||||
from .utils import (
|
from .utils import (
|
||||||
maybe_future,
|
maybe_future,
|
||||||
url_path_join,
|
url_path_join,
|
||||||
@@ -227,13 +227,19 @@ class JupyterHub(Application):
|
|||||||
'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."),
|
'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."),
|
||||||
}
|
}
|
||||||
|
|
||||||
classes = List([
|
classes = List()
|
||||||
Spawner,
|
@default('classes')
|
||||||
LocalProcessSpawner,
|
def _load_classes(self):
|
||||||
Authenticator,
|
classes = [Spawner, Authenticator, CryptKeeper]
|
||||||
PAMAuthenticator,
|
for name, trait in self.traits(config=True).items():
|
||||||
CryptKeeper,
|
# load entry point groups into configurable class list
|
||||||
])
|
# so that they show up in config files, etc.
|
||||||
|
if isinstance(trait, EntryPointType):
|
||||||
|
for key, entry_point in trait.load_entry_points().items():
|
||||||
|
cls = entry_point.load()
|
||||||
|
if cls not in classes and isinstance(cls, Configurable):
|
||||||
|
classes.append(cls)
|
||||||
|
return classes
|
||||||
|
|
||||||
load_groups = Dict(List(Unicode()),
|
load_groups = Dict(List(Unicode()),
|
||||||
help="""Dict of 'group': ['usernames'] to load at startup.
|
help="""Dict of 'group': ['usernames'] to load at startup.
|
||||||
@@ -651,20 +657,25 @@ class JupyterHub(Application):
|
|||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
_service_map = Dict()
|
_service_map = Dict()
|
||||||
|
|
||||||
authenticator_class = Type(PAMAuthenticator, Authenticator,
|
authenticator_class = EntryPointType(
|
||||||
|
default_value=PAMAuthenticator,
|
||||||
|
klass=Authenticator,
|
||||||
|
entry_point_group="jupyterhub.authenticators",
|
||||||
help="""Class for authenticating users.
|
help="""Class for authenticating users.
|
||||||
|
|
||||||
This should be a class with the following form:
|
This should be a subclass of :class:`jupyterhub.auth.Authenticator`
|
||||||
|
|
||||||
- constructor takes one kwarg: `config`, the IPython config object.
|
with an :meth:`authenticate` method that:
|
||||||
|
|
||||||
with an authenticate method that:
|
|
||||||
|
|
||||||
- is a coroutine (asyncio or tornado)
|
- is a coroutine (asyncio or tornado)
|
||||||
- returns username on success, None on failure
|
- returns username on success, None on failure
|
||||||
- takes two arguments: (handler, data),
|
- takes two arguments: (handler, data),
|
||||||
where `handler` is the calling web.RequestHandler,
|
where `handler` is the calling web.RequestHandler,
|
||||||
and `data` is the POST form data from the login page.
|
and `data` is the POST form data from the login page.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0
|
||||||
|
authenticators may be registered via entry points,
|
||||||
|
e.g. `c.JupyterHub.authenticator_class = 'pam'`
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@@ -679,10 +690,17 @@ class JupyterHub(Application):
|
|||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
# class for spawning single-user servers
|
# class for spawning single-user servers
|
||||||
spawner_class = Type(LocalProcessSpawner, Spawner,
|
spawner_class = EntryPointType(
|
||||||
|
default_value=LocalProcessSpawner,
|
||||||
|
klass=Spawner,
|
||||||
|
entry_point_group="jupyterhub.spawners",
|
||||||
help="""The class to use for spawning single-user servers.
|
help="""The class to use for spawning single-user servers.
|
||||||
|
|
||||||
Should be a subclass of Spawner.
|
Should be a subclass of :class:`jupyterhub.spawner.Spawner`.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0
|
||||||
|
spawners may be registered via entry points,
|
||||||
|
e.g. `c.JupyterHub.spawner_class = 'localprocess'`
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
@@ -4,7 +4,8 @@ Traitlets that are used in JupyterHub
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
from traitlets import List, Unicode, Integer, TraitType, TraitError
|
import entrypoints
|
||||||
|
from traitlets import List, Unicode, Integer, Type, TraitType, TraitError
|
||||||
|
|
||||||
|
|
||||||
class URLPrefix(Unicode):
|
class URLPrefix(Unicode):
|
||||||
@@ -91,3 +92,46 @@ class Callable(TraitType):
|
|||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
self.error(obj, value)
|
self.error(obj, value)
|
||||||
|
|
||||||
|
|
||||||
|
class EntryPointType(Type):
|
||||||
|
"""Entry point-extended Type
|
||||||
|
|
||||||
|
classes can be registered via entry points
|
||||||
|
in addition to standard 'mypackage.MyClass' strings
|
||||||
|
"""
|
||||||
|
|
||||||
|
_original_help = ''
|
||||||
|
|
||||||
|
def __init__(self, *args, entry_point_group, **kwargs):
|
||||||
|
self.entry_point_group = entry_point_group
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help(self):
|
||||||
|
"""Extend help by listing currently installed choices"""
|
||||||
|
chunks = [self._original_help]
|
||||||
|
chunks.append("Currently installed: ")
|
||||||
|
for key, entry_point in self.load_entry_points().items():
|
||||||
|
chunks.append(" - {}: {}.{}".format(key, entry_point.module_name, entry_point.object_name))
|
||||||
|
return '\n'.join(chunks)
|
||||||
|
|
||||||
|
@help.setter
|
||||||
|
def help(self, value):
|
||||||
|
self._original_help = value
|
||||||
|
|
||||||
|
def load_entry_points(self):
|
||||||
|
"""Load my entry point group"""
|
||||||
|
# load the group
|
||||||
|
group = entrypoints.get_group_named(self.entry_point_group)
|
||||||
|
# make it case-insensitive
|
||||||
|
return {key.lower(): value for key, value in group.items()}
|
||||||
|
|
||||||
|
def validate(self, obj, value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
# first, look up in entry point registry
|
||||||
|
registry = self.load_entry_points()
|
||||||
|
key = value.lower()
|
||||||
|
if key in registry:
|
||||||
|
value = registry[key].load()
|
||||||
|
return super().validate(obj, value)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
alembic
|
alembic
|
||||||
async_generator>=1.8
|
async_generator>=1.8
|
||||||
|
entrypoints
|
||||||
traitlets>=4.3.2
|
traitlets>=4.3.2
|
||||||
tornado>=5.0
|
tornado>=5.0
|
||||||
jinja2
|
jinja2
|
||||||
|
11
setup.py
11
setup.py
@@ -106,6 +106,17 @@ setup_args = dict(
|
|||||||
platforms = "Linux, Mac OS X",
|
platforms = "Linux, Mac OS X",
|
||||||
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
|
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
|
||||||
python_requires = ">=3.5",
|
python_requires = ">=3.5",
|
||||||
|
entry_points = {
|
||||||
|
'jupyterhub.authenticators': [
|
||||||
|
'default = jupyterhub.auth:PAMAuthenticator',
|
||||||
|
'pam = jupyterhub.auth:PAMAuthenticator',
|
||||||
|
'dummy = jupyterhub.auth:DummyAuthenticator',
|
||||||
|
],
|
||||||
|
'jupyterhub.spawners': [
|
||||||
|
'default = jupyterhub.spawner:LocalProcessSpawner',
|
||||||
|
'localprocess = jupyterhub.spawner:LocalProcessSpawner',
|
||||||
|
],
|
||||||
|
},
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'Intended Audience :: System Administrators',
|
'Intended Audience :: System Administrators',
|
||||||
|
Reference in New Issue
Block a user