conkernelclient 0.0.11__tar.gz → 0.0.13__tar.gz
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.
- {conkernelclient-0.0.11/conkernelclient.egg-info → conkernelclient-0.0.13}/PKG-INFO +3 -2
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/README.md +1 -0
- conkernelclient-0.0.13/conkernelclient/__init__.py +3 -0
- conkernelclient-0.0.13/conkernelclient/_modidx.py +71 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient/core.py +25 -19
- conkernelclient-0.0.13/conkernelclient/ops.py +222 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13/conkernelclient.egg-info}/PKG-INFO +3 -2
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/SOURCES.txt +1 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/pyproject.toml +5 -2
- conkernelclient-0.0.11/conkernelclient/__init__.py +0 -2
- conkernelclient-0.0.11/conkernelclient/_modidx.py +0 -25
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/LICENSE +0 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/MANIFEST.in +0 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/dependency_links.txt +0 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/entry_points.txt +0 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/requires.txt +1 -1
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/top_level.txt +0 -0
- {conkernelclient-0.0.11 → conkernelclient-0.0.13}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conkernelclient
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: Concurrent-safe Jupyter KernelClient
|
|
5
5
|
Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -13,8 +13,8 @@ Requires-Python: >=3.10
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: jupyter_client
|
|
16
|
+
Requires-Dist: fastcore
|
|
16
17
|
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: fastcore; extra == "dev"
|
|
18
18
|
Requires-Dist: ipykernel; extra == "dev"
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
@@ -125,3 +125,4 @@ if await km.is_alive():
|
|
|
125
125
|
kc.stop_channels()
|
|
126
126
|
await km.shutdown_kernel()
|
|
127
127
|
```
|
|
128
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/conkernelclient',
|
|
5
|
+
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
|
+
'git_url': 'https://github.com/AnswerDotAI/conkernelclient',
|
|
7
|
+
'lib_path': 'conkernelclient'},
|
|
8
|
+
'syms': { 'conkernelclient.core': { 'conkernelclient.core.ConKernelClient': ('core.html#conkernelclient', 'conkernelclient/core.py'),
|
|
9
|
+
'conkernelclient.core.ConKernelClient.__init__': ( 'core.html#conkernelclient.__init__',
|
|
10
|
+
'conkernelclient/core.py'),
|
|
11
|
+
'conkernelclient.core.ConKernelClient._async_recv_reply': ( 'core.html#conkernelclient._async_recv_reply',
|
|
12
|
+
'conkernelclient/core.py'),
|
|
13
|
+
'conkernelclient.core.ConKernelClient._check_alive': ( 'core.html#conkernelclient._check_alive',
|
|
14
|
+
'conkernelclient/core.py'),
|
|
15
|
+
'conkernelclient.core.ConKernelClient._fail_pending': ( 'core.html#conkernelclient._fail_pending',
|
|
16
|
+
'conkernelclient/core.py'),
|
|
17
|
+
'conkernelclient.core.ConKernelClient.execute': ( 'core.html#conkernelclient.execute',
|
|
18
|
+
'conkernelclient/core.py'),
|
|
19
|
+
'conkernelclient.core.ConKernelClient.start_channels': ( 'core.html#conkernelclient.start_channels',
|
|
20
|
+
'conkernelclient/core.py'),
|
|
21
|
+
'conkernelclient.core.ConKernelClient.stdin_send': ( 'core.html#conkernelclient.stdin_send',
|
|
22
|
+
'conkernelclient/core.py'),
|
|
23
|
+
'conkernelclient.core.ConKernelClient.stop_channels': ( 'core.html#conkernelclient.stop_channels',
|
|
24
|
+
'conkernelclient/core.py'),
|
|
25
|
+
'conkernelclient.core.ConKernelManager': ('core.html#conkernelmanager', 'conkernelclient/core.py'),
|
|
26
|
+
'conkernelclient.core.DeadKernelError': ('core.html#deadkernelerror', 'conkernelclient/core.py'),
|
|
27
|
+
'conkernelclient.core._send': ('core.html#_send', 'conkernelclient/core.py'),
|
|
28
|
+
'conkernelclient.core.apply_session_patch': ( 'core.html#apply_session_patch',
|
|
29
|
+
'conkernelclient/core.py')},
|
|
30
|
+
'conkernelclient.ops': { 'conkernelclient.ops.ConKernelClient.cmd': ('ops.html#conkernelclient.cmd', 'conkernelclient/ops.py'),
|
|
31
|
+
'conkernelclient.ops.ConKernelClient.control_request': ( 'ops.html#conkernelclient.control_request',
|
|
32
|
+
'conkernelclient/ops.py'),
|
|
33
|
+
'conkernelclient.ops.ConKernelClient.ctl': ('ops.html#conkernelclient.ctl', 'conkernelclient/ops.py'),
|
|
34
|
+
'conkernelclient.ops.ConKernelClient.dap': ('ops.html#conkernelclient.dap', 'conkernelclient/ops.py'),
|
|
35
|
+
'conkernelclient.ops.ConKernelClient.eval_expr': ( 'ops.html#conkernelclient.eval_expr',
|
|
36
|
+
'conkernelclient/ops.py'),
|
|
37
|
+
'conkernelclient.ops.ConKernelClient.exec_drain': ( 'ops.html#conkernelclient.exec_drain',
|
|
38
|
+
'conkernelclient/ops.py'),
|
|
39
|
+
'conkernelclient.ops.ConKernelClient.exec_ok': ( 'ops.html#conkernelclient.exec_ok',
|
|
40
|
+
'conkernelclient/ops.py'),
|
|
41
|
+
'conkernelclient.ops.ConKernelClient.exec_outs': ( 'ops.html#conkernelclient.exec_outs',
|
|
42
|
+
'conkernelclient/ops.py'),
|
|
43
|
+
'conkernelclient.ops.ConKernelClient.input_reply': ( 'ops.html#conkernelclient.input_reply',
|
|
44
|
+
'conkernelclient/ops.py'),
|
|
45
|
+
'conkernelclient.ops.ConKernelClient.interrupt': ( 'ops.html#conkernelclient.interrupt',
|
|
46
|
+
'conkernelclient/ops.py'),
|
|
47
|
+
'conkernelclient.ops.ConKernelClient.iopub_drain': ( 'ops.html#conkernelclient.iopub_drain',
|
|
48
|
+
'conkernelclient/ops.py'),
|
|
49
|
+
'conkernelclient.ops.ConKernelClient.iopub_flush': ( 'ops.html#conkernelclient.iopub_flush',
|
|
50
|
+
'conkernelclient/ops.py'),
|
|
51
|
+
'conkernelclient.ops.ConKernelClient.shell_request': ( 'ops.html#conkernelclient.shell_request',
|
|
52
|
+
'conkernelclient/ops.py'),
|
|
53
|
+
'conkernelclient.ops.ConKernelClient.user_exprs': ( 'ops.html#conkernelclient.user_exprs',
|
|
54
|
+
'conkernelclient/ops.py'),
|
|
55
|
+
'conkernelclient.ops.EvalError': ('ops.html#evalerror', 'conkernelclient/ops.py'),
|
|
56
|
+
'conkernelclient.ops._DapProxy': ('ops.html#_dapproxy', 'conkernelclient/ops.py'),
|
|
57
|
+
'conkernelclient.ops._DapProxy.__getattr__': ( 'ops.html#_dapproxy.__getattr__',
|
|
58
|
+
'conkernelclient/ops.py'),
|
|
59
|
+
'conkernelclient.ops._DapProxy.__init__': ('ops.html#_dapproxy.__init__', 'conkernelclient/ops.py'),
|
|
60
|
+
'conkernelclient.ops._ReqProxy': ('ops.html#_reqproxy', 'conkernelclient/ops.py'),
|
|
61
|
+
'conkernelclient.ops._ReqProxy.__getattr__': ( 'ops.html#_reqproxy.__getattr__',
|
|
62
|
+
'conkernelclient/ops.py'),
|
|
63
|
+
'conkernelclient.ops._ReqProxy.__init__': ('ops.html#_reqproxy.__init__', 'conkernelclient/ops.py'),
|
|
64
|
+
'conkernelclient.ops.iopub_msgs': ('ops.html#iopub_msgs', 'conkernelclient/ops.py'),
|
|
65
|
+
'conkernelclient.ops.iopub_streams': ('ops.html#iopub_streams', 'conkernelclient/ops.py'),
|
|
66
|
+
'conkernelclient.ops.iter_timeout': ('ops.html#iter_timeout', 'conkernelclient/ops.py'),
|
|
67
|
+
'conkernelclient.ops.nb_outputs': ('ops.html#nb_outputs', 'conkernelclient/ops.py'),
|
|
68
|
+
'conkernelclient.ops.parent_id': ('ops.html#parent_id', 'conkernelclient/ops.py'),
|
|
69
|
+
'conkernelclient.ops.parse_expr': ('ops.html#parse_expr', 'conkernelclient/ops.py'),
|
|
70
|
+
'conkernelclient.ops.reconnect': ('ops.html#reconnect', 'conkernelclient/ops.py'),
|
|
71
|
+
'conkernelclient.ops.run_kernel': ('ops.html#run_kernel', 'conkernelclient/ops.py')}}}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
"""Concurrent-safe Jupyter KernelClient
|
|
2
2
|
|
|
3
|
-
Docs: https://AnswerDotAI.github.io/
|
|
3
|
+
Docs: https://AnswerDotAI.github.io/conkernelclient/core.html.md"""
|
|
4
4
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['DeadKernelError', 'ConKernelClient', 'ConKernelManager']
|
|
8
|
+
__all__ = ['DeadKernelError', 'apply_session_patch', 'ConKernelClient', 'ConKernelManager']
|
|
9
9
|
|
|
10
10
|
# %% ../nbs/00_core.ipynb #d3b521c0
|
|
11
|
-
from
|
|
12
|
-
from asyncio import create_task
|
|
13
|
-
from jupyter_client import KernelClient, AsyncKernelClient
|
|
11
|
+
from jupyter_client import AsyncKernelClient, AsyncKernelManager
|
|
14
12
|
from jupyter_client.session import Session
|
|
15
|
-
from jupyter_client.channels import AsyncZMQSocketChannel
|
|
16
13
|
from zmq.error import ZMQError
|
|
17
|
-
from jupyter_client.kernelspec import KernelSpec
|
|
18
|
-
from jupyter_client import AsyncKernelManager
|
|
19
14
|
from traitlets import Type
|
|
20
15
|
import asyncio, zmq.asyncio, time, logging
|
|
21
16
|
|
|
@@ -26,24 +21,35 @@ _log = logging.getLogger(__name__)
|
|
|
26
21
|
class DeadKernelError(Exception): pass
|
|
27
22
|
|
|
28
23
|
# %% ../nbs/00_core.ipynb #374b75d0
|
|
29
|
-
if not hasattr(Session, '_orig_send'): Session._orig_send = Session.send
|
|
30
|
-
|
|
31
24
|
def _send(self, stream, msg_or_type, content=None, parent=None, ident=None,
|
|
32
|
-
|
|
25
|
+
buffers=None, track=False, header=None, metadata=None):
|
|
33
26
|
msg = self._orig_send(stream, msg_or_type, content=content, parent=parent,
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
27
|
+
ident=ident, buffers=buffers, track=track, header=header, metadata=metadata)
|
|
28
|
+
# Session.send sends via a SYNC shadow of the asyncio socket (zmq.Socket.shadow), which runs
|
|
29
|
+
# libzmq process_commands() and consumes the FD edge a buffered reply may have signalled —
|
|
30
|
+
# a recv parked in poll(None) on that edge would then sleep forever. Reading EVENTS through
|
|
31
|
+
# the async override (_AsyncSocket.get) makes pyzmq re-schedule _handle_events for pending
|
|
32
|
+
# recv/poll futures, compensating the consumed edge. It MUST be .get(): pyzmq aliases
|
|
33
|
+
# `getsockopt = SocketBase.get` at class level, so .getsockopt() bypasses the override
|
|
34
|
+
# and consumes the edge without rescheduling.
|
|
38
35
|
if stream:
|
|
39
36
|
if hasattr(stream, 'io_thread'): stream.io_thread.socket.get(zmq.EVENTS)
|
|
40
|
-
|
|
37
|
+
# See tests/zmq_regression.py for a regression test which fails is 'get' is switched to 'getsockopt'
|
|
38
|
+
elif hasattr(stream, 'get'): stream.get(zmq.EVENTS)
|
|
41
39
|
return msg
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
def apply_session_patch():
|
|
42
|
+
"Make `Session.send` compensate the ZMQ FD edge a sync send consumes (see `_send` comment). Idempotent; applied automatically when a `ConKernelClient` is constructed, and available for use with vanilla clients."
|
|
43
|
+
if hasattr(Session, '_orig_send'): return
|
|
44
|
+
Session._orig_send = Session.send
|
|
45
|
+
Session.send = _send
|
|
44
46
|
|
|
45
47
|
# %% ../nbs/00_core.ipynb #d6a5fa6a
|
|
46
48
|
class ConKernelClient(AsyncKernelClient):
|
|
49
|
+
def __init__(self, *args, **kwargs):
|
|
50
|
+
apply_session_patch()
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
|
|
47
53
|
def _fail_pending(self, exc:Exception, skip=None):
|
|
48
54
|
for k,(q,_) in list(getattr(self, '_pending', {}).items()):
|
|
49
55
|
if k != skip:
|
|
@@ -104,7 +110,7 @@ class ConKernelClient(AsyncKernelClient):
|
|
|
104
110
|
finally: self._pending.pop(msg_id, None)
|
|
105
111
|
|
|
106
112
|
def execute(self, code, user_expressions=None, allow_stdin=None, reply=False, subsh_id=None,
|
|
107
|
-
|
|
113
|
+
cts_typ='code', timeout=60, msg_id=None, stop_on_error=True, fail_pending=False, **kw):
|
|
108
114
|
"Send an execute request, returning a coroutine for the reply if `reply`, else the msg_id"
|
|
109
115
|
if not self._check_alive(): return asyncio.sleep(0) if reply else None
|
|
110
116
|
if user_expressions is None: user_expressions = {}
|
|
@@ -115,7 +121,7 @@ class ConKernelClient(AsyncKernelClient):
|
|
|
115
121
|
if msg_id is not None: msg["header"]["msg_id"] = msg_id
|
|
116
122
|
if subsh_id is not None: msg["header"]["subshell_id"] = subsh_id
|
|
117
123
|
msg_id = msg["header"]["msg_id"]
|
|
118
|
-
if reply: self._pending[msg_id] = (asyncio.Queue(maxsize=1), stop_on_error)
|
|
124
|
+
if reply: self._pending[msg_id] = (asyncio.Queue(maxsize=1), stop_on_error if fail_pending is None else fail_pending)
|
|
119
125
|
try: self.shell_channel.send(msg)
|
|
120
126
|
except (AssertionError, OSError, ZMQError) as e: raise DeadKernelError(f"Kernel socket closed: {e}") from e
|
|
121
127
|
if not reply: return msg_id
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Standard async operations on a running kernel, for `ConKernelClient`
|
|
2
|
+
|
|
3
|
+
Docs: https://AnswerDotAI.github.io/conkernelclient/ops.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_ops.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['default_timeout', 'output_types', 'parent_id', 'iter_timeout', 'nb_outputs', 'iopub_msgs', 'iopub_streams',
|
|
9
|
+
'EvalError', 'parse_expr', 'run_kernel', 'reconnect']
|
|
10
|
+
|
|
11
|
+
# %% ../nbs/01_ops.ipynb #23936f1c
|
|
12
|
+
from fastcore.utils import *
|
|
13
|
+
from .core import ConKernelClient, ConKernelManager
|
|
14
|
+
from contextlib import asynccontextmanager
|
|
15
|
+
from ast import literal_eval
|
|
16
|
+
from queue import Empty
|
|
17
|
+
import asyncio, time
|
|
18
|
+
|
|
19
|
+
# %% ../nbs/01_ops.ipynb #d62f6b51
|
|
20
|
+
default_timeout = 10
|
|
21
|
+
|
|
22
|
+
def parent_id(msg):
|
|
23
|
+
"The `msg_id` of the request this `msg` responds to, or None"
|
|
24
|
+
return nested_idx(msg, "parent_header", "msg_id") or None
|
|
25
|
+
|
|
26
|
+
def iter_timeout(timeout=None, default=default_timeout):
|
|
27
|
+
"Yield remaining seconds until `timeout` expires, using monotonic time"
|
|
28
|
+
end = time.monotonic() + (timeout or default)
|
|
29
|
+
while (rem := end - time.monotonic()) > 0: yield rem
|
|
30
|
+
|
|
31
|
+
# %% ../nbs/01_ops.ipynb #ad596b2b
|
|
32
|
+
@patch
|
|
33
|
+
async def iopub_drain(self:ConKernelClient, msg_id, timeout=default_timeout):
|
|
34
|
+
"Collect iopub messages parented to `msg_id` until its idle status arrives. Other requests' messages are discarded: one drain at a time"
|
|
35
|
+
res = []
|
|
36
|
+
for rem in iter_timeout(timeout):
|
|
37
|
+
try: msg = await self.get_iopub_msg(timeout=rem)
|
|
38
|
+
except Empty: continue
|
|
39
|
+
if parent_id(msg) != msg_id: continue
|
|
40
|
+
res.append(msg)
|
|
41
|
+
if msg.get('msg_type') == 'status' and nested_idx(msg, 'content', 'execution_state') == 'idle': return res
|
|
42
|
+
raise TimeoutError(f"iopub idle not seen for {msg_id}")
|
|
43
|
+
|
|
44
|
+
# %% ../nbs/01_ops.ipynb #e705c283
|
|
45
|
+
@patch
|
|
46
|
+
async def iopub_flush(self:ConKernelClient, timeout=0.1):
|
|
47
|
+
"Discard all pending iopub messages, e.g. leftovers from fire-and-forget executes"
|
|
48
|
+
try:
|
|
49
|
+
while True: await self.get_iopub_msg(timeout=timeout)
|
|
50
|
+
except Empty: pass
|
|
51
|
+
|
|
52
|
+
# %% ../nbs/01_ops.ipynb #add288fe
|
|
53
|
+
output_types = {'stream', 'execute_result', 'display_data', 'error'}
|
|
54
|
+
|
|
55
|
+
def nb_outputs(msgs):
|
|
56
|
+
"Convert iopub `msgs` to nbformat-style output dicts, dropping non-output messages"
|
|
57
|
+
res = []
|
|
58
|
+
for m in msgs:
|
|
59
|
+
if (mt := m['msg_type']) not in output_types: continue
|
|
60
|
+
d = dict(output_type=mt, **m['content'])
|
|
61
|
+
d.pop('transient', None)
|
|
62
|
+
res.append(d)
|
|
63
|
+
return res
|
|
64
|
+
|
|
65
|
+
# %% ../nbs/01_ops.ipynb #ebdbd6eb
|
|
66
|
+
def iopub_msgs(msgs, msg_type=None):
|
|
67
|
+
"Filter iopub `msgs` by `msg_type` - a single type, or a collection of types (all messages if None)"
|
|
68
|
+
if msg_type is None: return msgs
|
|
69
|
+
types = {msg_type} if isinstance(msg_type, str) else msg_type
|
|
70
|
+
return [m for m in msgs if m['msg_type'] in types]
|
|
71
|
+
|
|
72
|
+
def iopub_streams(msgs, name=None):
|
|
73
|
+
"The stream messages in `msgs`, optionally only stream `name` ('stdout'/'stderr')"
|
|
74
|
+
streams = iopub_msgs(msgs, 'stream')
|
|
75
|
+
return streams if name is None else [m for m in streams if m['content'].get('name') == name]
|
|
76
|
+
|
|
77
|
+
# %% ../nbs/01_ops.ipynb #52e7c8f1
|
|
78
|
+
@patch
|
|
79
|
+
async def exec_drain(self:ConKernelClient, code, timeout=default_timeout, **kw):
|
|
80
|
+
"Execute `code`; return `(reply, outputs)` where outputs are the request's iopub messages"
|
|
81
|
+
reply = await self.execute(code, reply=True, timeout=timeout, **kw)
|
|
82
|
+
return reply, await self.iopub_drain(parent_id(reply), timeout=timeout)
|
|
83
|
+
|
|
84
|
+
@patch
|
|
85
|
+
async def exec_ok(self:ConKernelClient, code, timeout=default_timeout, **kw):
|
|
86
|
+
"`exec_drain`, asserting the reply status is ok"
|
|
87
|
+
reply, outputs = await self.exec_drain(code, timeout=timeout, **kw)
|
|
88
|
+
assert reply['content']['status'] == 'ok', reply['content']
|
|
89
|
+
return reply, outputs
|
|
90
|
+
|
|
91
|
+
@patch
|
|
92
|
+
async def exec_outs(self:ConKernelClient, code, timeout=default_timeout, **kw):
|
|
93
|
+
"Execute `code` and return just its nbformat-style outputs"
|
|
94
|
+
reply, outputs = await self.exec_drain(code, timeout=timeout, **kw)
|
|
95
|
+
return nb_outputs(outputs)
|
|
96
|
+
|
|
97
|
+
# %% ../nbs/01_ops.ipynb #77ab9da4
|
|
98
|
+
class EvalError(Exception):
|
|
99
|
+
"An `eval_expr` expression raised in the kernel"
|
|
100
|
+
|
|
101
|
+
def parse_expr(s):
|
|
102
|
+
"The `literal_eval` of `s` when its form allows, else `s` unchanged"
|
|
103
|
+
try: return literal_eval(s)
|
|
104
|
+
except Exception: return s
|
|
105
|
+
|
|
106
|
+
@patch
|
|
107
|
+
async def user_exprs(self:ConKernelClient, exprs:dict, code:str='', timeout=default_timeout, **kw):
|
|
108
|
+
"Run `code` and evaluate each of the `exprs` expressions in the same round trip; returns the reply content (statuses and mimebundles unparsed)"
|
|
109
|
+
return (await self.execute(code, reply=True, timeout=timeout, user_expressions=exprs, store_history=False, **kw))['content']
|
|
110
|
+
|
|
111
|
+
@patch
|
|
112
|
+
async def eval_expr(self:ConKernelClient, expr:str, code:str='', timeout=default_timeout, **kw):
|
|
113
|
+
"Evaluate `expr` in the kernel (optionally after running `code`) and return its value: parsed via `literal_eval` when its repr allows, else the repr string. Raises `EvalError` if the kernel raises"
|
|
114
|
+
cts = await self.user_exprs({'_': expr}, code, timeout=timeout, **kw)
|
|
115
|
+
if cts['status'] != 'ok': raise EvalError(f"{cts.get('ename')}: {cts.get('evalue')}")
|
|
116
|
+
r = cts['user_expressions']['_']
|
|
117
|
+
if r.get('status') != 'ok': raise EvalError(f"{r.get('ename')}: {r.get('evalue')}")
|
|
118
|
+
return parse_expr(r['data']['text/plain'])
|
|
119
|
+
|
|
120
|
+
# %% ../nbs/01_ops.ipynb #aafc1ab4
|
|
121
|
+
@patch
|
|
122
|
+
def shell_request(
|
|
123
|
+
self:ConKernelClient, msg_type, timeout=default_timeout, reply=True, buffers=None,
|
|
124
|
+
msg_id=None, # Override the auto-generated message id
|
|
125
|
+
subshell_id=None, # Route via this subshell (header field)
|
|
126
|
+
**content
|
|
127
|
+
):
|
|
128
|
+
"Send an arbitrary shell request, routed through the reply reader like `execute`. Returns a coroutine for the reply, or just the msg_id when `reply=False` (for fire-and-forget types like `comm_open` that never get replies)"
|
|
129
|
+
if not self._check_alive(): return asyncio.sleep(0) if reply else None
|
|
130
|
+
msg = self.session.msg(msg_type, content)
|
|
131
|
+
if msg_id is not None: msg['header']['msg_id'] = msg_id
|
|
132
|
+
if subshell_id is not None: msg['header']['subshell_id'] = subshell_id
|
|
133
|
+
mid = msg['header']['msg_id']
|
|
134
|
+
if reply: self._pending[mid] = (asyncio.Queue(maxsize=1), False)
|
|
135
|
+
if buffers: self.session.send(self.shell_channel.socket, msg, buffers=buffers)
|
|
136
|
+
else: self.shell_channel.send(msg)
|
|
137
|
+
return self._async_recv_reply(mid, timeout=timeout) if reply else mid
|
|
138
|
+
|
|
139
|
+
@patch
|
|
140
|
+
async def control_request(self:ConKernelClient, msg_type, timeout=default_timeout, **content):
|
|
141
|
+
"Send a request on the control channel and await its matching reply"
|
|
142
|
+
msg = self.session.msg(msg_type, content)
|
|
143
|
+
self.control_channel.send(msg)
|
|
144
|
+
for rem in iter_timeout(timeout):
|
|
145
|
+
try: reply = await self.get_control_msg(timeout=rem)
|
|
146
|
+
except Empty: continue
|
|
147
|
+
if parent_id(reply) == msg['header']['msg_id']: return reply
|
|
148
|
+
raise TimeoutError(f"no reply to {msg_type}")
|
|
149
|
+
|
|
150
|
+
# %% ../nbs/01_ops.ipynb #4f01d2b6
|
|
151
|
+
@patch
|
|
152
|
+
async def input_reply(self:ConKernelClient, value:str):
|
|
153
|
+
"Answer the kernel's pending input_request (from `input()` in an execute sent with `allow_stdin=True`)"
|
|
154
|
+
await self.stdin_send(self.session.msg('input_reply', dict(value=value)))
|
|
155
|
+
|
|
156
|
+
# %% ../nbs/01_ops.ipynb #97d75b1e
|
|
157
|
+
class _ReqProxy:
|
|
158
|
+
"Attribute access to protocol requests: `kc.cmd.complete(code=...)` sends `complete_request` on shell"
|
|
159
|
+
def __init__(self, kc, send, suffix='_request'): self.kc,self.send,self.suffix = kc,send,suffix
|
|
160
|
+
def __getattr__(self, name):
|
|
161
|
+
if name.startswith('_'): raise AttributeError(name)
|
|
162
|
+
def _call(*, timeout=default_timeout, **content): return self.send(f"{name}{self.suffix}", timeout=timeout, **content)
|
|
163
|
+
return _call
|
|
164
|
+
|
|
165
|
+
@patch(as_prop=True)
|
|
166
|
+
def cmd(self:ConKernelClient):
|
|
167
|
+
"Shell requests as methods: `await kc.cmd.history(...)` sends `history_request`"
|
|
168
|
+
return _ReqProxy(self, self.shell_request)
|
|
169
|
+
|
|
170
|
+
@patch(as_prop=True)
|
|
171
|
+
def ctl(self:ConKernelClient):
|
|
172
|
+
"Control requests as methods: `await kc.ctl.shutdown(restart=True)` sends `shutdown_request`"
|
|
173
|
+
return _ReqProxy(self, self.control_request)
|
|
174
|
+
|
|
175
|
+
# %% ../nbs/01_ops.ipynb #af8183ea
|
|
176
|
+
class _DapProxy:
|
|
177
|
+
"DAP requests as methods: `kc.dap.stackTrace(threadId=1)` sends a `debug_request` on control"
|
|
178
|
+
def __init__(self, kc): self.kc,self.seq = kc,0
|
|
179
|
+
def __getattr__(self, command):
|
|
180
|
+
if command.startswith('_'): raise AttributeError(command)
|
|
181
|
+
if command.endswith('_') and not command.endswith('__'): command = command[:-1]
|
|
182
|
+
async def _call(*, timeout=default_timeout, full=False, **arguments):
|
|
183
|
+
self.seq += 1
|
|
184
|
+
r = await self.kc.control_request('debug_request', timeout=timeout,
|
|
185
|
+
type='request', seq=self.seq, command=command, arguments=arguments)
|
|
186
|
+
return r if full else r['content']
|
|
187
|
+
return _call
|
|
188
|
+
|
|
189
|
+
@patch(as_prop=True)
|
|
190
|
+
def dap(self:ConKernelClient):
|
|
191
|
+
"DAP debug requests as methods. Stateful (`seq` counter), so cached per client, unlike `cmd`/`ctl`"
|
|
192
|
+
if getattr(self, '_dap', None) is None: self._dap = _DapProxy(self)
|
|
193
|
+
return self._dap
|
|
194
|
+
|
|
195
|
+
# %% ../nbs/01_ops.ipynb #8b1967f4
|
|
196
|
+
@patch
|
|
197
|
+
async def interrupt(self:ConKernelClient, timeout=5):
|
|
198
|
+
"Interrupt the running request via `interrupt_request` on control; returns the reply"
|
|
199
|
+
return await self.control_request('interrupt_request', timeout=timeout)
|
|
200
|
+
|
|
201
|
+
# %% ../nbs/01_ops.ipynb #edaf42c9
|
|
202
|
+
@asynccontextmanager
|
|
203
|
+
async def run_kernel(
|
|
204
|
+
kernel_name='python3', # Kernelspec name to launch
|
|
205
|
+
manager_cls=ConKernelManager, # Manager class, e.g. a subclass customizing launch
|
|
206
|
+
**kwargs # Passed to `start_kernel` (e.g. `env`, `cwd`)
|
|
207
|
+
):
|
|
208
|
+
"Start a kernel, yield `(km, kc)` with channels running, and always shut both down"
|
|
209
|
+
km = manager_cls(kernel_name=kernel_name)
|
|
210
|
+
await km.start_kernel(**kwargs)
|
|
211
|
+
kc = await km.client().start_channels()
|
|
212
|
+
try: yield km, kc
|
|
213
|
+
finally:
|
|
214
|
+
kc.stop_channels()
|
|
215
|
+
await km.shutdown_kernel(now=True)
|
|
216
|
+
|
|
217
|
+
# %% ../nbs/01_ops.ipynb #839e20f6
|
|
218
|
+
async def reconnect(km, kc=None):
|
|
219
|
+
"Restart `km`'s kernel (fresh process, state discarded) and return a newly connected client, stopping `kc` if given"
|
|
220
|
+
if kc is not None: kc.stop_channels()
|
|
221
|
+
await km.restart_kernel(now=True)
|
|
222
|
+
return await km.client().start_channels()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conkernelclient
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: Concurrent-safe Jupyter KernelClient
|
|
5
5
|
Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -13,8 +13,8 @@ Requires-Python: >=3.10
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: jupyter_client
|
|
16
|
+
Requires-Dist: fastcore
|
|
16
17
|
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: fastcore; extra == "dev"
|
|
18
18
|
Requires-Dist: ipykernel; extra == "dev"
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
@@ -125,3 +125,4 @@ if await km.is_alive():
|
|
|
125
125
|
kc.stop_channels()
|
|
126
126
|
await km.shutdown_kernel()
|
|
127
127
|
```
|
|
128
|
+
|
|
@@ -16,12 +16,12 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
'jupyter_client'
|
|
19
|
+
'jupyter_client',
|
|
20
|
+
'fastcore',
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
[project.optional-dependencies]
|
|
23
24
|
dev = [
|
|
24
|
-
"fastcore",
|
|
25
25
|
"ipykernel",
|
|
26
26
|
]
|
|
27
27
|
|
|
@@ -41,3 +41,6 @@ include = ["conkernelclient"]
|
|
|
41
41
|
[tool.nbdev]
|
|
42
42
|
allowed_metadata_keys = ['solveit']
|
|
43
43
|
allowed_cell_metadata_keys = ['solveit_ai']
|
|
44
|
+
|
|
45
|
+
[tool.chkstyle]
|
|
46
|
+
skip_paths = ["_modidx.py"]
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Autogenerated by nbdev
|
|
2
|
-
|
|
3
|
-
d = { 'settings': { 'branch': 'main',
|
|
4
|
-
'doc_baseurl': '/conkernelclient',
|
|
5
|
-
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
|
-
'git_url': 'https://github.com/AnswerDotAI/conkernelclient',
|
|
7
|
-
'lib_path': 'conkernelclient'},
|
|
8
|
-
'syms': { 'conkernelclient.core': { 'conkernelclient.core.ConKernelClient': ('core.html#conkernelclient', 'conkernelclient/core.py'),
|
|
9
|
-
'conkernelclient.core.ConKernelClient._async_recv_reply': ( 'core.html#conkernelclient._async_recv_reply',
|
|
10
|
-
'conkernelclient/core.py'),
|
|
11
|
-
'conkernelclient.core.ConKernelClient._check_alive': ( 'core.html#conkernelclient._check_alive',
|
|
12
|
-
'conkernelclient/core.py'),
|
|
13
|
-
'conkernelclient.core.ConKernelClient._fail_pending': ( 'core.html#conkernelclient._fail_pending',
|
|
14
|
-
'conkernelclient/core.py'),
|
|
15
|
-
'conkernelclient.core.ConKernelClient.execute': ( 'core.html#conkernelclient.execute',
|
|
16
|
-
'conkernelclient/core.py'),
|
|
17
|
-
'conkernelclient.core.ConKernelClient.start_channels': ( 'core.html#conkernelclient.start_channels',
|
|
18
|
-
'conkernelclient/core.py'),
|
|
19
|
-
'conkernelclient.core.ConKernelClient.stdin_send': ( 'core.html#conkernelclient.stdin_send',
|
|
20
|
-
'conkernelclient/core.py'),
|
|
21
|
-
'conkernelclient.core.ConKernelClient.stop_channels': ( 'core.html#conkernelclient.stop_channels',
|
|
22
|
-
'conkernelclient/core.py'),
|
|
23
|
-
'conkernelclient.core.ConKernelManager': ('core.html#conkernelmanager', 'conkernelclient/core.py'),
|
|
24
|
-
'conkernelclient.core.DeadKernelError': ('core.html#deadkernelerror', 'conkernelclient/core.py'),
|
|
25
|
-
'conkernelclient.core._send': ('core.html#_send', 'conkernelclient/core.py')}}}
|
|
File without changes
|
|
File without changes
|
{conkernelclient-0.0.11 → conkernelclient-0.0.13}/conkernelclient.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|