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.

433 lines
18 KiB

  1. ;; -*- lexical-binding: t -*-
  2. ;;; ob-ein.el --- org-babel functions for template evaluation
  3. ;; Copyright (C) John M. Miller
  4. ;; Author: John M. Miller <millejoh at mac.com>
  5. ;;
  6. ;;; License:
  7. ;; This file is NOT part of GNU Emacs.
  8. ;; ob-ein.el is free software: you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;; ob-ein.el is distributed in the hope that it will be useful,
  13. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;; GNU General Public License for more details.
  16. ;; You should have received a copy of the GNU General Public License
  17. ;; along with ob-ein.el. If not, see <http://www.gnu.org/licenses/>.
  18. ;;; Commentary:
  19. ;; Support executing org-babel source blocks using EIN worksheets.
  20. ;;; Credits:
  21. ;; Uses code from https://github.com/gregsexton/ob-ipython (MIT License)
  22. ;;; Code:
  23. (require 'ob)
  24. (require 'ein-utils)
  25. (autoload 'org-element-property "org-element")
  26. (autoload 'org-element-context "org-element")
  27. (autoload 'ein:notebooklist-new-notebook-with-name "ein-notebooklist")
  28. (autoload 'ein:notebooklist-login "ein-notebooklist")
  29. (autoload 'ein:notebook-get-opened-notebook "ein-notebook")
  30. (autoload 'ein:notebook-url "ein-notebook")
  31. (autoload 'ein:notebook-open "ein-notebook")
  32. (autoload 'ein:notebook-close "ein-notebook")
  33. (autoload 'ein:process-url-or-port "ein-process")
  34. (autoload 'ein:process-url-match "ein-process")
  35. (autoload 'ein:process-refresh-processes "ein-process")
  36. (autoload 'ein:jupyter-server-conn-info "ein-jupyter")
  37. (autoload 'ein:jupyter-server-start "ein-jupyter")
  38. (autoload 'ein:connect-buffer-to-notebook "ein-connect")
  39. (autoload 'ein:connect-run-buffer "ein-connect")
  40. (autoload 'ein:shared-output-get-cell "ein-shared-output")
  41. (autoload 'ein:shared-output-eval-string "ein-shared-output")
  42. (autoload 'ein:kernel-live-p "ein-kernel")
  43. (autoload 'ein:query-singleton-ajax "ein:query")
  44. (defvar *ob-ein-sentinel* "[....]"
  45. "Placeholder string replaced after async cell execution")
  46. (defcustom ob-ein-timeout-seconds 600
  47. "Maximum seconds to wait for block to finish (for synchronous operations)."
  48. :type 'integer
  49. :group 'ein)
  50. (defcustom ob-ein-languages
  51. '(("ein" . python)
  52. ("ein-python" . python)
  53. ("ein-R" . R)
  54. ("ein-r" . R)
  55. ("ein-julia" . julia)
  56. ("ein-hy" . hy)
  57. ("ein-c" . c)
  58. ("ein-C++11" . c++)
  59. ("ein-C++14" . c++)
  60. ("ein-C++17" . c++))
  61. "ob-ein has knowledge of these (ein-LANG . LANG-MODE) pairs."
  62. :type '(repeat (cons string symbol))
  63. :group 'ein)
  64. (defcustom ob-ein-anonymous-path ".%s.ipynb"
  65. "When session header doesn't specify ipynb, prosecute all interactions for a given language in this throwaway notebook (substitute %s with language)."
  66. :type '(string)
  67. :group 'ein)
  68. (defsubst ob-ein-anonymous-p (path)
  69. "Return t if PATH looks like ob-ein-anonymous-path. Fragile"
  70. (string-match (replace-regexp-in-string "%s" ".+"
  71. (replace-regexp-in-string "\\." "\\\\." ob-ein-anonymous-path))
  72. path))
  73. (defcustom ob-ein-inline-image-directory "ein-images"
  74. "Store ob-ein images here."
  75. :group 'ein
  76. :type '(directory))
  77. (defcustom ob-ein-default-header-args:ein nil
  78. "No documentation."
  79. :group 'ein
  80. :type '(repeat string))
  81. (defun ob-ein--inline-image-info (value)
  82. (let* ((f (md5 value))
  83. (d ob-ein-inline-image-directory)
  84. (tf (concat d "/ob-ein-" f ".png")))
  85. (unless (file-directory-p d)
  86. (make-directory d 'parents))
  87. tf))
  88. (defun ob-ein--write-base64-image (img-string file)
  89. (with-temp-file file
  90. (let ((buffer-read-only nil)
  91. (buffer-file-coding-system 'binary)
  92. (require-final-newline nil)
  93. (file-precious-flag t))
  94. (insert img-string)
  95. (base64-decode-region (point-min) (point-max)))))
  96. (defun ob-ein--return-mime-type (json file)
  97. (cl-loop
  98. for key in ein:output-types-text-preferred
  99. for type = (intern (format ":%s" key)) ; something like `:text'
  100. for value = (plist-get json type) ; FIXME: optimize
  101. when (plist-member json type)
  102. return
  103. (cl-case key
  104. ((svg image/svg)
  105. (let ((file (or file (ob-ein--inline-image-info value))))
  106. (ob-ein--write-base64-image value file)
  107. (format "[[file:%s]]" file)))
  108. ((png image/png jpeg image/jpeg)
  109. (let ((file (or file (ob-ein--inline-image-info value))))
  110. (ob-ein--write-base64-image value file)
  111. (format "[[file:%s]]" file)))
  112. (t (plist-get json type)))))
  113. (defun ob-ein--process-outputs (outputs params)
  114. (let ((file (cdr (assoc :image params))))
  115. (ein:join-str "\n"
  116. (cl-loop for o in outputs
  117. collecting (ob-ein--return-mime-type o file)))))
  118. (defun ob-ein--get-name-create (src-block-info)
  119. "Get the name of a src block or add a uuid as the name."
  120. (if-let ((name (cl-fifth src-block-info)))
  121. name
  122. (save-excursion
  123. (let ((el (org-element-context))
  124. (id (org-id-new 'none)))
  125. (goto-char (org-element-property :begin el))
  126. (back-to-indentation)
  127. (split-line)
  128. (insert (format "#+NAME: %s" id))
  129. id))))
  130. (defun ob-ein--babelize-lang (lang-name lang-mode)
  131. "Stand-up LANG-NAME as a babelized language with LANG-MODE syntax table.
  132. Based on ob-ipython--configure-kernel."
  133. (add-to-list 'org-src-lang-modes `(,lang-name . ,lang-mode))
  134. (defvaralias (intern (concat "org-babel-default-header-args:" lang-name))
  135. 'ob-ein-default-header-args:ein)
  136. (fset (intern (concat "org-babel-execute:" lang-name))
  137. `(lambda (body params)
  138. (require (quote ,(intern (format "ob-%s" lang-mode))) nil t)
  139. (if (boundp 'python-indent-guess-indent-offset-verbose)
  140. (setq python-indent-guess-indent-offset-verbose nil))
  141. (let* ((parser
  142. (quote
  143. ,(intern
  144. (format "org-babel-variable-assignments:%s" lang-mode))))
  145. (assignments (if (fboundp parser)
  146. (funcall (symbol-function parser) params)
  147. (ein:log 'verbose "%s: No suitable ob-%s module"
  148. (concat "org-babel-execute:" ,lang-name)
  149. (quote ,lang-mode))
  150. nil)))
  151. (ob-ein--execute-body body params assignments)))))
  152. (defun ob-ein--execute-body (body params assignments)
  153. (let* ((buffer (current-buffer))
  154. (processed-params (org-babel-process-params params))
  155. (result-params (cdr (assq :result-params params)))
  156. (session (or (ein:aand (cdr (assoc :session processed-params))
  157. (unless (string= "none" it)
  158. (format "%s" it)))
  159. ein:url-localhost))
  160. (lang (nth 0 (org-babel-get-src-block-info)))
  161. (kernelspec (or (cdr (assoc :kernelspec processed-params))
  162. (ein:aif (cdr (assoc lang org-src-lang-modes))
  163. (cons 'language (format "%s" it))
  164. (error "ob-ein--execute-body: %s not among %s"
  165. lang (mapcar #'car org-src-lang-modes)))))
  166. (name (ob-ein--get-name-create (org-babel-get-src-block-info)))
  167. (full-body (org-babel-expand-body:generic
  168. (encode-coding-string body 'utf-8)
  169. params
  170. assignments))
  171. (callback (lambda (notebook)
  172. (ob-ein--execute-async
  173. buffer
  174. full-body
  175. (ein:$notebook-kernel notebook)
  176. processed-params
  177. result-params
  178. name))))
  179. (save-excursion
  180. (cl-assert (not (stringp (org-babel-goto-named-src-block name))))
  181. (org-babel-insert-result *ob-ein-sentinel* result-params))
  182. (ob-ein--initiate-session session kernelspec callback)
  183. (if (ein:eval-if-bound 'org-current-export-file)
  184. (save-excursion
  185. (cl-loop with interval = 2000
  186. with pending = t
  187. repeat (/ (* ob-ein-timeout-seconds 1000) interval)
  188. do (progn
  189. (org-babel-goto-named-result name)
  190. (forward-line 1)
  191. (setq pending (re-search-forward
  192. (regexp-quote *ob-ein-sentinel*)
  193. (org-babel-result-end) t)))
  194. until (not pending)
  195. do (sleep-for 0 interval)
  196. finally return
  197. (if pending
  198. (progn
  199. (ein:log 'error "ob-ein--execute-body: %s timed out" name)
  200. "")
  201. (ob-ein--process-outputs
  202. (ein:oref-safe (ein:shared-output-get-cell) 'outputs)
  203. processed-params))))
  204. (org-babel-remove-result)
  205. *ob-ein-sentinel*)))
  206. (defsubst ob-ein--execute-async-callback (buffer params result-params name)
  207. "Callback of 1-arity (the shared output cell) to update org buffer when
  208. `ein:shared-output-eval-string' completes."
  209. (apply-partially
  210. (lambda (buffer* params* result-params* name* cell)
  211. (let* ((raw (ein:aif (ein:oref-safe cell 'traceback)
  212. (ansi-color-apply (ein:join-str "\n" it))
  213. (ob-ein--process-outputs
  214. (ein:oref-safe cell 'outputs) params*)))
  215. (result
  216. (let ((tmp-file (org-babel-temp-file "ein-")))
  217. (with-temp-file tmp-file raw)
  218. (org-babel-result-cond result-params*
  219. raw (org-babel-import-elisp-from-file tmp-file '(16)))))
  220. (info (org-babel-get-src-block-info 'light)))
  221. (ein:log 'debug "ob-ein--execute-async-callback %s \"%s\" %s" name* result buffer*)
  222. (save-excursion
  223. (save-restriction
  224. (with-current-buffer buffer*
  225. (unless (stringp (org-babel-goto-named-src-block name*)) ;; stringp=error
  226. (when info ;; kill #+RESULTS: (no-name)
  227. (setf (nth 4 info) nil)
  228. (org-babel-remove-result info))
  229. (org-babel-remove-result) ;; kill #+RESULTS: name
  230. (org-babel-insert-result
  231. result
  232. (cdr (assoc :result-params
  233. (cl-third (org-babel-get-src-block-info)))))
  234. (org-redisplay-inline-images)))))))
  235. buffer params result-params name))
  236. (defun ob-ein--execute-async (buffer body kernel params result-params name)
  237. "As `ein:shared-output-get-cell' is a singleton, ob-ein can only execute blocks
  238. one at a time. Further, we do not order the queued up blocks!"
  239. (deferred:$
  240. (deferred:next
  241. (deferred:lambda ()
  242. (let ((cell (ein:shared-output-get-cell)))
  243. (if (eq (slot-value cell 'callback) #'ignore)
  244. (let ((callback
  245. (ob-ein--execute-async-callback buffer params
  246. result-params name)))
  247. (setf (slot-value cell 'callback) callback))
  248. ;; still pending previous callback
  249. (deferred:nextc (deferred:wait 1200) self)))))
  250. (deferred:nextc it
  251. (lambda (_x)
  252. (ein:shared-output-eval-string kernel body nil)))))
  253. (defun ob-ein--parse-session (session)
  254. (cl-multiple-value-bind (url-or-port _password) (ein:jupyter-server-conn-info)
  255. (let ((tokens (split-string session "/"))
  256. (parsed-url (url-generic-parse-url session)))
  257. (cond ((null (url-host parsed-url))
  258. (let* ((candidate (apply #'ein:url (car tokens) (cdr tokens)))
  259. (parsed-candidate (url-generic-parse-url candidate))
  260. (missing (url-scheme-get-property
  261. (url-type parsed-candidate)
  262. 'default-port)))
  263. (if (and url-or-port
  264. (= (url-port parsed-candidate) missing))
  265. (apply #'ein:url url-or-port (cdr tokens))
  266. candidate)))
  267. (t (ein:url session))))))
  268. (defun ob-ein--initiate-session (session kernelspec callback &optional babel-info)
  269. "Retrieve notebook based on SESSION path and KERNELSPEC, starting jupyter instance
  270. if necessary. Install CALLBACK (i.e., cell execution) upon notebook retrieval."
  271. (let* ((nbpath (ob-ein--parse-session session))
  272. (info (or (org-babel-get-src-block-info) babel-info))
  273. (anonymous-path (format ob-ein-anonymous-path (nth 0 info)))
  274. (parsed-url (url-generic-parse-url nbpath))
  275. (slash-path (car (url-path-and-query parsed-url)))
  276. (path (if (string= slash-path "") anonymous-path
  277. (substring slash-path 1)))
  278. (url-or-port (if (string= slash-path "")
  279. nbpath
  280. (substring nbpath 0 (- (length slash-path)))))
  281. (notebook (ein:notebook-get-opened-notebook url-or-port path))
  282. (callback-nbopen (lambda (nb _created)
  283. (cl-loop repeat 50
  284. for live-p = (ein:kernel-live-p (ein:$notebook-kernel nb))
  285. until live-p
  286. do (sleep-for 0 300)
  287. finally
  288. do (if (not live-p)
  289. (ein:log 'error
  290. "Kernel for %s failed to launch"
  291. (ein:$notebook-notebook-name nb))
  292. (funcall callback nb)))))
  293. (errback-nbopen (lambda (url-or-port status-code)
  294. (if (eq status-code 404)
  295. (ein:notebooklist-new-notebook-with-name
  296. url-or-port kernelspec path callback-nbopen t))))
  297. (callback-login (lambda (_buffer url-or-port)
  298. (ein:notebook-open url-or-port path kernelspec
  299. callback-nbopen errback-nbopen t))))
  300. (cond ((and notebook
  301. (string= path anonymous-path)
  302. (stringp kernelspec)
  303. (not (equal (ein:$kernelspec-name (ein:$notebook-kernelspec notebook))
  304. kernelspec)))
  305. (ein:log 'debug "ob-ein--initiate-session: switching %s from %s to %s"
  306. path (ein:$kernelspec-name (ein:$notebook-kernelspec notebook))
  307. kernelspec)
  308. (cl-letf (((symbol-function 'y-or-n-p) #'ignore))
  309. (ein:notebook-close notebook)
  310. (ein:query-singleton-ajax
  311. (list 'ob-ein--initiate-session (ein:url url-or-port path))
  312. (ein:notebook-url notebook)
  313. :type "DELETE"))
  314. (cl-loop repeat 8
  315. for extant = (file-exists-p path)
  316. until (not extant)
  317. do (sleep-for 0 500)
  318. finally do (if extant
  319. (ein:display-warning (format "cannot del %s" path))
  320. (ob-ein--initiate-session session kernelspec callback))))
  321. (notebook (funcall callback notebook))
  322. ((string= (url-host parsed-url) ein:url-localhost)
  323. (ein:process-refresh-processes)
  324. (ein:aif (ein:process-url-match nbpath)
  325. (ein:notebooklist-login (ein:process-url-or-port it) callback-login)
  326. (ein:jupyter-server-start
  327. (executable-find (or (ein:eval-if-bound 'ein:jupyter-default-server-command)
  328. "jupyter"))
  329. (read-directory-name "Notebook directory: " default-directory)
  330. nil
  331. callback-login
  332. (let* ((port (url-port parsed-url))
  333. (avoid (url-scheme-get-property (url-type parsed-url) 'default-port)))
  334. (cond ((= port avoid) nil)
  335. (t (url-port parsed-url)))))))
  336. (t (ein:notebooklist-login url-or-port callback-login)))))
  337. (defun ob-ein--edit-ctrl-c-ctrl-c ()
  338. "C-c C-c mapping in ein:connect-mode-map."
  339. (interactive)
  340. (if (not (org-src-edit-buffer-p))
  341. (ein:connect-run-buffer)
  342. (org-edit-src-save)
  343. (when (boundp 'org-src--beg-marker)
  344. (let* ((beg org-src--beg-marker)
  345. (buf (marker-buffer beg)))
  346. (with-current-buffer buf
  347. (save-excursion
  348. (goto-char beg)
  349. (org-ctrl-c-ctrl-c)))))))
  350. (defcustom ob-ein-babel-edit-polymode-ignore nil
  351. "When false override default python mode key mapping for `\C-c\C-c' while inside a babel edit buffer.
  352. Instead the binding will be to `ob-ein--edit-ctrl-c-ctrl-c', which will execute the code block being edited."
  353. :group 'ein
  354. :type '(boolean))
  355. (defun org-babel-edit-prep:ein (babel-info)
  356. (if (and ein:polymode (not ob-ein-babel-edit-polymode-ignore))
  357. (progn
  358. (use-local-map (copy-keymap python-mode-map))
  359. (local-set-key "\C-c\C-c" 'ob-ein--edit-ctrl-c-ctrl-c))
  360. (let* ((buffer (current-buffer))
  361. (processed-parameters (nth 2 babel-info))
  362. (session (or (ein:aand (cdr (assoc :session processed-parameters))
  363. (unless (string= "none" it)
  364. (format "%s" it)))
  365. ein:url-localhost))
  366. (lang (car babel-info))
  367. (kernelspec (or (cdr (assoc :kernelspec processed-parameters))
  368. (ein:aif (cdr (assoc lang org-src-lang-modes))
  369. (cons 'language (format "%s" it))
  370. (error "ob-ein--execute-body: %s not among %s"
  371. lang (mapcar #'car org-src-lang-modes))))))
  372. (ob-ein--initiate-session
  373. session
  374. kernelspec
  375. (lambda (notebook)
  376. (ein:connect-buffer-to-notebook notebook buffer t)
  377. (define-key ein:connect-mode-map "\C-c\C-c" 'ob-ein--edit-ctrl-c-ctrl-c))
  378. babel-info))))
  379. (defun org-babel-edit-prep:ein-python (babel-info)
  380. (org-babel-edit-prep:ein babel-info))
  381. (cl-loop for (lang . mode) in ob-ein-languages
  382. do (ob-ein--babelize-lang lang mode))
  383. ;;;###autoload
  384. (if (featurep 'org)
  385. (let* ((orig (get 'org-babel-load-languages 'custom-type))
  386. (orig-cdr (cdr orig))
  387. (choices (plist-get orig-cdr :key-type)))
  388. (push '(const :tag "Ein" ein) (nthcdr 1 choices))
  389. (put 'org-babel-load-languages 'custom-type
  390. (cons (car orig) (plist-put orig-cdr :key-type choices)))))
  391. (provide 'ob-ein)