Merge pull request #2203 from minrk/entrypoints

allow spawners and authenticators to register via entry points
This commit is contained in:
Min RK
2018-10-01 17:22:11 +02:00
committed by GitHub
6 changed files with 164 additions and 20 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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',