mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-05 07:41:37 -05:00
Add support for the JupyterHub REST API
This commit is contained in:
parent
702186720b
commit
b5213de775
1 changed files with 359 additions and 0 deletions
359
jupyter-hub-api.el
Normal file
359
jupyter-hub-api.el
Normal file
|
@ -0,0 +1,359 @@
|
|||
;;; jupyter-hub-api.el --- Jupyter Hub REST API -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 21 Jun 2019
|
||||
;; Version: 0.8.1
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Routines for working with the Jupyter Hub REST API. Supports JupyterHub v1.0
|
||||
;; and above.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-server)
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
;; TODO: Set this as a client local variable as well using `jupyter-set' if
|
||||
;; needed.
|
||||
(defvar-local jupyter-hub-current-user nil
|
||||
"The current logged in JupyterHub user.")
|
||||
(put 'jupyter-hub-current-user 'permanent-local t)
|
||||
|
||||
(defclass jupyter-hub-client (jupyter-rest-client)
|
||||
((auth :initform 'token)
|
||||
(authenticator
|
||||
:initform 'jupyter-hub-pam-authenticator
|
||||
:documentation "Authentication method for logging in users.
|
||||
Must be a function that takes three arguments, (HUB USER),
|
||||
where USER is the username of the user to login on HUB.")))
|
||||
|
||||
;; TODO: The Hub should be logged in differently than this
|
||||
|
||||
;; NOTE: Going through the login page installs a login cookie for the user
|
||||
;; whose credentials were passed to the HUB. The cookie is the means of
|
||||
;; authentication and not the token.
|
||||
;;
|
||||
;; The PAM authenticator is more of a user authenticator rather than a HUB
|
||||
;; authenticator. The HUB API is mainly accessible through a token. Logging in
|
||||
;; mainly stores a login cookie and has the HUB handle the token stuff via the
|
||||
;; login cookie.
|
||||
|
||||
;; FIXME: Have a better way to handle this case
|
||||
(defvar jupyter-hub--current-user-logging-in nil)
|
||||
|
||||
(cl-defmethod jupyter-api-server-accessible-p ((hub jupyter-hub-client))
|
||||
(if jupyter-hub--current-user-logging-in
|
||||
(and (jupyter-hub-service-available-p
|
||||
hub "user" jupyter-hub--current-user-logging-in)
|
||||
(jupyter-hub-user-logged-in-p hub jupyter-hub--current-user-logging-in))
|
||||
(let ((url (if (url-p (oref hub url)) (oref hub url)
|
||||
(url-generic-parse-url (oref hub url)))))
|
||||
(and (jupyter-api-auth-headers hub)
|
||||
(cl-loop
|
||||
for cookie in (url-cookie-retrieve (url-host url) "/hub/")
|
||||
thereis (equal (url-cookie-name cookie) "jupyterhub-hub-login"))))))
|
||||
|
||||
(defun jupyter-hub-pam-authenticator (hub user)
|
||||
"Authenticate USER using PAM authentication via HUB.
|
||||
A username and password will be asked for.
|
||||
|
||||
Raise an error on failure."
|
||||
(let ((jupyter-hub--current-user-logging-in user))
|
||||
(jupyter-api-authenticate hub
|
||||
(lambda (hub _try)
|
||||
;; TODO: Handle multiple users and :save-function
|
||||
(let ((matches (jupyter-api-auth-source-search hub user)) auth)
|
||||
(setq auth (if (= (length matches) 1) (car matches)
|
||||
(error "TODO")))
|
||||
(cond
|
||||
(auth
|
||||
(let* ((user (plist-get auth :user))
|
||||
(pass (if (functionp (plist-get auth :secret))
|
||||
(funcall (plist-get auth :secret))
|
||||
(plist-get auth :secret)))
|
||||
(url-request-method "POST")
|
||||
(url-request-extra-headers
|
||||
(append (list (cons "Content-Type" "application/x-www-form-urlencoded"))
|
||||
url-request-extra-headers))
|
||||
(url-request-data
|
||||
(concat "password=" pass "&"
|
||||
"username=" user)))
|
||||
(condition-case err
|
||||
(jupyter-api-login hub)
|
||||
(error
|
||||
(when (eq (nth 2 err) 'connection-failed)
|
||||
(signal (car err) (cdr err)))))))
|
||||
(t
|
||||
(error "Can't authenticate"))))))))
|
||||
|
||||
;;; Endpoints
|
||||
|
||||
(cl-defgeneric jupyter-hub/api/authorizations ((client jupyter-hub-client) method &rest plist)
|
||||
(declare (indent 2)))
|
||||
|
||||
(cl-defmethod jupyter-hub/api/authorizations ((client jupyter-hub-client) method &rest plist)
|
||||
(apply #'jupyter-api-request client method "hub" "api" "authorizations" plist))
|
||||
|
||||
(cl-defgeneric jupyter-hub/api/users ((client jupyter-hub-client) method &rest plist)
|
||||
(declare (indent 2)))
|
||||
|
||||
(cl-defmethod jupyter-hub/api/users ((client jupyter-hub-client) method &rest plist)
|
||||
(apply #'jupyter-api-request client method "hub" "api" "users" plist))
|
||||
|
||||
(cl-defgeneric jupyter-hub/api/groups ((client jupyter-hub-client) method &rest plist)
|
||||
(declare (indent 2)))
|
||||
|
||||
(cl-defmethod jupyter-hub/api/groups ((client jupyter-hub-client) method &rest plist)
|
||||
(apply #'jupyter-api-request client method "hub" "api" "groups" plist))
|
||||
|
||||
;;; Users
|
||||
|
||||
(defun jupyter-hub-current-user (hub)
|
||||
(jupyter-api-request hub "GET" "hub" "user"))
|
||||
|
||||
(defun jupyter-hub-get-user (hub &optional user)
|
||||
(jupyter-hub/api/users hub "GET" user))
|
||||
|
||||
(defun jupyter-hub-make-user (hub user)
|
||||
(jupyter-hub/api/users hub "POST" user))
|
||||
|
||||
(defun jupyter-hub-modify-user (hub user &rest options)
|
||||
(apply #'jupyter-hub/api/users hub "PATCH" user options))
|
||||
|
||||
(defun jupyter-hub-delete-user (hub user)
|
||||
(jupyter-hub/api/users hub "DELETE" user))
|
||||
|
||||
;;;; Tokens
|
||||
|
||||
(defun jupyter-hub-get-token (hub user &optional id)
|
||||
(jupyter-hub/api/users hub "GET" user "tokens" id))
|
||||
|
||||
(cl-defun jupyter-hub-make-token (hub user &key auth expires-in note)
|
||||
(declare (indent 2))
|
||||
(apply #'jupyter-hub/api/users hub "POST" user "tokens"
|
||||
(nconc (when auth (list :auth auth))
|
||||
(when expires-in (list :expires_in expires-in))
|
||||
(when note (list :note note)))))
|
||||
|
||||
(defun jupyter-hub-delete-token (hub user id)
|
||||
(jupyter-hub/api/users hub "DELETE" user "tokens" id))
|
||||
|
||||
;;;;; `jupyter-hub-token-list-mode'
|
||||
|
||||
(defvar jupyter-hub-token-list-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "d") #'jupyter-hub-token-list-revoke)
|
||||
(define-key map (kbd "C-c C-d") #'jupyter-hub-token-list-revoke)
|
||||
(define-key map (kbd "C-<return>") #'jupyter-hub-token-list-create)
|
||||
(define-key map "r" #'revert-buffer)
|
||||
(define-key map "g" #'revert-buffer)
|
||||
map))
|
||||
|
||||
(defun jupyter-hub-token-list-revoke ()
|
||||
"Revoke the token under `point'."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id)))
|
||||
(jupyter-hub-delete-token jupyter-current-server jupyter-hub-current-user id)
|
||||
(tabulated-list-delete-entry)))
|
||||
|
||||
(defun jupyter-hub-token-list-create ()
|
||||
"Create a new Jupyter Hub API token.
|
||||
The value of the new token is added to the `kill-ring' and also
|
||||
inserted into the *Messages* buffer."
|
||||
(interactive)
|
||||
(let* ((note (read-string "Token note: "))
|
||||
(expires-in (read-number "Expires in seconds [<= 0 means never]: " 0))
|
||||
(token (jupyter-hub-make-token
|
||||
jupyter-current-server jupyter-hub-current-user
|
||||
:expires-in (unless (<= expires-in 0) expires-in)
|
||||
:note note)))
|
||||
(message (kill-new (plist-get token :token)))
|
||||
(revert-buffer)
|
||||
(goto-char (point-min))
|
||||
(while (not (equal (tabulated-list-get-id)
|
||||
(plist-get token :id)))
|
||||
(forward-line 1))))
|
||||
|
||||
(define-derived-mode jupyter-hub-token-list-mode
|
||||
tabulated-list-mode "Tokens"
|
||||
"A list of tokens for a user."
|
||||
(tabulated-list-init-header)
|
||||
(tabulated-list-print)
|
||||
(let* ((inhibit-read-only t)
|
||||
(url (oref jupyter-current-server url))
|
||||
(header (format "%s [%s]" jupyter-hub-current-user url)))
|
||||
(overlay-put
|
||||
(make-overlay 1 2)
|
||||
'before-string
|
||||
(concat (propertize header 'face '(fixed-pitch default)) "\n"))))
|
||||
|
||||
(defun jupyter-hub--token-list-format ()
|
||||
`[("Kind" 10 nil)
|
||||
("ID" 4 nil)
|
||||
("Note" 35 t)
|
||||
("Last Used" 20 t)
|
||||
("Created/First authorized" 25 nil)])
|
||||
|
||||
(defun jupyter-hub--token-list-entries (user)
|
||||
(let* ((tokens (jupyter-hub-get-token jupyter-current-server user))
|
||||
(format-time
|
||||
(lambda (time)
|
||||
(propertize
|
||||
(jupyter-format-time-low-res
|
||||
(when time (jupyter-decode-time time)))
|
||||
'face 'font-lock-doc-face)))
|
||||
(format
|
||||
(lambda (token)
|
||||
(cl-destructuring-bind (&key id kind created expires_at last_activity
|
||||
note oauth_client
|
||||
&allow-other-keys)
|
||||
token
|
||||
(setq expires_at (funcall format-time expires_at))
|
||||
(setq last_activity (funcall format-time last_activity))
|
||||
(setq created (funcall format-time created))
|
||||
(setq note (or note oauth_client))
|
||||
(list id (vector kind id note last_activity created))))))
|
||||
(cl-loop
|
||||
for token across (vconcat (plist-get tokens :api_tokens)
|
||||
(plist-get tokens :oauth_tokens))
|
||||
collect (funcall format token))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-hub-list-tokens (hub user)
|
||||
"Display a list of tokens for USER on HUB."
|
||||
(interactive
|
||||
(let ((hub (jupyter-current-server current-prefix-arg)))
|
||||
(list hub (read-string
|
||||
(format "Username [%s] (default %s): "
|
||||
(oref hub url)
|
||||
user-login-name)))))
|
||||
(with-current-buffer
|
||||
(jupyter-get-buffer-create
|
||||
(format "tokens:%s[%s]"
|
||||
user (oref hub url)))
|
||||
(setq jupyter-current-server hub)
|
||||
(setq jupyter-hub-current-user user)
|
||||
(if (eq major-mode 'jupyter-hub-token-list-mode)
|
||||
(revert-buffer)
|
||||
(setq tabulated-list-format
|
||||
(jupyter-hub--token-list-format)
|
||||
tabulated-list-entries
|
||||
(apply-partially #'jupyter-hub--token-list-entries user))
|
||||
(jupyter-hub-token-list-mode))
|
||||
(jupyter-display-current-buffer-reuse-window)))
|
||||
|
||||
;;;; Server
|
||||
|
||||
(defun jupyter-hub-start-server (hub user &rest options)
|
||||
(declare (indent 2))
|
||||
(apply #'jupyter-hub/api/users hub "POST" user "server" options))
|
||||
|
||||
(defun jupyter-hub-delete-server (hub user)
|
||||
(jupyter-hub/api/users hub "DELETE" user "server"))
|
||||
|
||||
;;; Authorizations
|
||||
|
||||
(defun jupyter-hub-user-from-cookie (client name value)
|
||||
(jupyter-hub/api/authorizations client "GET" "cookie" name value))
|
||||
|
||||
(defun jupyter-hub-user-from-token (client token)
|
||||
(jupyter-hub/api/authorizations client "GET" "token" token))
|
||||
|
||||
;;; Groups
|
||||
|
||||
(defun jupyter-hub-get-group (client &optional group)
|
||||
(jupyter-hub/api/groups client "GET" group))
|
||||
|
||||
(defun jupyter-hub-delete-group (client group)
|
||||
(jupyter-hub/api/groups client "DELETE" group))
|
||||
|
||||
(defun jupyter-hub-make-group (client group)
|
||||
(jupyter-hub/api/groups client "POST" group))
|
||||
|
||||
(defun jupyter-hub-delete-group-users (client group &rest users)
|
||||
(declare (indent 2))
|
||||
(jupyter-hub/api/groups client "DELETE" group "users" :users users))
|
||||
|
||||
(defun jupyter-hub-add-group-users (client group &rest users)
|
||||
(declare (indent 2))
|
||||
(jupyter-hub/api/groups client "POST" group "users" :users users))
|
||||
|
||||
;;; Info
|
||||
|
||||
(defun jupyter-hub-info (client)
|
||||
(jupyter-api-request client "GET" "hub" "info"))
|
||||
|
||||
;;; Client
|
||||
|
||||
;; TODO: Determine when a cookie is invalid.
|
||||
(cl-defmethod jupyter-hub-user-logged-in-p ((hub jupyter-hub-client) user)
|
||||
(and (jupyter-api-server-accessible-p hub)
|
||||
(let ((cookie (format "jupyterhub-user-%s" user))
|
||||
(localpart (format "/user/%s/" user))
|
||||
(url (url-generic-parse-url (oref hub url))))
|
||||
(url-cookie-value
|
||||
(cl-find-if (lambda (c) (equal (url-cookie-name c) cookie))
|
||||
(url-cookie-retrieve
|
||||
(url-host url) localpart
|
||||
(equal (url-type url) "https")))))
|
||||
t))
|
||||
|
||||
(cl-defmethod jupyter-hub-make-client ((hub jupyter-hub-client) user &optional class)
|
||||
(or class (setq class 'jupyter-rest-client))
|
||||
(jupyter-error-if-not-client-class-p class 'jupyter-rest-client)
|
||||
(let ((url (url-generic-parse-url (oref hub url))))
|
||||
(setf (url-filename url) (concat "/user/" user))
|
||||
(let ((client (make-instance class :url (url-recreate-url url) :auth t)))
|
||||
(prog1 client
|
||||
(unless (jupyter-hub-user-logged-in-p hub user)
|
||||
(funcall (oref hub authenticator) hub user))
|
||||
(jupyter-api-copy-cookies-for-websocket (oref client url))))))
|
||||
|
||||
;;; Utility
|
||||
|
||||
;; See https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#special-handling-of-api-requests
|
||||
(cl-defmethod jupyter-hub-service-available-p ((hub jupyter-hub-client) &rest endpoint)
|
||||
"Send a GET request to ENDPOINT at HUB and return non-nil if it was successful.
|
||||
If a 503 error (service unavailable) occurs return nil. Raise an
|
||||
error otherwise."
|
||||
(condition-case err
|
||||
(prog1 t (apply #'jupyter-api-request hub "GET" endpoint))
|
||||
(jupyter-api-http-error
|
||||
(unless (eq (nth 1 err) 503) ; Service unavailable
|
||||
(signal (car err) (cdr err))))))
|
||||
|
||||
;; See https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#hub-spawn-pending-username-servername
|
||||
(defun jupyter-hub-spawn-pending-p (hub user)
|
||||
"Return non-nil if USER's server on HUB has been spawned and is pending."
|
||||
(when (jupyter-hub-service-available-p hub "user" user)
|
||||
(let ((url-max-redirections 0))
|
||||
;; If the spawn-pending page is redirected, then the server is ready.
|
||||
(condition-case nil
|
||||
(prog1 t
|
||||
(jupyter-api-request hub "GET" "hub" "spawn-pending" user))
|
||||
(jupyter-api-http-redirect-limit nil)))))
|
||||
|
||||
(defun jupyter-hub-user-server (hub user)
|
||||
(jupyter-hub-make-client hub user 'jupyter-server))
|
||||
|
||||
(provide 'jupyter-hub-api)
|
||||
|
||||
;;; jupyter-hub-api.el ends here
|
Loading…
Add table
Reference in a new issue