allow spawners and authenticators to register via entrypoints

jupyterhub.authenticators for authenticators, jupyterhub.spawners for spawners

This has the effect that authenticators and spawners can be selected by name instead of full import string (e.g. 'github' or 'dummy' or 'kubernetes')
and, perhaps more importantly, the autogenerated configuration file will include a section for each installed and registered class.
This commit is contained in:
Min RK
2018-09-28 10:08:10 +02:00
parent 5c3530cc7f
commit c02ab23b3d
6 changed files with 164 additions and 20 deletions

View File

@@ -68,7 +68,6 @@ Writing an Authenticator that looks up passwords in a dictionary
requires only overriding this one method:
```python
from tornado import gen
from IPython.utils.traitlets import Dict
from jupyterhub.auth import Authenticator
@@ -78,8 +77,7 @@ class DictionaryAuthenticator(Authenticator):
help="""dict of username:password for authentication"""
)
@gen.coroutine
def authenticate(self, handler, data):
async def authenticate(self, handler, data):
if self.passwords.get(data['username']) == data['password']:
return data['username']
```
@@ -136,6 +134,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
[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

View File

@@ -10,6 +10,7 @@ and a custom Spawner needs to be able to take three actions:
## Examples
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
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).
### 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)
Some spawners of the single-user notebook servers allow setting limits or

View File

@@ -42,7 +42,7 @@ from traitlets import (
Tuple, Type, Set, Instance, Bytes, Float,
observe, default,
)
from traitlets.config import Application, catch_config_error
from traitlets.config import Application, Configurable, catch_config_error
here = os.path.dirname(__file__)
@@ -58,7 +58,7 @@ from .oauth.provider import make_provider
from ._data import DATA_FILES_PATH
from .log import CoroutineLogFormatter, log_request
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command
from .traitlets import URLPrefix, Command, EntryPointType
from .utils import (
maybe_future,
url_path_join,
@@ -227,13 +227,19 @@ class JupyterHub(Application):
'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."),
}
classes = List([
Spawner,
LocalProcessSpawner,
Authenticator,
PAMAuthenticator,
CryptKeeper,
])
classes = List()
@default('classes')
def _load_classes(self):
classes = [Spawner, Authenticator, CryptKeeper]
for name, trait in self.traits(config=True).items():
# 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()),
help="""Dict of 'group': ['usernames'] to load at startup.
@@ -651,20 +657,25 @@ class JupyterHub(Application):
).tag(config=True)
_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.
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 authenticate method that:
with an :meth:`authenticate` method that:
- is a coroutine (asyncio or tornado)
- returns username on success, None on failure
- takes two arguments: (handler, data),
where `handler` is the calling web.RequestHandler,
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)
@@ -679,10 +690,17 @@ class JupyterHub(Application):
).tag(config=True)
# 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.
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)

View File

@@ -4,7 +4,8 @@ Traitlets that are used in JupyterHub
# Copyright (c) Jupyter Development Team.
# 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):
@@ -91,3 +92,46 @@ class Callable(TraitType):
return value
else:
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)

View File

@@ -1,5 +1,6 @@
alembic
async_generator>=1.8
entrypoints
traitlets>=4.3.2
tornado>=5.0
jinja2

View File

@@ -106,6 +106,17 @@ setup_args = dict(
platforms = "Linux, Mac OS X",
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
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 = [
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',