;;; ein-contents-api.el --- Interface to Jupyter's Contents API ;; Copyright (C) 2015 - John Miller ;; Authors: Takafumi Arakaki ;; John M. Miller ;; 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 . ;;; Commentary: ;;; ;;; An interface to the Jupyter Contents API as described in ;;; https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service. ;;; ;; ;;; Code: (require 'cl) (require 'ein-core) (require 'ein-utils) (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) (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 ipython-version name path type writable created last-modified mimetype raw-content format) (defun ein:content-url (content) (let ((url-or-port (ein:$content-url-or-port content)) (path (ein:$content-path content))) (ein:url url-or-port "api/contents" path))) (defun ein:content-url-legacy (content) "Generate content url's for IPython Notebook version 2.x" (let ((url-or-port (ein:$content-url-or-port content)) (path (ein:$content-path content))) (ein:url url-or-port "api/notebooks" path))) (defun ein:content-query-contents (path &optional url-or-port force-sync callback) "Return the contents of the object at the specified path from the Jupyter server." (let* ((url-or-port (or url-or-port (ein:default-url-or-port))) (new-content (make-ein:$content :url-or-port url-or-port :ipython-version (ein:query-ipython-version url-or-port) :path path)) (url (ein:content-url new-content))) (if (= 2 (ein:$content-ipython-version new-content)) (setq new-content (ein:content-query-contents-legacy path url-or-port force-sync callback)) (ein:query-singleton-ajax (list 'content-query-contents url-or-port path) url :type "GET" :timeout ein:content-query-timeout :parser #'ein:json-read :sync force-sync :success (apply-partially #'ein:new-content new-content callback) :error (apply-partially #'ein:content-query-contents-error url))) new-content)) (defun ein:content-query-contents-legacy (path &optional url-or-port force-sync callback) "Return contents of object at specified path for IPython Notebook versions 2.x" (let* ((url-or-port (or url-or-port (ein:default-url-or-port))) (new-content (make-ein:$content :url-or-port url-or-port :ipython-version (ein:query-ipython-version url-or-port) :path path)) (url (ein:content-url-legacy new-content))) (ein:query-singleton-ajax (list 'content-query-contents-legacy url-or-port path) url :type "GET" :timeout ein:content-query-timeout :parser #'ein:json-read :sync force-sync :success (apply-partially #'ein:query-contents-legacy-success path new-content callback) :error (apply-partially #'ein:content-query-contents-error url)) new-content)) (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)))))) (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))) (defun* ein:new-content (content callback &key data &allow-other-keys) (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) (ein:$content-raw-content content) (plist-get data :content)) (when callback (funcall callback content)) content) (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 (or (cl-position ?/ (ein:$content-path content) :from-end t) 0))))) (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) :url-or-port (ein:$notebook-url-or-port nb) :type "notebook" :ipython-version (ein:$notebook-api-version nb) :raw-content nb-content))) (defun* ein:content-query-contents-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)) ;; *** (defvar *ein:content-hierarchy* (make-hash-table)) (defun ein:make-content-hierarchy (path url-or-port) (let* ((node (ein:content-query-contents path url-or-port t)) (items (ein:$content-raw-content node))) (ein:flatten (loop for item in items for c = (make-ein:$content :url-or-port url-or-port) do (ein:new-content c nil :data item) 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)))) ;;; Save Content (defun ein:content-save (content &optional callback cbargs) (ein:query-singleton-ajax (list 'content-save (ein:$content-url-or-port content) (ein:$content-path content)) (ein:content-url content) :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 content)))) (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 (defun ein:content-legacy-rename (content new-path callback cbargs) (let ((path (substring new-path 0 (or (position ?/ new-path :from-end t) 0))) (name (substring new-path (or (position ?/ new-path :from-end t) 0)))) (ein:query-singleton-ajax (list 'content-rename (ein:$content-url-or-port content) (ein:$content-path content)) (ein:content-url-legacy content) :type "PATCH" :data (json-encode `((name . ,name) (path . ,path))) :parser #'ein:json-read :success (apply-partially #'update-content-path content callback cbargs) :error (apply-partially #'ein:content-rename-error new-path)))) (defun ein:content-rename (content new-path &optional callback cbargs) (if (>= (ein:$content-ipython-version content) 3) (ein:query-singleton-ajax (list 'content-rename (ein:$content-url-or-port content) (ein:$content-path content)) (ein:content-url content) :type "PATCH" :data (json-encode `((path . ,new-path))) :parser #'ein:json-read :success (apply-partially #'update-content-path content callback cbargs) :error (apply-partially #'ein:content-rename-error (ein:$content-path content))) (ein:content-legacy-rename content new-path callback cbargs))) (defun* update-content-path (content callback cbargs &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)) (when callback (apply callback cbargs))) (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)) (provide 'ein-contents-api)