emacs-jupyter/jupyter-base.el

674 lines
26 KiB
EmacsLisp
Raw Normal View History

2018-01-08 21:38:32 -06:00
;;; jupyter-base.el --- Core definitions for Jupyter -*- lexical-binding: t -*-
;; Copyright (C) 2018 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 06 Jan 2018
2019-03-14 09:08:31 -05:00
;; Version: 0.7.3
2018-01-08 21:38:32 -06:00
;; Keywords: jupyter literate-programming
;; This program 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 2, or (at
;; your option) any later version.
;; This program 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 GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; This file holds the core requires, variables, and type definitions necessary
;; for jupyter.
;;; Code:
2019-03-10 19:53:54 -05:00
(eval-when-compile (require 'subr-x))
(require 'cl-lib)
(require 'eieio)
2018-05-20 12:09:00 -05:00
(require 'eieio-base)
(require 'json)
(require 'zmq)
(require 'hmac-def)
2018-01-08 21:38:32 -06:00
(require 'jupyter-kernelspec)
(declare-function tramp-dissect-file-name "tramp" (name &optional nodefault))
(declare-function tramp-file-name-user "tramp")
(declare-function tramp-file-name-host "tramp")
2018-11-30 21:24:07 -06:00
(declare-function jupyter-message-content "jupyter-messages" (msg))
;;; Call the Jupyter command
(defun jupyter-command (&rest args)
"Run a Jupyter shell command synchronously, return its output.
The shell command run is
jupyter ARGS..."
(with-temp-buffer
(apply #'process-file "jupyter" nil t nil args)
(string-trim-right (buffer-string))))
;;; Custom variables
(defcustom jupyter-include-other-output nil
"Whether or not to handle IOPub messages from other clients.
A Jupyter client can receive messages from other clients
connected to the same kernel on the IOPub channel. You can choose
to ignore these messages by setting
`jupyter-include-other-output' to nil. If
`jupyter-include-other-output' is non-nil, then any messages that
are not associated with a request from a client are sent to the
client's handler methods with a nil value for the request
argument. To change the value of this variable for a particular
client use `jupyter-set'."
:group 'jupyter
:type 'boolean)
(defcustom jupyter-iopub-message-hook nil
2019-03-02 17:44:57 -06:00
"Hook run when a message is received on the IOPub channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly, use
`jupyter-add-hook'. If any of the message hooks return a non-nil
value, the client handlers will be prevented from running for the
message."
:group 'jupyter
:type 'hook)
(put 'jupyter-iopub-message-hook 'permanent-local t)
(defcustom jupyter-shell-message-hook nil
2019-03-02 17:44:57 -06:00
"Hook run when a message is received on the SHELL channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly, use
`jupyter-add-hook'. If any of the message hooks return a non-nil
value, the client handlers will be prevented from running for the
message."
:group 'jupyter
:type 'hook)
(put 'jupyter-shell-message-hook 'permanent-local t)
(defcustom jupyter-stdin-message-hook nil
2019-03-02 17:44:57 -06:00
"Hook run when a message is received on the STDIN channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly,
use `jupyter-add-hook'. If any of the message hooks return a
non-nil value, the client handlers will be prevented from running
for the message."
:group 'jupyter
:type 'hook)
(put 'jupyter-stdin-message-hook 'permanent-local t)
(defcustom jupyter-runtime-directory (jupyter-command "--runtime-dir")
"The Jupyter runtime directory.
2018-01-23 13:44:12 -06:00
When a new kernel is started through `jupyter-start-kernel', this
directory is where kernel connection files are written to."
:group 'jupyter
:type 'string)
2019-02-20 23:58:29 -06:00
(defcustom jupyter-pop-up-frame nil
"Whether or not buffers should be displayed in a new frame by default.
Note, this variable is only considered when evaluating code
interactively with functions like `jupyter-eval-line-or-region'.
If equal to nil, frames will never be popped up. When equal to t,
pop-up frames instead of windows.
`jupyter-pop-up-frame' can also be a list of message type
keywords for messages which will cause frames to be used. For any
message type not in the list, windows will be used instead.
Currently only `:execute-result', `:error', and `:stream'
messages consider this variable."
:group 'jupyter
:type '(choice (const :tag "Pop up frames" t)
(const :tag "Pop up windows" nil)
;; TODO: These are the only ones where `jupyter-pop-up-frame'
;; is checked at the moment.
(set (const :execute-result)
(const :error)
(const :stream))))
2018-05-20 12:09:00 -05:00
(defconst jupyter-root (file-name-directory load-file-name)
"Root directory containing emacs-jupyter.")
(defconst jupyter-protocol-version "5.3"
"The jupyter protocol version that is implemented.")
(defconst jupyter-socket-types
(list :hb zmq-REQ
:shell zmq-DEALER
:iopub zmq-SUB
:stdin zmq-DEALER
:control zmq-DEALER)
"The socket types for the various channels used by `jupyter'.")
(defconst jupyter-message-types
(list :execute-result "execute_result"
:execute-request "execute_request"
:execute-reply "execute_reply"
:inspect-request "inspect_request"
:inspect-reply "inspect_reply"
:complete-request "complete_request"
:complete-reply "complete_reply"
:history-request "history_request"
:history-reply "history_reply"
:is-complete-request "is_complete_request"
:is-complete-reply "is_complete_reply"
:comm-info-request "comm_info_request"
:comm-info-reply "comm_info_reply"
2018-05-20 12:09:00 -05:00
:comm-open "comm_open"
:comm-msg "comm_msg"
:comm-close "comm_close"
:kernel-info-request "kernel_info_request"
:kernel-info-reply "kernel_info_reply"
:shutdown-request "shutdown_request"
:shutdown-reply "shutdown_reply"
:interupt-request "interrupt_request"
:interrupt-reply "interrupt_reply"
:stream "stream"
:display-data "display_data"
:update-display-data "update_display_data"
:execute-input "execute_input"
:error "error"
:status "status"
:clear-output "clear_output"
:input-reply "input_reply"
:input-request "input_request")
"A plist mapping keywords to Jupyter message type strings.
The plist values are the message types either sent or received
from the kernel.")
Implement `jupyter-insert` method The goal of this method is to act as a single entry point for insertion of kernel results in any context. One would simply add another method to handle a specific context. * jupyter-base.el (jupyter-mime-types): (jupyter-nongraphic-mime-types): New variables that give mime-types that can be handled. (jupyter-insert): New method for dispatching to code that inserts mimetype representations in the current buffer. * jupyter-mime.el: New file. (jupyter-display-ids): (jupyter-handle-control-codes): (jupyter-fontify-buffers): (jupyter-get-fontify-buffer): (jupyter-fixup-font-lock-properties): (jupyter-add-font-lock-properties): (jupyter-fontify-according-to-mode): (jupyter-insert-html): (jupyter-markdown-mouse-map): (juputer-markdown-follow-link-at-point): (jupyter-insert-markdown): (jupyter-insert-latex): (jupyter-insert-ansi-coded-text): Moved from jupyter-repl.el, replaced `jupyter-repl-` prefix with `jupyter-`. (jupyter--shr-put-image): Ditto. Also add `shr-` prefix. (jupyter--delete-javascript-tags): Ditto. Also mark as private functions. (jupyter-insert-image): Ditto. Also mark as a public function. (jupyter-insert): (DISPLAY-ID ...) Moved from jupyter-repl.el. Was `jupyter-repl-insert-data-with-id`. (jupyter-with-control-code-handling): (jupyter-markdown-follow-link): Moved from jupyter-repl.el (jupyter-insert): Implement methods to do the work previously done by `jupyter-repl-insert-data`. * jupyter-repl.el (jupyter-repl-graphic-mimetypes): Moved to jupyter-base.el, inverted and renamed to `jupyter-nongraphic-mime-types`. (jupyter-repl-graphic-data-p): Remove unused function. (jupyter-repl-insert-data): Remove, replace calls with `jupyter-insert`. (jupyter-repl-add-font-lock-properties): (jupyter-repl-fixup-font-lock-properties): (jupyter-repl-get-fontify-buffer): (jupyter-repl-fontify-according-to-mode): (jupyter-repl-delete-javascript-tags): (jupyter-repl-put-image): (jupyter-repl-insert-html): (jupyter-repl-markdown-mouse-map): (jupyter-repl-markdown-follow-link-at-point): (jupyter-repl-insert-markdown): (jupyter-repl-insert-latex): (jupyter-repl--insert-image): Moved to jupyter-mime.el, which see. (jupyter-repl-insert-data-with-id): Ditto. Changed to a `jupyter-insert` method dispatched on a string argument. (jupyter-repl-insert-ansi-coded-text): Ditto. Replace calls with `jupyter-insert-ansi-coded-text`. (jupyter-with-control-code-handling): (jupyter-markdown-follow-link): Moved to jupyter-mime.el. * jupyter-org-client.el (jupyter-handle-error): Replace `jupyter-repl-insert-ansi-coded-text` with `jupyter-insert-ansi-coded-text`. * jupyter-tests.el (jupyter-insert): Add tests for `jupyter-insert`
2018-11-09 12:20:38 -06:00
(defconst jupyter-mime-types '(:application/vnd.jupyter.widget-view+json
:text/html :text/markdown
:image/svg+xml :image/jpeg :image/png
:text/latex :text/plain)
"MIME types handled by Jupyter.")
(defconst jupyter-nongraphic-mime-types '(:application/vnd.jupyter.widget-view+json
:text/html :text/markdown
:text/plain)
"MIME types that can be used in terminal Emacs.")
(defvar jupyter--debug nil
"When non-nil, some parts of Jupyter will emit debug statements.")
(defvar jupyter-default-timeout 2.5
"The default timeout in seconds for `jupyter-wait-until'.")
(defvar jupyter-long-timeout 10
2019-03-02 18:01:22 -06:00
"A longer timeout than `jupyter-default-timeout' used for some operations.
A longer timeout is needed, for example, when retrieving the
`jupyter-kernel-info' to allow for the kernel to startup.")
;;; Macros
(defmacro jupyter-with-timeout (spec &rest wait-forms)
"Periodically evaluate WAIT-FORMS until timeout.
Or until WAIT-FORMS evaluates to a non-nil value.
Wait until timeout SECONDS, periodically evaluating WAIT-FORMS
until it returns non-nil. If WAIT-FORMS returns non-nil, stop
waiting and return its value. Otherwise if timeout SECONDS
elapses, evaluate TIMEOUT-FORMS and return its value.
2018-12-06 00:39:39 -06:00
If PROGRESS is non-nil and evaluates to a string, a progress
reporter will be used with PROGRESS as the message while waiting.
SPEC takes the form (PROGRESS SECONDS TIMEOUT-FORMS...).
\(fn (PROGRESS SECONDS TIMEOUT-FORMS...) WAIT-FORMS...)"
(declare (indent 1) (debug ((form form body) body)))
(let ((res (make-symbol "res"))
(prog (make-symbol "prog"))
(prog-msg (make-symbol "prog-msg"))
(timeout (make-symbol "timeout"))
(wait-time (make-symbol "wait-time")))
`(let* ((,res nil)
(,prog-msg ,(pop spec))
(,timeout ,(pop spec))
(,wait-time (/ ,timeout 10.0))
(,prog (and (stringp ,prog-msg)
(make-progress-reporter ,prog-msg))))
(with-timeout (,timeout ,@spec)
(while (not (setq ,res (progn ,@wait-forms)))
(accept-process-output nil ,wait-time)
(when ,prog (progress-reporter-update ,prog))))
(prog1 ,res
(when ,prog (progress-reporter-done ,prog))))))
(defmacro jupyter-with-insertion-bounds (beg end bodyform &rest afterforms)
"Bind BEG and END to `point-marker's, evaluate BODYFORM then AFTERFORMS.
The END marker will advance if BODYFORM inserts text in the
current buffer. Thus after BODYFORM is evaluated, AFTERFORMS will
have access to the bounds of the text inserted by BODYFORM in the
2018-12-06 00:39:39 -06:00
variables BEG and END. The result of evaluating BODYFORM is
returned."
(declare (indent 3) (debug (symbolp symbolp form body)))
`(let ((,beg (point-marker))
(,end (point-marker)))
(set-marker-insertion-type end t)
(unwind-protect
(prog1 ,bodyform ,@afterforms)
(set-marker ,beg nil)
(set-marker ,end nil))))
(defmacro jupyter-loop-over-mime (mime-order mime data metadata &rest bodyforms)
"Loop over MIME types in MIME-ORDER.
MIME-ORDER should evaluate to a list of MIME types to loop over.
MIME will be bound to the MIME type for the current iteration.
DATA and METADATA are variables that hold the property list of
MIME data to loop over and any associated metadata, respectively.
Evaluate BODYFORMS with DATA and METADATA temporarily bound to
the data and metadata of the MIME type for the current iteration.
If BODYFORMS returns non-nil, return its value. Otherwise loop
over the next MIME type in MIME-ORDER that has a non-nil value in
the DATA property list."
(declare (indent 4) (debug ([&or form symbolp listp]
symbolp symbolp symbolp body)))
`(cl-loop
for ,mime in ,mime-order
thereis (let ((,data (plist-get ,data ,mime))
(,metadata (plist-get ,metadata ,mime)))
(when ,data ,@bodyforms))))
;;;; Display buffers
(defvar-local jupyter-display-buffer-marker nil
"The marker to store the last output position of an output buffer.
See `jupyter-with-display-buffer'.")
(defvar-local jupyter-display-buffer-request-id nil
"The last `jupyter-request' message ID that generated output.")
(defun jupyter-get-buffer-create (name)
"Return a buffer with some special properties.
- The buffer's name is based on NAME, specifically it will be
\"*jupyter-NAME*\"
- Its `major-mode' will be `special-mode'."
(let* ((bname (format "*jupyter-%s*" name))
(buffer (get-buffer bname)))
(unless buffer
(setq buffer (get-buffer-create bname))
(with-current-buffer buffer
(unless (eq major-mode 'special-mode)
(special-mode))))
buffer))
(defun jupyter--reset-display-buffer-p (arg)
"Return non-nil if the current output buffer should be reset.
If ARG is a `jupyter-request', reset the buffer if ARG's
`jupyter-request-id' is no equal to the
`jupyter-buffer-last-request-id'. If ARG is not a
`jupyter-request-id', return ARG."
(if (jupyter-request-p arg)
;; Reset the output buffer is the last request ID does not
;; match the current request's ID.
(let ((id (jupyter-request-id arg)))
(and (not (equal id jupyter-display-buffer-request-id))
(setq jupyter-display-buffer-request-id id)
t))
;; Otherwise reset the output buffer if RESET evaluates to a
;; non-nil value
arg))
(defmacro jupyter-with-display-buffer (name reset &rest body)
"In a buffer with a name derived from NAME current, evaluate BODY.
The buffer's name is obtained by a call to
`jupyter-get-buffer-create'. A display buffer is similar to a
*Help* buffer, but maintains its previous output on subsequent
invocations that use the same NAME. Before BODY is evaluated,
`point' is moved to the end of the most recent output. Also note,
BODY is wrapped using `jupyter-with-control-code-handling'.
RESET is a form or symbol that determines if the buffer should be
erased before evaluating BODY. If RESET is nil, no erasing of the
buffer is ever performed. If RESET evaluates to a
`jupyter-request' object, reset the buffer if the previous
request that generated output in the buffer is not the same
request. Otherwise if RESET evaluates to any non-nil value, reset
the output buffer."
(declare (indent 2) (debug (stringp [&or atom form] body)))
(let ((buffer (make-symbol "buffer")))
`(let ((,buffer (jupyter-get-buffer-create ,name)))
(setq other-window-scroll-buffer ,buffer)
(with-current-buffer ,buffer
(unless jupyter-display-buffer-marker
(setq jupyter-display-buffer-marker (point-max-marker))
(set-marker-insertion-type jupyter-display-buffer-marker t))
(let ((inhibit-read-only t))
(when (jupyter--reset-display-buffer-p ,reset)
(erase-buffer)
(set-marker jupyter-display-buffer-marker (point)))
(goto-char jupyter-display-buffer-marker)
(jupyter-with-control-code-handling ,@body))))))
2019-02-20 23:58:29 -06:00
(defun jupyter-display-current-buffer-reuse-window (&optional msg-type alist &rest actions)
"Convenience function to call `display-buffer' on the `current-buffer'.
If a window showing the current buffer is already available,
2019-02-20 23:58:29 -06:00
re-use it.
If ALIST is non-nil it is used as the ACTION alist of
`display-buffer'.
If MSG-TYPE is specified, it should be one of the keywords in
`jupyter-message-types' and is used in setting `pop-up-frames'
and `pop-up-windows'. See `jupyter-pop-up-frame'.
The rest of the arguments are display ACTIONS tried after
attempting to re-use a window and before attempting to pop-up a
new window or frame."
(let* ((jupyter-pop-up-frame (jupyter-pop-up-frame-p msg-type))
(pop-up-frames (and jupyter-pop-up-frame 'graphic-only))
(pop-up-windows (not jupyter-pop-up-frame))
(display-buffer-base-action
(cons
(append '(display-buffer-reuse-window)
(delq nil actions))
alist)))
(display-buffer (current-buffer))))
2019-02-20 23:58:29 -06:00
(defun jupyter-pop-up-frame-p (msg-type)
"Return non-nil if a frame should be popped up for MSG-TYPE."
(or (eq jupyter-pop-up-frame t)
(memq msg-type jupyter-pop-up-frame)))
;;; Signing functions/UUID
(defun jupyter-sha256 (object)
"Return the SHA256 hash of OBJECT."
(secure-hash 'sha256 object nil nil t))
(define-hmac-function jupyter-hmac-sha256 jupyter-sha256 64 32)
(defun jupyter-new-uuid ()
2018-12-28 12:49:50 -06:00
"Return a version 4 UUID."
(format "%04x%04x-%04x-%04x-%04x-%06x%06x"
(cl-random 65536)
(cl-random 65536)
(cl-random 65536)
;; https://tools.ietf.org/html/rfc4122
(let ((r (cl-random 65536)))
(if (= (byteorder) ?l)
;; ?l = little-endian
(logior (logand r 4095) 16384)
;; big-endian
(logior (logand r 65295) 64)))
(let ((r (cl-random 65536)))
(if (= (byteorder) ?l)
(logior (logand r 49151) 32768)
(logior (logand r 65471) 128)))
(cl-random 16777216)
(cl-random 16777216)))
(defclass jupyter-instance-tracker ()
((tracking-symbol :type symbol))
:documentation "Similar to `eieio-instance-tracker', but keeping weak references.
To access all the objects in TRACKING-SYMBOL, use
`jupyter-all-objects'."
:abstract t)
(cl-defmethod initialize-instance ((obj jupyter-instance-tracker) &rest _)
(cl-call-next-method)
(let ((sym (oref obj tracking-symbol)))
(unless (hash-table-p (symbol-value sym))
(put sym 'jupyter-instance-tracker t)
(set sym (make-hash-table :weakness 'key)))
(puthash obj t (symbol-value sym))))
(defun jupyter-all-objects (sym)
"Return all tracked objects in tracking SYM.
2019-02-07 13:44:14 -06:00
SYM is a symbol used for tracking objects that inherit from the
class corresponding to the symbol `jupyter-instance-tracker'."
(let ((table (symbol-value sym)))
(when (hash-table-p table)
(cl-assert (get sym 'jupyter-instance-tracker) t)
(hash-table-keys table))))
(defclass jupyter-finalized-object ()
((finalizers :type list :initform nil))
:documentation "A list of finalizers."
:documentation "A base class for cleaning up resources.
Adds the method `jupyter-add-finalizer' which maintains a list of
finalizer functions to be called when the object is garbage
collected.")
(cl-defgeneric jupyter-add-finalizer ((obj jupyter-finalized-object) finalizer)
"Cleanup resources automatically.
FINALIZER if a function to be added to a list of finalizers that
will be called when OBJ is garbage collected."
(declare (indent 1))
(cl-check-type finalizer function)
(push (make-finalizer finalizer) (oref obj finalizers)))
;;; Session object definition
(cl-defstruct (jupyter-session
(:constructor nil)
(:constructor
jupyter-session
(&key
(conn-info nil)
(id (jupyter-new-uuid))
(key nil))))
"A `jupyter-session' holds the information needed to
2018-05-26 20:04:02 -05:00
authenticate messages. A `jupyter-session' contains the following
fields:
- CONN-INFO :: The connection info. property list of the kernel
this session is used to sign messages for.
- ID :: A string of bytes used as the `zmq-ROUTING-ID' for every
`jupyter-channel' that utilizes the sessiong object.
- KEY :: The key used when signing messages. If KEY is nil,
message signing is not performed."
(conn-info nil :read-only t)
(id nil :read-only t)
(key nil :read-only t))
;;; Request object definition
(cl-defstruct (jupyter-request
(:constructor nil)
(:constructor jupyter-request))
"A `jupyter-request' encapsulates the current status of a
request to a kernel. A `jupyter-request' consists of the
following fields:
- ID :: A UUID to match a `jupyter-request' to the received
messages of a kernel.
- TIME :: The time at which the request was made.
- IDLE-RECEIVED-P :: A flag variable that is set to t when a
`jupyter-kernel-client' has received the
status: idle message for the request.
2018-05-20 12:09:00 -05:00
- LAST-MESSAGE :: The raw message property list of the last
message received by the kernel in response to
this request.
- INHIBITED-HANDLERS :: A list of handler message types to
prevent the running of that particular
handler. If set to t, disable all
handlers for this request. Note this
should not be set directly, dynamically
bind `jupyter-inhibit-handlers' before
making the request.
- CALLBACKS :: An alist mapping message types to their
corresponding callbacks. This alist is modified
through calls to `jupyter-add-callback' on the request."
(id "")
(time (current-time))
(idle-received-p nil)
2018-05-20 12:09:00 -05:00
(last-message nil)
(inhibited-handlers nil)
(callbacks))
;;; Connecting to a kernel's channels
2019-02-18 10:03:18 -06:00
(eval-when-compile (require 'tramp))
2018-05-15 19:25:03 -05:00
(defun jupyter-tunnel-connection (conn-file &optional server)
"Forward local ports to the remote ports in CONN-FILE.
CONN-FILE is the path to a Jupyter connection file, SERVER is the
host that the kernel connection in CONN-FILE is located. Return a
copy of the connection plist in CONN-FILE, but with the ports
replaced by the local ports used for the forwarding.
2019-02-17 23:06:00 -06:00
If CONN-FILE is a `tramp' file name, the SERVER argument will be
2018-05-15 19:25:03 -05:00
ignored and the host will be extracted from the information
contained in the file name.
Note that `zmq-make-tunnel' is used to create the tunnels."
(catch 'no-tunnels
(let ((conn-info (jupyter-read-plist conn-file)))
(when (and (file-remote-p conn-file)
(functionp 'tramp-dissect-file-name))
(pcase-let (((cl-struct tramp-file-name method user host)
(tramp-dissect-file-name conn-file)))
(pcase method
("docker"
;; Assume docker is using the -p argument to publish its exposed
;; ports to the localhost. The ports used in the container should
;; be the same ports accessible on the local host. For example, if
;; the shell port is on 1234 in the container, the published port
;; flag should be "-p 1234:1234".
(throw 'no-tunnels conn-info))
(_
(setq server (if user (concat user "@" host)
host))))))
(let ((sock (zmq-socket (zmq-current-context) zmq-REP)))
(unwind-protect
(cl-loop
with remoteip = (plist-get conn-info :ip)
for (key maybe-rport) on conn-info by #'cddr
collect key and if (memq key '(:hb_port :shell_port :control_port
:stdin_port :iopub_port))
collect (let ((lport (zmq-bind-to-random-port sock "tcp://127.0.0.1")))
(zmq-unbind sock (zmq-socket-get sock zmq-LAST-ENDPOINT))
(prog1 lport
(zmq-make-tunnel lport maybe-rport server remoteip)))
else collect maybe-rport)
(zmq-socket-set sock zmq-LINGER 0)
(zmq-close sock))))))
2018-05-15 19:25:03 -05:00
(cl-defun jupyter-create-connection-info (&key
(kernel-name "python")
(transport "tcp")
(ip "127.0.0.1")
(signature-scheme "hmac-sha256")
(key (jupyter-new-uuid))
(hb-port 0)
(stdin-port 0)
(control-port 0)
(shell-port 0)
(iopub-port 0))
"Create a connection info plist used to connect to a kernel.
The plist has the standard keys found in the jupyter spec. See
http://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files.
A port number of 0 for a channel means to use a randomly assigned
port for that channel."
(unless (or (= (length key) 0)
(equal signature-scheme "hmac-sha256"))
(error "Only hmac-sha256 signing is currently supported"))
(append
(list :kernel_name kernel-name
:transport transport
:ip ip)
(when (> (length key) 0)
(list :signature_scheme signature-scheme
:key key))
(cl-loop
with sock = (zmq-socket (zmq-current-context) zmq-REP)
with addr = (concat transport "://" ip)
for (channel . port) in `((:hb_port . ,hb-port)
(:stdin_port . ,stdin-port)
(:control_port . ,control-port)
(:shell_port . ,shell-port)
(:iopub_port . ,iopub-port))
collect channel and
if (= port 0) do (setq port (zmq-bind-to-random-port sock addr))
2018-05-15 17:48:40 -05:00
and collect port else collect port
finally
(zmq-socket-set sock zmq-LINGER 0)
(zmq-close sock))))
(defun jupyter-connect-endpoint (type endpoint &optional identity)
"Create socket with TYPE and connect to ENDPOINT.
If IDENTITY is non-nil, it will be set as the ROUTING-ID of the
socket. Return the created socket."
(let ((sock (zmq-socket (zmq-current-context) type)))
(prog1 sock
(zmq-socket-set sock zmq-LINGER 1000)
(when identity
(zmq-socket-set sock zmq-ROUTING-ID identity))
(zmq-connect sock endpoint))))
(defun jupyter-connect-channel (ctype endpoint &optional identity)
"Create a socket based on a Jupyter channel type.
CTYPE is one of the symbols `:hb', `:stdin', `:shell',
`:control', or `:iopub' and represents the type of channel to
connect to ENDPOINT. If IDENTITY is non-nil, it will be set as
the ROUTING-ID of the socket. Return the created socket."
(let ((sock-type (plist-get jupyter-socket-types ctype)))
(unless sock-type
(error "Invalid channel type (%s)" ctype))
(jupyter-connect-endpoint sock-type endpoint identity)))
2018-05-15 17:53:32 -05:00
;;; Helper functions
(defun jupyter-read-plist (file)
"Read a JSON encoded FILE as a property list."
(let ((json-object-type 'plist))
2018-05-15 17:53:32 -05:00
(json-read-file file)))
2018-05-20 12:09:00 -05:00
(defun jupyter-read-plist-from-string (string)
"Read a property list from a JSON encoded STRING."
(let ((json-object-type 'plist))
2018-05-20 12:09:00 -05:00
(json-read-from-string string)))
2018-11-30 21:24:07 -06:00
(defun jupyter-normalize-data (plist &optional metadata)
"Return a list (DATA META) from PLIST.
DATA is a property list of mimetype data extracted from PLIST. If
PLIST is a message plist, then DATA will be the value of the
:data key in the messages contents. If PLIST is not a message
plist, then DATA is either the :data key of PLIST or PLIST
itself.
A similar extraction process is performed for the :metadata key
of PLIST which will be the META argument in the return value. If
no :metadata key can be found, then META will be METADATA."
(list
(or
;; Allow for passing message plists
(plist-get (jupyter-message-content plist) :data)
;; Allow for passing (jupyter-message-content msg)
(plist-get plist :data)
;; Otherwise assume the plist contains mimetypes
plist)
(or (plist-get (jupyter-message-content plist) :metadata)
(plist-get plist :metadata)
metadata)))
(provide 'jupyter-base)
2018-01-08 21:38:32 -06:00
;;; jupyter-base.el ends here