Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
8.7 KiB

  1. ;;; ein-jupyterhub.el --- Interface to Jupyterhub -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2016 - John Miller
  3. ;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
  4. ;; John M. Miller <millejoh at mac.com>
  5. ;; This file is NOT part of GNU Emacs.
  6. ;; ein-jupyterhub.el is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; ein-jupyterhub.el is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with ein-jupyter.el. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;
  18. ;;;
  19. ;;; An interface to the Jupyterhub login and management API as described in
  20. ;;; http://jupyterhub.readthedocs.io/en/latest/api/index.html
  21. ;;;
  22. ;;
  23. ;;; Code:
  24. (require 'ein-query)
  25. (require 'ein-websocket)
  26. (require 'ein-notebooklist)
  27. (defvar *ein:jupyterhub-connections* (make-hash-table :test #'equal))
  28. (cl-defstruct ein:$jh-conn
  29. "Data representing a connection to a jupyterhub server."
  30. url-or-port
  31. version
  32. user
  33. token)
  34. (cl-defstruct ein:$jh-user
  35. "A jupyterhub user, per https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/User"
  36. name
  37. admin
  38. groups
  39. server
  40. pending
  41. last-activity)
  42. (defsubst ein:jupyterhub-user-path (url-or-port &rest paths)
  43. "Goes from URL-OR-PORT/PATHS to URL-OR-PORT/user/someone/PATHS"
  44. (let ((user-base (ein:aif (gethash url-or-port *ein:jupyterhub-connections*)
  45. (ein:$jh-user-server (ein:$jh-conn-user it)))))
  46. (apply #'ein:url url-or-port user-base paths)))
  47. (defsubst ein:jupyterhub-api-path (url-or-port &rest paths)
  48. (apply #'ein:url url-or-port "hub/api" paths))
  49. (defun ein:jupyterhub--store-cookies (conn)
  50. "Websockets use the url-cookie API"
  51. (let* ((url-or-port (ein:$jh-conn-url-or-port conn))
  52. (parsed-url (url-generic-parse-url url-or-port))
  53. (host-port (if (url-port-if-non-default parsed-url)
  54. (format "%s:%s" (url-host parsed-url) (url-port parsed-url))
  55. (url-host parsed-url)))
  56. (securep (string= (url-type parsed-url) "https"))
  57. (cookies (append
  58. (request-cookie-alist (url-host parsed-url) "/hub/" securep)
  59. (ein:aand (ein:$jh-conn-user conn) (ein:$jh-user-server it)
  60. (request-cookie-alist (url-host parsed-url) it securep)))))
  61. (dolist (c cookies)
  62. (ein:websocket-store-cookie c host-port
  63. (car (url-path-and-query parsed-url)) securep))))
  64. (cl-defun ein:jupyterhub--login-complete (dobj conn &key response &allow-other-keys)
  65. (deferred:callback-post dobj (list conn response)))
  66. (defmacro ein:jupyterhub--add-header (header)
  67. `(setq my-settings
  68. (plist-put my-settings :headers
  69. (append (plist-get my-settings :headers) (list ,header)))))
  70. (defmacro ein:jupyterhub-query (conn-key url cb cbargs &rest settings)
  71. `(let ((my-settings (list ,@settings)))
  72. (ein:and-let* ((conn (gethash ,conn-key *ein:jupyterhub-connections*)))
  73. (ein:jupyterhub--add-header
  74. (cons "Referer" (ein:url (ein:$jh-conn-url-or-port conn) "hub/login")))
  75. (ein:aif (ein:$jh-conn-token conn)
  76. (ein:jupyterhub--add-header
  77. (cons "Authorization" (format "token %s" it)))))
  78. (apply #'ein:query-singleton-ajax
  79. ,url ,url
  80. :error
  81. (lambda (&rest args)
  82. (ein:log 'error "ein:jupyterhub-query--error (%s) %s (%s)" ,url
  83. (request-response-status-code (plist-get args :response))
  84. (plist-get args :symbol-status)))
  85. :complete
  86. (lambda (&rest args)
  87. (ein:log 'debug "ein:jupyterhub-query--complete (%s) %s (%s)" ,url
  88. (request-response-status-code (plist-get args :response))
  89. (plist-get args :symbol-status)))
  90. :success
  91. (lambda (&rest args)
  92. (apply ,cb (request-response-data (plist-get args :response)) ,cbargs))
  93. my-settings)))
  94. (defsubst ein:jupyterhub--query-login (callback username password conn)
  95. (ein:jupyterhub-query
  96. (ein:$jh-conn-url-or-port conn)
  97. (ein:url (ein:$jh-conn-url-or-port conn) "hub/login")
  98. #'ein:jupyterhub--receive-login
  99. `(,callback ,username ,password ,conn)
  100. ;; :type "POST" ;; no type here else redirect will use POST
  101. :parser #'ignore
  102. :data `(("username" . ,username)
  103. ("password" . ,password))))
  104. (defun ein:jupyterhub--receive-version (data url-or-port callback username password)
  105. (let ((conn (make-ein:$jh-conn
  106. :url-or-port url-or-port
  107. :version (plist-get data :version))))
  108. (setf (gethash url-or-port *ein:jupyterhub-connections*) conn)
  109. (ein:jupyterhub--query-login callback username password conn)))
  110. (defun ein:jupyterhub--receive-user (data callback username password conn iteration)
  111. (let ((user (make-ein:$jh-user :name (plist-get data :name)
  112. :admin (plist-get data :admin)
  113. :groups (plist-get data :groups)
  114. :server (plist-get data :server)
  115. :pending (plist-get data :pending)
  116. :last-activity (plist-get data :last_activity))))
  117. (setf (ein:$jh-conn-user conn) user)
  118. (ein:jupyterhub--store-cookies conn)
  119. (if (not (ein:$jh-user-server user))
  120. (if (<= iteration 0)
  121. (ein:jupyterhub--query-token callback username password conn)
  122. (ein:display-warning "jupyterhub cannot start single-user server" :error))
  123. (ein:notebooklist-open*
  124. (ein:jupyterhub-user-path (ein:$jh-conn-url-or-port conn))
  125. nil nil callback))))
  126. (defsubst ein:jupyterhub--query-user (callback username password conn iteration)
  127. (ein:jupyterhub-query
  128. (ein:$jh-conn-url-or-port conn)
  129. (ein:jupyterhub-api-path (ein:$jh-conn-url-or-port conn) "users" username)
  130. #'ein:jupyterhub--receive-user
  131. `(,callback ,username ,password ,conn ,iteration)
  132. :type "GET"
  133. :parser #'ein:json-read))
  134. (defun ein:jupyterhub--receive-login (_data callback username password conn)
  135. (ein:jupyterhub--store-cookies conn)
  136. (ein:jupyterhub--query-user callback username password conn 0))
  137. (defun ein:jupyterhub--receive-server (_data callback username password conn)
  138. (ein:jupyterhub--query-user callback username password conn 1))
  139. (defsubst ein:jupyterhub--query-server (callback username password conn)
  140. (ein:jupyterhub-query
  141. (ein:$jh-conn-url-or-port conn)
  142. (ein:jupyterhub-api-path (ein:$jh-conn-url-or-port conn)
  143. "users" username "server")
  144. #'ein:jupyterhub--receive-server
  145. `(,callback ,username ,password ,conn)
  146. :type "POST"
  147. :parser #'ein:json-read))
  148. (defun ein:jupyterhub--receive-token (data callback username password conn)
  149. (setf (ein:$jh-conn-token conn) (plist-get data :token))
  150. (ein:jupyterhub--query-server callback username password conn))
  151. (defun ein:jupyterhub--query-token (callback username password conn)
  152. (ein:jupyterhub-query
  153. (ein:$jh-conn-url-or-port conn)
  154. (ein:jupyterhub-api-path (ein:$jh-conn-url-or-port conn)
  155. "authorizations/token")
  156. #'ein:jupyterhub--receive-token
  157. `(,callback ,username ,password ,conn)
  158. :type "POST"
  159. :data (json-encode `((:username . ,username)
  160. (:password . ,password)))
  161. :parser #'ein:json-read))
  162. (defsubst ein:jupyterhub--query-version (url-or-port callback username password)
  163. (ein:jupyterhub-query
  164. url-or-port
  165. (ein:jupyterhub-api-path url-or-port)
  166. #'ein:jupyterhub--receive-version
  167. `(,url-or-port ,callback ,username ,password)
  168. :type "GET"
  169. :parser #'ein:json-read))
  170. ;;;###autoload
  171. (defun ein:jupyterhub-connect (url-or-port username password callback)
  172. "Log on to a jupyterhub server using PAM authentication. Requires jupyterhub version 0.8 or greater. CALLBACK takes two arguments, the resulting buffer and the singleuser url-or-port"
  173. (interactive (let ((url-or-port (ein:notebooklist-ask-url-or-port))
  174. (pam-plist (ein:notebooklist-ask-user-pw-pair "User" "Password")))
  175. (cl-loop for (user pw) on pam-plist by (function cddr)
  176. return (list url-or-port (symbol-name user) pw (lambda (buffer _url-or-port) (pop-to-buffer buffer))))))
  177. (ein:jupyterhub--query-version url-or-port callback username password))
  178. (provide 'ein-jupyterhub)