Skip to content

Commit

Permalink
Add Python API documentation for plugins
Browse files Browse the repository at this point in the history
Add Python API documentation in the dev section for the JupyterHub
plugins. Do a proofreading pass on that rendered documentation and
clean up some cross-references.

Unfortunately, there doesn't seem to be a way to reference one
package installed by Nublado from another package installed by
Nublado in a way that Sphinx understands well enough to generate
cross-references, so the mention of GafaelfawrAuthenticator in
the NubladoSpawner documentation is not a link.
  • Loading branch information
rra committed Dec 1, 2023
1 parent a2501e8 commit 8a7d099
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ instance/
# Sphinx documentation
docs/_build/
docs/_static/openapi.json
docs/dev/api/contents/

# PyBuilder
target/
Expand Down
63 changes: 61 additions & 2 deletions authenticator/src/rubin/nublado/authenticator/_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
AuthInfo = dict[str, str | dict[str, str]]
Route = tuple[str, type[BaseHandler]]

__all__ = ["GafaelfawrAuthenticator"]


def _build_auth_info(headers: HTTPHeaders) -> AuthInfo:
"""Construct the authentication information for a user.
Expand Down Expand Up @@ -144,11 +146,34 @@ async def authenticate(
"""Login form authenticator.
This is not used in our authentication scheme.
Parameters
----------
handler
Tornado request handler.
data
Form data submitted during login.
Raises
------
NotImplementedError
Raised if called.
"""
raise NotImplementedError

def get_handlers(self, app: JupyterHub) -> list[Route]:
"""Register the header-only login and the logout handlers."""
"""Register the header-only login and the logout handlers.
Parameters
----------
app
Tornado app in which to register the handlers.
Returns
-------
list of tuple
Additional routes to add.
"""
return [
("/gafaelfawr/login", _GafaelfawrLoginHandler),
("/logout", _GafaelfawrLogoutHandler),
Expand All @@ -160,13 +185,47 @@ def login_url(self, base_url: str) -> str:
This must be changed to something other than ``/login`` to trigger
correct behavior when ``auto_login`` is set to true (as it is in our
case).
Parameters
----------
base_url
Base URL of this JupyterHub installation.
Returns
-------
str
URL to which the user is sent during login. For this
authenticator, this is a URL provided by a login handler that
looks at headers set by Gafaelfawr_.
"""
return url_path_join(base_url, "gafaelfawr/login")

async def refresh_user(
self, user: User, handler: RequestHandler | None = None
) -> bool | AuthInfo:
"""Optionally refresh the user's token."""
"""Optionally refresh the user's token.
Parameters
----------
user
JupyterHub user information.
handler
Tornado request handler.
Returns
-------
bool or dict
Returns `True` if we cannot refresh the auth state and should
use the existing state. Otherwise, returns the new auth state
taken from the request headers set by Gafaelfawr_.
Raises
------
tornado.web.HTTPError
Raised with a 401 error if the username does not match our current
auth state, since JupyterHub does not support changing users
during refresh.
"""
# If running outside of a Tornado handler, we can't refresh the auth
# state, so assume that it is okay.
if not handler:
Expand Down
10 changes: 10 additions & 0 deletions docs/dev/api/authenticator.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
##########################
Internal authenticator API
##########################

The module ``rubin.nublado.authenticator`` provides an implementation of the `JupyterHub Authenticator API <https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html>`__ that uses Gafaelfawr_ to authenticate users.

This authenticator class is registered as ``gafaelfawr`` in the ``jupyterhub.authenticators`` entry point.

.. automodapi:: rubin.nublado.authenticator
:include-all-objects:
12 changes: 12 additions & 0 deletions docs/dev/api/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
####################
Python internal APIs
####################

Nublado provides a :doc:`REST API </api>` for external users and does not provide Python libraries intended for use outside of Nublado.
The Python API is therefore only of interest to Nublado developers.

.. toctree::
:maxdepth: 2

authenticator
spawner
10 changes: 10 additions & 0 deletions docs/dev/api/spawner.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
####################
Internal spawner API
####################

