;;; ein-query.el --- jQuery like interface on top of curl -*- lexical-binding: t -*-
;; Copyright (C) 2012- Takafumi Arakaki
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
;; This file is NOT part of GNU Emacs.
;; ein-query.el 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 of the License, or
;; (at your option) any later version.
;; ein-query.el 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 ein-query.el. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'request)
(require 'url)
(require 'ein-core)
(require 'ein-log)
(defcustom ein:query-timeout 10000
"Default query timeout for HTTP access in millisecond."
:type '(choice (integer :tag "Timeout [ms]" 1000)
(const :tag "No timeout" nil))
:group 'ein)
(defvar ein:query-xsrf-cache (make-hash-table :test 'equal)
"Remember the last xsrf token by host.
This is a hack in case we catch cookie jar in transition.
The proper fix is to sempahore between competing curl processes.")
(defvar ein:query-authorization-tokens (make-hash-table :test 'equal)
"Jupyterhub authorization token by (host . username).")
(defun ein:query-get-cookies (host path-prefix)
"Return (:path :expire :name :value) for HOST, matching PATH-PREFIX."
(when-let ((filename (request--curl-cookie-jar)))
(with-temp-buffer
(insert-file-contents filename)
(cl-loop for (domain _flag path _secure _http-only expire name value)
in (request--netscape-cookie-parse)
when (and (string= domain host)
(cl-search path-prefix path))
collect `(:path ,path :expire ,expire :name ,name :value ,value)))))
(defun ein:query-prepare-header (url settings &optional securep)
"Ensure that REST calls to the jupyter server have the correct _xsrf argument."
(let* ((host (url-host (url-generic-parse-url url)))
(cookies (request-cookie-alist host "/" securep))
(xsrf (or (cdr (assoc-string "_xsrf" cookies))
(gethash host ein:query-xsrf-cache)))
(key (ein:query-divine-authorization-tokens-key url))
(token (aand key
(gethash key ein:query-authorization-tokens)
(cons "Authorization" (format "token %s" it)))))
(setq settings (plist-put settings :headers
(append (plist-get settings :headers)
(list (cons "User-Agent" "Mozilla/5.0")))))
(when token
(setq settings (plist-put settings :headers
(append (plist-get settings :headers)
(list token)))))
(when xsrf
(setq settings (plist-put settings :headers
(append (plist-get settings :headers)
(list (cons "X-XSRFTOKEN" xsrf)))))
(setf (gethash host ein:query-xsrf-cache) xsrf))
(setq settings (plist-put settings :encoding 'binary))
settings))
(defun ein:query-divine-authorization-tokens-key (url)
"Infer semblance of jupyterhub root.
From https://hub.data8x.berkeley.edu/hub/user/806b3e7/notebooks/Untitled.ipynb,
get (\"hub.data8x.berkeley.edu\" . \"806b3e7\")"
(-when-let* ((parsed-url (url-generic-parse-url url))
(url-host (url-host parsed-url))
(slash-path (car (url-path-and-query parsed-url)))
(components (split-string slash-path "/" t)))
(awhen (member "user" components)
(cons url-host (cl-second it)))))
(cl-defun ein:query-singleton-ajax (url &rest settings
&key (timeout ein:query-timeout)
&allow-other-keys)
(if (executable-find request-curl)
(let ((request-backend 'curl))
(when timeout
(setq settings (plist-put settings :timeout (/ timeout 1000.0))))
(unless (plist-member settings :sync)
(setq settings (plist-put settings :sync ein:force-sync)))
(apply #'request (url-encode-url url) (ein:query-prepare-header url settings)))
(ein:display-warning
(format "The %s program was not found, aborting" request-curl)
:error)))
(defun ein:query-kernelspecs (url-or-port callback &optional iteration)
"Send for kernelspecs of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
(setq iteration (or iteration 0))
(ein:query-singleton-ajax
(ein:url url-or-port "api/kernelspecs")
:type "GET"
:parser #'ein:json-read
:complete (apply-partially #'ein:query-kernelspecs--complete url-or-port)
:success (apply-partially #'ein:query-kernelspecs--success url-or-port callback)
:error (apply-partially #'ein:query-kernelspecs--error url-or-port callback iteration)))
(defun ein:normalize-kernelspec-language (name)
"Normalize the kernelspec language string"
(if (stringp name)
(replace-regexp-in-string "[ ]" "-" name)
name))
(cl-defun ein:query-kernelspecs--success (url-or-port callback
&key data _symbol-status _response
&allow-other-keys)
(let ((ks (list :default (plist-get data :default)))
(specs (ein:plist-iter (plist-get data :kernelspecs))))
(setf (gethash url-or-port *ein:kernelspecs*)
(ein:flatten (dolist (spec specs ks)
(let ((name (car spec))
(info (cdr spec)))
(push (list name (make-ein:$kernelspec :name (plist-get info :name)
:display-name (plist-get (plist-get info :spec)
:display_name)
:resources (plist-get info :resources)
:language (ein:normalize-kernelspec-language
(plist-get (plist-get info :spec)
:language))
:spec (plist-get info :spec)))
ks))))))
(when callback (funcall callback)))
(cl-defun ein:query-kernelspecs--error
(url-or-port callback iteration
&key data response error-thrown &allow-other-keys
&aux
(response-status (request-response-status-code response))
(hub-p (request-response-header response "x-jupyterhub-version")))
(if (< iteration 3)
(if (and hub-p (eq response-status 405))
(ein:query-kernelspecs--success url-or-port callback :data data)
(ein:log 'verbose "Retry kernelspecs #%s in response to %s"
iteration response-status)
(ein:query-kernelspecs url-or-port callback (1+ iteration)))
(ein:log 'error
"ein:query-kernelspecs--error %s: ERROR %s DATA %s"
url-or-port (car error-thrown) (cdr error-thrown))
(when callback (funcall callback))))
(cl-defun ein:query-kernelspecs--complete (_url-or-port &key data response &allow-other-keys
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
(ein:log 'debug "ein:query-kernelspecs--complete %s" resp-string))
(defun ein:query-notebook-api-version (url-or-port callback)
"Get notebook version of URL-OR-PORT with CALLBACK arity 0 (a semaphore)."
(ein:query-singleton-ajax
(ein:url url-or-port "api/spec.yaml")
;; the melpa yaml package was taking too long, unfortunately
:parser (lambda ()
(if (re-search-forward "api\\s-+version: \\(\\S-+\\)"
nil t)
;; emacs-25.3 doesn't have the right string-trim
(string-remove-prefix
"\"" (string-remove-suffix
"\"" (match-string-no-properties 1)))
""))
:complete (apply-partially #'ein:query-notebook-api-version--complete
url-or-port callback)))
(cl-defun ein:query-notebook-api-version--complete
(url-or-port callback
&key data response
&allow-other-keys &aux
(resp-string (format "STATUS: %s DATA: %s"
(request-response-status-code response) data))
(hub-p (request-response-header response "x-jupyterhub-version")))
(ein:log 'debug "ein:query-notebook-api-version--complete %s" resp-string)
(if (not (zerop (string-to-number data)))
(setf (gethash url-or-port *ein:notebook-api-version*) data)
(if hub-p
(let ((key (ein:query-divine-authorization-tokens-key url-or-port)))
(remhash key ein:query-authorization-tokens)
(ein:display-warning
(format "Server for user %s requires start, aborting"
(or (cdr key) "unknown"))
:error)
(setq callback nil))
(ein:log 'warn "notebook api version currently unknowable")))
(when callback (funcall callback)))
(provide 'ein-query)
;;; ein-query.el ends here