jsonyter 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jsonyter/__init__.py +16 -0
- jsonyter/__main__.py +5 -0
- jsonyter/cli.py +429 -0
- jsonyter/client.py +250 -0
- jsonyter/kernel.py +421 -0
- jsonyter/messages.py +91 -0
- jsonyter/notebook.py +447 -0
- jsonyter-1.0.0.dist-info/METADATA +433 -0
- jsonyter-1.0.0.dist-info/RECORD +12 -0
- jsonyter-1.0.0.dist-info/WHEEL +4 -0
- jsonyter-1.0.0.dist-info/entry_points.txt +3 -0
- jsonyter-1.0.0.dist-info/licenses/LICENSE +674 -0
jsonyter/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""jsonyter: a JSON-first Python interface to a Jupyter server.
|
|
2
|
+
|
|
3
|
+
Every public method returns plain Python objects (dicts, lists, strings,
|
|
4
|
+
numbers, booleans, None) that serialize directly with ``json.dumps``, so the
|
|
5
|
+
library can sit behind any editor front end — the original target being an
|
|
6
|
+
Emacs REPL driven over a JSON pipe (see ``jsonyter.cli``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .client import Client, JupyterError
|
|
10
|
+
from .kernel import KernelConnection
|
|
11
|
+
from .notebook import NotebookConflict, file_hash, read_notebook, write_notebook
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["Client", "KernelConnection", "JupyterError", "NotebookConflict",
|
|
16
|
+
"read_notebook", "write_notebook", "file_hash", "__version__"]
|
jsonyter/__main__.py
ADDED
jsonyter/cli.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""JSON-over-stdio interface, designed to be driven by Emacs.
|
|
2
|
+
|
|
3
|
+
Run ``jsonyter --url http://localhost:8888`` and write one JSON request per
|
|
4
|
+
line to stdin; one JSON response per line comes back on stdout. Requests look
|
|
5
|
+
like::
|
|
6
|
+
|
|
7
|
+
{"id": 1, "method": "execute", "params": {"kernel_id": "...", "code": "1+1"}}
|
|
8
|
+
|
|
9
|
+
and responses like::
|
|
10
|
+
|
|
11
|
+
{"id": 1, "result": {...}}
|
|
12
|
+
{"id": 1, "error": {"error": "JupyterError", "message": "...", ...}}
|
|
13
|
+
|
|
14
|
+
Requests are handled concurrently: calls that don't touch a kernel's socket
|
|
15
|
+
run immediately, and each kernel gets its own worker so a long ``execute``
|
|
16
|
+
never blocks the reader. That means an ``interrupt_kernel`` sent while code
|
|
17
|
+
is running is acted on right away, and responses may arrive out of request
|
|
18
|
+
order — match them by ``id``.
|
|
19
|
+
|
|
20
|
+
Lines that are not final responses are tagged by an extra key instead of
|
|
21
|
+
``result``/``error``, so a client can dispatch on which key is present:
|
|
22
|
+
|
|
23
|
+
- ``{"id": N, "output": {...}}`` — incremental output from a running
|
|
24
|
+
``execute`` (only when that request passed ``"stream": true``).
|
|
25
|
+
- ``{"id": N, "input_request": {"prompt": ...}}`` — the kernel wants stdin;
|
|
26
|
+
reply with ``{"id": N, "input": "..."}`` (a bare ``{"input": "..."}`` also
|
|
27
|
+
works when only one request is waiting).
|
|
28
|
+
- ``{"event": {...}, "kernel_id": ...}`` — async kernel state, after
|
|
29
|
+
``subscribe``.
|
|
30
|
+
|
|
31
|
+
Available methods (params in parentheses):
|
|
32
|
+
|
|
33
|
+
- ``status``, ``version``, ``list_kernelspecs``, ``list_kernels``,
|
|
34
|
+
``list_sessions``
|
|
35
|
+
- ``start_kernel`` (``name``), ``get_kernel``/``shutdown_kernel``/
|
|
36
|
+
``restart_kernel``/``interrupt_kernel`` (``kernel_id``)
|
|
37
|
+
- ``create_session`` (``path``, ``kernel_name``), ``delete_session`` (``session_id``)
|
|
38
|
+
- ``execute`` (``kernel_id``, ``code``, ``timeout``, ``silent``, ``stream``)
|
|
39
|
+
- ``complete``/``inspect`` (``kernel_id``, ``code``, ``cursor_pos``),
|
|
40
|
+
``is_complete`` (``kernel_id``, ``code``), ``kernel_info`` (``kernel_id``)
|
|
41
|
+
- ``subscribe``/``unsubscribe`` (``kernel_id``) — async kernel status events
|
|
42
|
+
- ``disconnect`` (``kernel_id``) — close the websocket but leave the kernel up
|
|
43
|
+
- ``read_notebook`` (``path``), ``write_notebook`` (``path``, ``cells``,
|
|
44
|
+
``expect_hash``, ``include_outputs``), ``notebook_hash`` (``path``) — local
|
|
45
|
+
``.ipynb`` files; these need no server and no kernel, so the bridge is
|
|
46
|
+
usable offline
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import os
|
|
52
|
+
import queue
|
|
53
|
+
import sys
|
|
54
|
+
import threading
|
|
55
|
+
import time
|
|
56
|
+
|
|
57
|
+
from .client import Client, JupyterError
|
|
58
|
+
|
|
59
|
+
# Client methods invocable directly, mapped to accepted params.
|
|
60
|
+
_CLIENT_METHODS = {
|
|
61
|
+
"status": (), "version": (), "list_kernelspecs": (), "list_kernels": (),
|
|
62
|
+
"list_sessions": (),
|
|
63
|
+
"start_kernel": ("name",),
|
|
64
|
+
"get_kernel": ("kernel_id",),
|
|
65
|
+
"shutdown_kernel": ("kernel_id",),
|
|
66
|
+
"restart_kernel": ("kernel_id",),
|
|
67
|
+
"interrupt_kernel": ("kernel_id",),
|
|
68
|
+
"create_session": ("path", "kernel_name", "session_type", "name"),
|
|
69
|
+
"get_session": ("session_id",),
|
|
70
|
+
"delete_session": ("session_id",),
|
|
71
|
+
"get_contents": ("path", "content"),
|
|
72
|
+
# Local filesystem, no server contact — and not on a kernel worker, so a
|
|
73
|
+
# save never queues behind a running execute.
|
|
74
|
+
"read_notebook": ("path",),
|
|
75
|
+
"write_notebook": ("path", "cells", "expect_hash", "include_outputs"),
|
|
76
|
+
"notebook_hash": ("path",),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_KERNEL_METHODS = {
|
|
80
|
+
"execute": ("code", "timeout", "silent", "store_history"),
|
|
81
|
+
"complete": ("code", "cursor_pos", "timeout"),
|
|
82
|
+
"inspect": ("code", "cursor_pos", "detail_level", "timeout"),
|
|
83
|
+
"is_complete": ("code", "timeout"),
|
|
84
|
+
"kernel_info": ("timeout",),
|
|
85
|
+
"history": ("n", "timeout"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_LOCAL_METHODS = ("subscribe", "unsubscribe", "disconnect", "methods")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Dispatcher:
|
|
92
|
+
"""Routes JSON requests to the client/kernels, concurrently."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, client, stdin=None, stdout=None, pretty=False,
|
|
95
|
+
stream=False):
|
|
96
|
+
self.client = client
|
|
97
|
+
self.connections = {}
|
|
98
|
+
self.stdin = stdin or sys.stdin
|
|
99
|
+
self.stdout = stdout or sys.stdout
|
|
100
|
+
self.pretty = pretty
|
|
101
|
+
self.stream = stream
|
|
102
|
+
self._write_lock = threading.Lock()
|
|
103
|
+
self._state_lock = threading.Lock()
|
|
104
|
+
self._workers = {} # kernel_id -> (Queue, Thread)
|
|
105
|
+
self._rest_workers = []
|
|
106
|
+
self._rest_queue = queue.Queue()
|
|
107
|
+
self._pending_input = {} # request id -> Queue
|
|
108
|
+
self._subscribed = {} # kernel_id -> listener callable
|
|
109
|
+
self._stopping = False
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------------- output
|
|
112
|
+
|
|
113
|
+
def _emit(self, obj):
|
|
114
|
+
line = json.dumps(obj, indent=2 if self.pretty else None) + "\n"
|
|
115
|
+
with self._write_lock: # keep concurrent replies from interleaving
|
|
116
|
+
self.stdout.write(line)
|
|
117
|
+
self.stdout.flush()
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------ connections
|
|
120
|
+
|
|
121
|
+
def _connection(self, kernel_id):
|
|
122
|
+
with self._state_lock:
|
|
123
|
+
if kernel_id not in self.connections:
|
|
124
|
+
self.connections[kernel_id] = self.client.kernel(kernel_id)
|
|
125
|
+
return self.connections[kernel_id]
|
|
126
|
+
|
|
127
|
+
def _drop_connection(self, kernel_id):
|
|
128
|
+
with self._state_lock:
|
|
129
|
+
conn = self.connections.pop(kernel_id, None)
|
|
130
|
+
self._subscribed.pop(kernel_id, None)
|
|
131
|
+
if conn is not None:
|
|
132
|
+
try:
|
|
133
|
+
conn.close()
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
return conn
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------- workers
|
|
139
|
+
|
|
140
|
+
def _kernel_queue(self, kernel_id):
|
|
141
|
+
"""Per-kernel work queue: one socket, so one request at a time."""
|
|
142
|
+
with self._state_lock:
|
|
143
|
+
entry = self._workers.get(kernel_id)
|
|
144
|
+
if entry is None:
|
|
145
|
+
work = queue.Queue()
|
|
146
|
+
thread = threading.Thread(
|
|
147
|
+
target=self._worker_loop, args=(work,),
|
|
148
|
+
name="jsonyter-kernel-" + kernel_id[:8], daemon=True)
|
|
149
|
+
entry = (work, thread)
|
|
150
|
+
self._workers[kernel_id] = entry
|
|
151
|
+
thread.start()
|
|
152
|
+
return entry[0]
|
|
153
|
+
|
|
154
|
+
def _worker_loop(self, work):
|
|
155
|
+
while True:
|
|
156
|
+
request = work.get()
|
|
157
|
+
if request is None:
|
|
158
|
+
return
|
|
159
|
+
self._run(request)
|
|
160
|
+
|
|
161
|
+
def _rest_worker_loop(self):
|
|
162
|
+
while True:
|
|
163
|
+
request = self._rest_queue.get()
|
|
164
|
+
if request is None:
|
|
165
|
+
return
|
|
166
|
+
self._run(request)
|
|
167
|
+
|
|
168
|
+
def _start_rest_workers(self, count=4):
|
|
169
|
+
for i in range(count):
|
|
170
|
+
thread = threading.Thread(
|
|
171
|
+
target=self._rest_worker_loop,
|
|
172
|
+
name="jsonyter-rest-{}".format(i), daemon=True)
|
|
173
|
+
thread.start()
|
|
174
|
+
self._rest_workers.append(thread)
|
|
175
|
+
|
|
176
|
+
def _run(self, request):
|
|
177
|
+
try:
|
|
178
|
+
self._emit(self.dispatch(request))
|
|
179
|
+
except JupyterError as exc:
|
|
180
|
+
self._emit({"id": request.get("id"), "error": exc.to_json()})
|
|
181
|
+
except Exception as exc: # keep the pipe alive on bugs
|
|
182
|
+
self._emit({"id": request.get("id"), "error": {
|
|
183
|
+
"error": type(exc).__name__, "message": str(exc)}})
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------ stdin
|
|
186
|
+
|
|
187
|
+
def _stdin_callback(self, request_id):
|
|
188
|
+
"""Ask the front end for input; the reader thread routes the reply."""
|
|
189
|
+
def ask(content):
|
|
190
|
+
answer = queue.Queue()
|
|
191
|
+
with self._state_lock:
|
|
192
|
+
self._pending_input[request_id] = answer
|
|
193
|
+
try:
|
|
194
|
+
self._emit({"id": request_id, "input_request": content})
|
|
195
|
+
return answer.get()
|
|
196
|
+
finally:
|
|
197
|
+
with self._state_lock:
|
|
198
|
+
self._pending_input.pop(request_id, None)
|
|
199
|
+
return ask
|
|
200
|
+
|
|
201
|
+
def _route_input(self, obj):
|
|
202
|
+
value = obj.get("input", "")
|
|
203
|
+
request_id = obj.get("id")
|
|
204
|
+
with self._state_lock:
|
|
205
|
+
if request_id is not None:
|
|
206
|
+
answer = self._pending_input.get(request_id)
|
|
207
|
+
elif len(self._pending_input) == 1:
|
|
208
|
+
answer = next(iter(self._pending_input.values()))
|
|
209
|
+
else:
|
|
210
|
+
answer = None
|
|
211
|
+
if answer is not None:
|
|
212
|
+
answer.put(value)
|
|
213
|
+
|
|
214
|
+
# -------------------------------------------------------------- dispatch
|
|
215
|
+
|
|
216
|
+
def dispatch(self, request):
|
|
217
|
+
request_id = request.get("id")
|
|
218
|
+
method = request.get("method")
|
|
219
|
+
params = request.get("params") or {}
|
|
220
|
+
|
|
221
|
+
if method in _CLIENT_METHODS:
|
|
222
|
+
allowed = _CLIENT_METHODS[method]
|
|
223
|
+
kwargs = {k: v for k, v in params.items() if k in allowed}
|
|
224
|
+
args = []
|
|
225
|
+
if "kernel_id" in kwargs:
|
|
226
|
+
args = [kwargs.pop("kernel_id")]
|
|
227
|
+
if "session_id" in kwargs:
|
|
228
|
+
args = [kwargs.pop("session_id")]
|
|
229
|
+
if method == "shutdown_kernel" and args:
|
|
230
|
+
self._drop_connection(args[0])
|
|
231
|
+
result = getattr(self.client, method)(*args, **kwargs)
|
|
232
|
+
elif method in _KERNEL_METHODS:
|
|
233
|
+
kernel_id = params.get("kernel_id")
|
|
234
|
+
if not kernel_id:
|
|
235
|
+
raise JupyterError("missing required param: kernel_id")
|
|
236
|
+
conn = self._connection(kernel_id)
|
|
237
|
+
kwargs = {k: v for k, v in params.items()
|
|
238
|
+
if k in _KERNEL_METHODS[method]}
|
|
239
|
+
if method == "execute":
|
|
240
|
+
kwargs["stdin_callback"] = self._stdin_callback(request_id)
|
|
241
|
+
if params.get("stream", self.stream):
|
|
242
|
+
kwargs["on_output"] = lambda output: self._emit(
|
|
243
|
+
{"id": request_id, "output": output})
|
|
244
|
+
result = getattr(conn, method)(**kwargs)
|
|
245
|
+
elif method == "subscribe":
|
|
246
|
+
result = self._subscribe(params.get("kernel_id"))
|
|
247
|
+
elif method == "unsubscribe":
|
|
248
|
+
result = self._unsubscribe(params.get("kernel_id"))
|
|
249
|
+
elif method == "disconnect":
|
|
250
|
+
conn = self._drop_connection(params.get("kernel_id"))
|
|
251
|
+
result = {"id": params.get("kernel_id"), "closed": conn is not None}
|
|
252
|
+
elif method == "methods":
|
|
253
|
+
result = (sorted(_CLIENT_METHODS) + sorted(_KERNEL_METHODS)
|
|
254
|
+
+ sorted(_LOCAL_METHODS))
|
|
255
|
+
else:
|
|
256
|
+
raise JupyterError("unknown method: {!r}".format(method))
|
|
257
|
+
return {"id": request_id, "result": result}
|
|
258
|
+
|
|
259
|
+
# ----------------------------------------------------------- subscriptions
|
|
260
|
+
|
|
261
|
+
def _subscribe(self, kernel_id):
|
|
262
|
+
if not kernel_id:
|
|
263
|
+
raise JupyterError("missing required param: kernel_id")
|
|
264
|
+
with self._state_lock:
|
|
265
|
+
already = kernel_id in self._subscribed
|
|
266
|
+
if already:
|
|
267
|
+
return {"kernel_id": kernel_id, "subscribed": True}
|
|
268
|
+
conn = self._connection(kernel_id)
|
|
269
|
+
conn.connect()
|
|
270
|
+
|
|
271
|
+
def listener(event):
|
|
272
|
+
self._emit({"kernel_id": kernel_id, "event": event})
|
|
273
|
+
|
|
274
|
+
conn.add_listener(listener)
|
|
275
|
+
with self._state_lock:
|
|
276
|
+
self._subscribed[kernel_id] = listener
|
|
277
|
+
return {"kernel_id": kernel_id, "subscribed": True,
|
|
278
|
+
"execution_state": conn.execution_state}
|
|
279
|
+
|
|
280
|
+
def _unsubscribe(self, kernel_id):
|
|
281
|
+
with self._state_lock:
|
|
282
|
+
listener = self._subscribed.pop(kernel_id, None)
|
|
283
|
+
conn = self.connections.get(kernel_id)
|
|
284
|
+
if listener is not None and conn is not None:
|
|
285
|
+
conn.remove_listener(listener)
|
|
286
|
+
return {"kernel_id": kernel_id, "subscribed": False}
|
|
287
|
+
|
|
288
|
+
# -------------------------------------------------------------- main loop
|
|
289
|
+
|
|
290
|
+
def run(self):
|
|
291
|
+
self._start_rest_workers()
|
|
292
|
+
for line in self.stdin:
|
|
293
|
+
line = line.strip()
|
|
294
|
+
if not line:
|
|
295
|
+
continue
|
|
296
|
+
try:
|
|
297
|
+
request = json.loads(line)
|
|
298
|
+
except ValueError as exc:
|
|
299
|
+
self._emit({"id": None, "error": {
|
|
300
|
+
"error": "ParseError", "message": str(exc)}})
|
|
301
|
+
continue
|
|
302
|
+
if not isinstance(request, dict):
|
|
303
|
+
self._emit({"id": None, "error": {
|
|
304
|
+
"error": "ParseError",
|
|
305
|
+
"message": "request must be a JSON object"}})
|
|
306
|
+
continue
|
|
307
|
+
# An input reply, not a request.
|
|
308
|
+
if "input" in request and "method" not in request:
|
|
309
|
+
self._route_input(request)
|
|
310
|
+
continue
|
|
311
|
+
method = request.get("method")
|
|
312
|
+
if method in _KERNEL_METHODS:
|
|
313
|
+
kernel_id = request.get("params", {}).get("kernel_id")
|
|
314
|
+
if kernel_id:
|
|
315
|
+
self._kernel_queue(kernel_id).put(request)
|
|
316
|
+
continue
|
|
317
|
+
self._rest_queue.put(request)
|
|
318
|
+
|
|
319
|
+
def shutdown(self, drain_timeout=10.0):
|
|
320
|
+
"""Finish queued work, then tear down.
|
|
321
|
+
|
|
322
|
+
Requests already accepted must still be answered: stdin reaching EOF
|
|
323
|
+
(a one-shot invocation, or the editor quitting) would otherwise kill
|
|
324
|
+
the worker threads mid-flight and silently drop their responses. The
|
|
325
|
+
sentinels queue behind the outstanding work, so joining the workers
|
|
326
|
+
drains it; the deadline keeps a long-running execute from blocking
|
|
327
|
+
exit forever.
|
|
328
|
+
"""
|
|
329
|
+
self._stopping = True
|
|
330
|
+
with self._state_lock:
|
|
331
|
+
workers = list(self._workers.values())
|
|
332
|
+
for work, _thread in workers:
|
|
333
|
+
work.put(None)
|
|
334
|
+
for _ in self._rest_workers:
|
|
335
|
+
self._rest_queue.put(None)
|
|
336
|
+
|
|
337
|
+
deadline = time.monotonic() + drain_timeout
|
|
338
|
+
for _work, thread in workers:
|
|
339
|
+
thread.join(timeout=max(0.0, deadline - time.monotonic()))
|
|
340
|
+
for thread in self._rest_workers:
|
|
341
|
+
thread.join(timeout=max(0.0, deadline - time.monotonic()))
|
|
342
|
+
|
|
343
|
+
with self._state_lock:
|
|
344
|
+
connections = list(self.connections.values())
|
|
345
|
+
self.connections.clear()
|
|
346
|
+
for conn in connections:
|
|
347
|
+
try:
|
|
348
|
+
conn.close()
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def resolve_token(args):
|
|
354
|
+
"""Token from the least-exposed source available.
|
|
355
|
+
|
|
356
|
+
``--token`` is accepted for convenience but is visible to any local user
|
|
357
|
+
via ``ps``; ``JUPYTER_TOKEN`` or ``--token-file`` (including ``-`` to read
|
|
358
|
+
the first stdin line, which pairs with ``gpg -d | jsonyter``) keep it off
|
|
359
|
+
the command line.
|
|
360
|
+
"""
|
|
361
|
+
if args.token_file:
|
|
362
|
+
if args.token_file == "-":
|
|
363
|
+
return sys.stdin.readline().strip() or None
|
|
364
|
+
with open(os.path.expanduser(args.token_file)) as handle:
|
|
365
|
+
return handle.read().strip() or None
|
|
366
|
+
if args.token:
|
|
367
|
+
return args.token
|
|
368
|
+
return os.environ.get("JUPYTER_TOKEN") or None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def main(argv=None):
|
|
372
|
+
parser = argparse.ArgumentParser(
|
|
373
|
+
prog="jsonyter",
|
|
374
|
+
description="JSON-over-stdio bridge to a Jupyter server.")
|
|
375
|
+
parser.add_argument("--url", default="http://localhost:8888",
|
|
376
|
+
help="Jupyter server base URL")
|
|
377
|
+
parser.add_argument("--token", default=None,
|
|
378
|
+
help="Jupyter auth token (INSECURE: visible to other "
|
|
379
|
+
"local users via ps; prefer JUPYTER_TOKEN or "
|
|
380
|
+
"--token-file)")
|
|
381
|
+
parser.add_argument("--token-file", default=None, metavar="PATH",
|
|
382
|
+
help="read the token from PATH, or from the first "
|
|
383
|
+
"line of stdin if PATH is '-' (e.g. "
|
|
384
|
+
"gpg -d token.gpg | jsonyter --token-file -)")
|
|
385
|
+
parser.add_argument("--timeout", type=float, default=30.0,
|
|
386
|
+
help="timeout in seconds for REST calls and the "
|
|
387
|
+
"WebSocket handshake (not kernel execution)")
|
|
388
|
+
parser.add_argument("--exec-timeout", type=float, default=None,
|
|
389
|
+
help="default timeout in seconds to wait for a "
|
|
390
|
+
"kernel reply on execute, measured as silence "
|
|
391
|
+
"since the last message (not total run time); "
|
|
392
|
+
"omit for no timeout (wait indefinitely — the "
|
|
393
|
+
"default, since user code may legitimately run "
|
|
394
|
+
"for any length of time; use interrupt_kernel "
|
|
395
|
+
"to stop it)")
|
|
396
|
+
parser.add_argument("--control-timeout", type=float, default=30.0,
|
|
397
|
+
help="same, for the introspection calls (complete, "
|
|
398
|
+
"inspect, is_complete, kernel_info, history), "
|
|
399
|
+
"which are bounded operations; default 30. Pass "
|
|
400
|
+
"0 to wait indefinitely — not advised, since a "
|
|
401
|
+
"kernel that never answers one of these (SAS "
|
|
402
|
+
"never answers history) would wedge its worker")
|
|
403
|
+
parser.add_argument("--stream", action="store_true",
|
|
404
|
+
help="emit incremental {\"id\": N, \"output\": {...}} "
|
|
405
|
+
"lines for every execute, without each request "
|
|
406
|
+
"having to ask for \"stream\": true")
|
|
407
|
+
parser.add_argument("--insecure", action="store_true",
|
|
408
|
+
help="skip TLS certificate verification")
|
|
409
|
+
parser.add_argument("--pretty", action="store_true",
|
|
410
|
+
help="indent JSON responses (for humans; breaks the "
|
|
411
|
+
"one-line-per-response protocol editors rely on)")
|
|
412
|
+
args = parser.parse_args(argv)
|
|
413
|
+
|
|
414
|
+
client = Client(args.url, token=resolve_token(args) or False,
|
|
415
|
+
timeout=args.timeout, exec_timeout=args.exec_timeout,
|
|
416
|
+
control_timeout=args.control_timeout or None,
|
|
417
|
+
verify_tls=not args.insecure)
|
|
418
|
+
dispatcher = Dispatcher(client, pretty=args.pretty, stream=args.stream)
|
|
419
|
+
try:
|
|
420
|
+
dispatcher.run()
|
|
421
|
+
except KeyboardInterrupt:
|
|
422
|
+
pass
|
|
423
|
+
finally:
|
|
424
|
+
dispatcher.shutdown()
|
|
425
|
+
return 0
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__":
|
|
429
|
+
sys.exit(main())
|