The module ``rubin.nublado.spawner`` provides an implementation of the `JupyterHub Spawner API <https://jupyterhub.readthedocs.io/en/stable/reference/spawners.html>`__ that uses the Nublado controller to manage user labs.

This authenticator class is registered as ``nublado`` in the ``jupyterhub.spawners`` entry point.

.. automodapi:: rubin.nublado.spawner
:include-all-objects:
5 changes: 5 additions & 0 deletions docs/dev/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ The repository uses the vertical monorepo structure defined in :sqr:`075`.
:caption: Development

plan

.. toctree::
:caption: Reference

api/index
8 changes: 8 additions & 0 deletions docs/documenteer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ nitpick_ignore = [
["py:exc", "fastapi.exceptions.RequestValidationError"],
["py:exc", "httpx.HTTPError"],
["py:obj", "fastapi.routing.APIRoute"],
["py:obj", "httpx.AsyncClient"],
["py:class", "kubernetes_asyncio.client.api_client.ApiClient"],
["py:class", "pydantic.env_settings.BaseSettings"],
["py:class", "pydantic.error_wrappers.ValidationError"],
Expand All @@ -47,16 +48,23 @@ nitpick_ignore = [
["py:class", "starlette.routing.Route"],
["py:class", "starlette.routing.BaseRoute"],
["py:exc", "starlette.exceptions.HTTPException"],
# traitlets does provide intersphinx, but the documentation generates some
# reference to this undocumented type.
["py:class", "traitlets.traitlets.HasDescriptors"],
]
nitpick_ignore_regex = [
["py:class", "kubernetes_asyncio\\.client\\.models\\..*"],
]
rst_epilog_file = "_rst_epilog.rst"
python_api_dir = "dev/api/contents"

[sphinx.intersphinx.projects]
jupyerhub = "https://jupyterhub.readthedocs.io/en/stable/"
python = "https://docs.python.org/3/"
safir = "https://safir.lsst.io/"
structlog = "https://www.structlog.org/en/stable/"
tornado = "https://www.tornadoweb.org/en/stable/"
traitlets = "https://traitlets.readthedocs.io/en/stable/"

[sphinx.linkcheck]
ignore = [
Expand Down
9 changes: 8 additions & 1 deletion spawner/src/rubin/nublado/spawner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""JupyterHub spawner that uses the Nublado controller to manage labs."""

from ._exceptions import ControllerWebError, InvalidAuthStateError
from ._internals import NubladoSpawner

__all__ = ["NubladoSpawner"]
__all__ = [
"ControllerWebError",
"InvalidAuthStateError",
"NubladoSpawner",
]
8 changes: 4 additions & 4 deletions spawner/src/rubin/nublado/spawner/_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ async def options_form(self, spawner: Spawner) -> str:
InvalidAuthStateError
Raised if there is no ``token`` attribute in the user's
authentication state. This should always be provided by
`~rsp_restspawner.auth.GafaelfawrAuthenticator`.
`rubin.nublado.authenticator.GafaelfawrAuthenticator`.
"""
r = await self._client.get(
self._controller_url("lab-form", self.user.name),
Expand Down Expand Up @@ -257,7 +257,7 @@ async def progress(self) -> AsyncIterator[dict[str, int | str]]:
Yields
------
dict of str to str or int
dict
Dictionary representing the event with fields ``progress``,
containing an integer completion percentage, and ``message``,
containing a human-readable description of the event.
Expand Down Expand Up @@ -351,11 +351,11 @@ def start(self) -> asyncio.Task[str]:
returned, JupyterHub only allows a much shorter timeout for the lab to
fully start.
In addition, JupyterHub handles exceptions from `start` and correctly
Also, JupyterHub handles exceptions from `start` and correctly
recognizes that the pod has failed to start, but exceptions from
`progress` are treated as uncaught exceptions and cause the UI to
break. Therefore, `progress` must never fail and all operations that
may fail need to be done in `start`.
may fail must be done in `start`.
"""
self._start_future = asyncio.create_task(self._start())
return self._start_future
Expand Down

0 comments on commit 8a7d099

Please sign in to comment.