mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
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:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
alembic
|
||||
async_generator>=1.8
|
||||
entrypoints
|
||||
traitlets>=4.3.2
|
||||
tornado>=5.0
|
||||
jinja2
|
||||
|
11
setup.py
11
setup.py
@@ -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',
|
||||
|
Reference in New Issue
Block a user