2015-01-31 10:13:49 -06:00
|
|
|
;;; ein-contents-api.el --- Interface to Jupyter's Contents API
|
|
|
|
|
|
|
|
;; Copyright (C) 2015 - John Miller
|
|
|
|
|
|
|
|
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
|
|
|
;; John M. Miller <millejoh at mac.com>
|
|
|
|
|
|
|
|
;; This file is NOT part of GNU Emacs.
|
|
|
|
|
|
|
|
;; ein-contents-api.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-contents-api.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-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
;;;
|
|
|
|
;;; An interface to the Jupyter Contents API as described in
|
|
|
|
;;; https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service.
|
|
|
|
;;;
|
|
|
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
(require 'ein-core)
|
2015-02-09 13:27:20 -06:00
|
|
|
(require 'ein-utils)
|
2015-01-31 10:13:49 -06:00
|
|
|
|
2015-02-10 14:53:08 -06:00
|
|
|
(defcustom ein:content-query-timeout (* 60 1000) ; 1 min
|
|
|
|
"Query timeout for getting content from Jupyter/IPython notebook.
|
|
|
|
If you cannot open large notebooks because of a timeout error try
|
|
|
|
increasing this value. Setting this value to `nil' means to use
|
|
|
|
global setting. For global setting and more information, see
|
|
|
|
`ein:query-timeout'."
|
|
|
|
:type '(choice (integer :tag "Timeout [ms]" 5000)
|
|
|
|
(const :tag "Use global setting" nil))
|
|
|
|
:group 'ein)
|
|
|
|
|
|
|
|
|
2015-01-31 10:13:49 -06:00
|
|
|
(defstruct ein:$content
|
|
|
|
"Content returned from the Jupyter notebook server:
|
|
|
|
`ein:$content-url-or-port'
|
|
|
|
URL or port of Jupyter server.
|
|
|
|
|
|
|
|
`ein:$content-name
|
|
|
|
The name/filename of the content. Always equivalent to the last
|
|
|
|
part of the path field
|
|
|
|
|
|
|
|
`ein:$content-path
|
|
|
|
The full file path. It will not start with /, and it will be /-delimited.
|
|
|
|
|
|
|
|
`ein:$content-type
|
|
|
|
One of three values: :directory, :file, :notebook.
|
|
|
|
|
|
|
|
`ein:$content-writable
|
|
|
|
Indicates if requester has permission to modified the requested content.
|
|
|
|
|
|
|
|
`ein:$content-created
|
|
|
|
|
|
|
|
`ein:$content-last-modified
|
|
|
|
|
|
|
|
`ein:$content-mimetype
|
|
|
|
Specify the mime-type of :file content, null otherwise.
|
|
|
|
|
|
|
|
`ein:$content-raw-content
|
|
|
|
Contents of resource as returned by Jupyter. Depending on content-type will hold:
|
|
|
|
:directory : JSON list of models for each item in the directory.
|
|
|
|
:file : Text of file as a string or base64 encoded string if mimetype
|
|
|
|
is other than 'text/plain'.
|
|
|
|
:notebook : JSON structure of the file.
|
|
|
|
|
|
|
|
`ein:$content-format
|
|
|
|
Value will depend on content-type:
|
|
|
|
:directory : :json.
|
|
|
|
:file : Either :text or :base64
|
|
|
|
:notebook : :json.
|
|
|
|
"
|
|
|
|
url-or-port
|
2015-02-10 14:53:08 -06:00
|
|
|
ipython-version
|
2015-01-31 10:13:49 -06:00
|
|
|
name
|
|
|
|
path
|
|
|
|
type
|
|
|
|
writable
|
|
|
|
created
|
|
|
|
last-modified
|
|
|
|
mimetype
|
|
|
|
raw-content
|
|
|
|
format)
|
|
|
|
|
|
|
|
(defun ein:content-url (url-or-port path &optional name)
|
|
|
|
(if name
|
|
|
|
(ein:url url-or-port "api/contents" path name)
|
|
|
|
(ein:url url-or-port "api/contents" path)))
|
|
|
|
|
2015-02-09 13:27:20 -06:00
|
|
|
(defun ein:content-url-legacy (url-or-port path &optional name)
|
|
|
|
"Generate content url's for IPython Notebook version 2.x"
|
|
|
|
(if name
|
|
|
|
(ein:url url-or-port "api/notebooks" path name)
|
|
|
|
(ein:url url-or-port "api/notebooks" path)))
|
|
|
|
|
2015-02-10 14:53:08 -06:00
|
|
|
(defun ein:content-query-contents (path &optional url-or-port force-sync callback)
|
2015-01-31 10:31:20 -06:00
|
|
|
"Return the contents of the object at the specified path from the Jupyter server."
|
2015-01-31 10:13:49 -06:00
|
|
|
(let* ((url-or-port (or url-or-port (ein:default-url-or-port)))
|
|
|
|
(url (ein:content-url url-or-port path))
|
2015-02-10 14:53:08 -06:00
|
|
|
(new-content (make-ein:$content
|
|
|
|
:url-or-port url-or-port
|
|
|
|
:ipython-version (ein:query-ipython-version url-or-port))))
|
|
|
|
(if (= 2 (ein:$content-ipython-version new-content))
|
|
|
|
(setq new-content (ein:content-query-contents-legacy path url-or-port force-sync callback))
|
2015-02-09 13:27:20 -06:00
|
|
|
(ein:query-singleton-ajax
|
|
|
|
(list 'content-query-contents url-or-port path)
|
|
|
|
url
|
|
|
|
:type "GET"
|
2015-02-10 14:53:08 -06:00
|
|
|
:timeout ein:content-query-timeout
|
2015-02-09 13:27:20 -06:00
|
|
|
:parser #'ein:json-read
|
|
|
|
:sync force-sync
|
2015-02-10 14:53:08 -06:00
|
|
|
:success (apply-partially #'ein:new-content new-content callback)
|
2015-02-09 13:27:20 -06:00
|
|
|
:error (apply-partially #'ein-content-list-contents-error url)))
|
|
|
|
new-content))
|
|
|
|
|
2015-02-10 14:53:08 -06:00
|
|
|
(defun ein:content-query-contents-legacy (path &optional url-or-port force-sync callback)
|
2015-02-09 13:27:20 -06:00
|
|
|
"Return contents of boject at specified path for IPython Notebook versions 2.x"
|
|
|
|
(let* ((url-or-port (or url-or-port (ein:default-url-or-port)))
|
|
|
|
(url (ein:content-url-legacy url-or-port path))
|
2015-02-10 14:53:08 -06:00
|
|
|
(new-content (make-ein:$content :url-or-port url-or-port
|
|
|
|
:ipython-version (ein:query-ipython-version url-or-port))))
|
2015-01-31 10:13:49 -06:00
|
|
|
(ein:query-singleton-ajax
|
2015-02-09 13:27:20 -06:00
|
|
|
(list 'content-query-contents-legacy url-or-port path)
|
2015-01-31 10:13:49 -06:00
|
|
|
url
|
|
|
|
:type "GET"
|
2015-02-10 14:53:08 -06:00
|
|
|
:timeout ein:content-query-timeout
|
2015-01-31 10:13:49 -06:00
|
|
|
:parser #'ein:json-read
|
|
|
|
:sync force-sync
|
2015-02-12 08:45:29 -06:00
|
|
|
:success (apply-partially #'ein:query-contents-legacy-success path new-content callback)
|
2015-02-09 13:27:20 -06:00
|
|
|
:error (apply-partially #'ein-content-query-contents-error url))
|
2015-01-31 10:13:49 -06:00
|
|
|
new-content))
|
|
|
|
|
2015-02-09 13:27:20 -06:00
|
|
|
(defun ein:fix-legacy-content-data (data)
|
|
|
|
(if (listp (car data))
|
|
|
|
(loop for item in data
|
|
|
|
collecting
|
|
|
|
(ein:fix-legacy-content-data item))
|
|
|
|
(if (string= (plist-get data :path) "")
|
|
|
|
(plist-put data :path (plist-get data :name))
|
|
|
|
(plist-put data :path (format "%s/%s" (plist-get data :path) (plist-get data :name))))))
|
|
|
|
|
2015-02-12 08:45:29 -06:00
|
|
|
(defun* ein:query-contents-legacy-success (path content callback &key data &allow-other-keys)
|
|
|
|
(if (not (plist-get data :type))
|
|
|
|
;; Content API in 2.x a bit inconsistent.
|
|
|
|
(progn
|
|
|
|
(setf (ein:$content-name content) (substring path (or (cl-position ?/ path) 0))
|
|
|
|
(ein:$content-path content) path
|
|
|
|
(ein:$content-type content) "directory"
|
|
|
|
;;(ein:$content-created content) (plist-get data :created)
|
|
|
|
;;(ein:$content-last-modified content) (plist-get data :last_modified)
|
|
|
|
(ein:$content-format content) nil
|
|
|
|
(ein:$content-writable content) nil
|
|
|
|
(ein:$content-mimetype content) nil
|
|
|
|
(ein:$content-raw-content content) (ein:fix-legacy-content-data data))
|
|
|
|
(when callback
|
|
|
|
(funcall callback content))
|
|
|
|
content)
|
|
|
|
(ein:new-content content callback :data data)))
|
2015-02-09 13:27:20 -06:00
|
|
|
|
2015-02-10 14:53:08 -06:00
|
|
|
(defun* ein:new-content (content callback &key data &allow-other-keys)
|
2015-01-31 10:31:20 -06:00
|
|
|
(setf (ein:$content-name content) (plist-get data :name)
|
|
|
|
(ein:$content-path content) (plist-get data :path)
|
|
|
|
(ein:$content-type content) (plist-get data :type)
|
|
|
|
(ein:$content-created content) (plist-get data :created)
|
|
|
|
(ein:$content-last-modified content) (plist-get data :last_modified)
|
|
|
|
(ein:$content-format content) (plist-get data :format)
|
|
|
|
(ein:$content-writable content) (plist-get data :writable)
|
|
|
|
(ein:$content-mimetype content) (plist-get data :mimetype)
|
2015-02-10 14:53:08 -06:00
|
|
|
(ein:$content-raw-content content) (plist-get data :content))
|
|
|
|
(when callback
|
|
|
|
(funcall callback content))
|
|
|
|
content)
|
2015-01-31 10:31:20 -06:00
|
|
|
|
2015-02-12 08:45:29 -06:00
|
|
|
(defun ein:content-to-json (content)
|
|
|
|
(let ((path (if (>= (ein:$content-ipython-version content) 3)
|
|
|
|
(ein:$content-path content)
|
|
|
|
(substring (ein:$content-path content)
|
|
|
|
0
|
|
|
|
(cl-position ?/ (ein:$content-path content) :from-end t)))))
|
|
|
|
(json-encode `((:type . ,(ein:$content-type content))
|
|
|
|
(:name . ,(ein:$content-name content))
|
|
|
|
(:path . ,path)
|
|
|
|
(:content ,@(ein:$content-raw-content content))))))
|
|
|
|
|
|
|
|
(defun ein:content-from-notebook (nb)
|
|
|
|
(let ((nb-content (ein:notebook-to-json nb)))
|
|
|
|
(make-ein:$content :name (ein:$notebook-notebook-name nb)
|
|
|
|
:path (ein:$notebook-notebook-path nb)
|
|
|
|
:type "notebook"
|
|
|
|
:ipython-version (ein:$notebook-api-version nb)
|
|
|
|
:raw-content nb-content)))
|
|
|
|
|
|
|
|
|
2015-02-09 13:27:20 -06:00
|
|
|
(defun* ein:content-query-contents-error (url &key symbol-status response &allow-other-keys)
|
2015-01-31 10:13:49 -06:00
|
|
|
(ein:log 'verbose
|
|
|
|
"Error thrown: %S" (request-response-error-thrown response))
|
|
|
|
(ein:log 'error
|
|
|
|
"Content list call %s failed with status %s." url symbol-status))
|
|
|
|
|
2015-01-31 10:31:20 -06:00
|
|
|
;; ***
|
2015-01-31 10:13:49 -06:00
|
|
|
|
|
|
|
(defvar *ein:content-hierarchy* (make-hash-table))
|
|
|
|
|
|
|
|
(defun ein:make-content-hierarchy (path url-or-port)
|
2015-02-09 13:27:20 -06:00
|
|
|
(let* ((node (ein:content-query-contents path url-or-port t))
|
2015-01-31 10:13:49 -06:00
|
|
|
(items (ein:$content-raw-content node)))
|
|
|
|
(ein:flatten (loop for item in items
|
|
|
|
for c = (make-ein:$content :url-or-port url-or-port)
|
2015-02-10 14:53:08 -06:00
|
|
|
do (ein:new-content c nil :data item)
|
2015-01-31 10:13:49 -06:00
|
|
|
collect
|
|
|
|
(cond ((string= (ein:$content-type c) "directory")
|
|
|
|
(cons c
|
|
|
|
(ein:make-content-hierarchy (ein:$content-path c) url-or-port)))
|
|
|
|
(t c))))))
|
|
|
|
|
|
|
|
(defun ein:refresh-content-hierarchy (&optional url-or-port)
|
|
|
|
(let ((url-or-port (or url-or-port (ein:default-url-or-port))))
|
|
|
|
(setf (gethash url-or-port *ein:content-hierarchy*)
|
|
|
|
(ein:make-content-hierarchy "" url-or-port))))
|
|
|
|
|
2015-02-12 08:45:29 -06:00
|
|
|
;;; Save Content
|
|
|
|
(defun ein:content-save (content &optional callback cbargs)
|
|
|
|
(let ((url-or-port (ein:$content-url-or-port content)))
|
|
|
|
(if (>= (ein:$content-ipython-version content) 3)
|
|
|
|
(let ((path (ein:$content-path content)))
|
|
|
|
(ein:query-singleton-ajax
|
|
|
|
(list 'content-save url-or-port path)
|
|
|
|
(ein:content-url url-or-port path)
|
|
|
|
:type "PUT"
|
|
|
|
:timeout ein:content-query-timeout
|
|
|
|
:data (ein:content-to-json content)
|
|
|
|
:success (apply-partially #'ein:content-save-success callback cbargs)
|
|
|
|
:error (apply-partially #'ein:content-save-error (ein:content-url url-or-port path)))))))
|
|
|
|
|
|
|
|
(defun* ein:content-save-success (callback cbargs &key status response &allow-other-keys)
|
|
|
|
(ein:log 'info "Saving content successful with status %s" status)
|
|
|
|
(when callback
|
|
|
|
(apply callback cbargs)))
|
|
|
|
|
|
|
|
(defun* ein:content-save-error (url &key symbol-status response &allow-other-keys)
|
|
|
|
(ein:log 'verbose
|
|
|
|
"Error thrown: %S" (request-response-error-thrown response))
|
|
|
|
(ein:log 'error
|
|
|
|
"Content list call %s failed with status %s." url symbol-status))
|
|
|
|
|
|
|
|
|
|
|
|
;;; Rename Content
|
2015-01-31 10:31:20 -06:00
|
|
|
|
|
|
|
(defun ein:content-rename (content new-path)
|
|
|
|
(let ((url-or-port (ein:$content-url-or-port content))
|
|
|
|
(path (ein:$content-path content)))
|
|
|
|
(ein:query-singleton-ajax
|
2015-02-12 08:45:29 -06:00
|
|
|
(list 'content-rename url-or-port path)
|
2015-01-31 10:31:20 -06:00
|
|
|
(ein:content-url url-or-port path)
|
|
|
|
:type "PATCH"
|
2015-01-31 10:32:52 -06:00
|
|
|
:data (json-encode `(:path ,new-path))
|
|
|
|
:parser #'ein:json-read
|
2015-01-31 10:31:20 -06:00
|
|
|
:success (apply-partially #'update-content-path content)
|
|
|
|
:error (apply-partially #'ein-content-rename-error path))))
|
|
|
|
|
|
|
|
(defun* update-content-path (content &key data &allow-other-keys)
|
|
|
|
(setf (ein:$content-path content) (plist-get data :path)
|
|
|
|
(ein:$content-name content) (plist-get data :name)
|
|
|
|
(ein:$content-last-modified content) (plist-get data :last_modified)))
|
|
|
|
|
|
|
|
(defun* ein:content-rename-error (path &key symbol-status response &allow-other-keys)
|
|
|
|
(ein:log 'verbose
|
|
|
|
"Error thrown: %S" (request-response-error-thrown response))
|
|
|
|
(ein:log 'error
|
|
|
|
"Renaming content %s failed with status %s." path symbol-status))
|
2015-02-09 13:27:20 -06:00
|
|
|
|
|
|
|
|
|
|
|
(provide 'ein-contents-api)
|