conkernelclient 0.0.19__tar.gz → 0.0.20__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conkernelclient
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: Concurrent-safe Jupyter KernelClient
5
5
  Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
6
6
  License: Apache-2.0
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: jupyter_client
16
16
  Requires-Dist: fastcore>=2.1.17
17
- Requires-Dist: jupywire>=0.1.4
17
+ Requires-Dist: jupywire>=0.1.5
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: ipykernel; extra == "dev"
20
20
  Requires-Dist: ipymini>=0.1.17; extra == "dev"
@@ -0,0 +1,4 @@
1
+ __version__ = "0.0.20"
2
+ from jupywire.route import JmsgQueues, DeadKernelError
3
+ from .core import *
4
+ from .ops import *
@@ -8,20 +8,22 @@ d = { 'settings': { 'branch': 'main',
8
8
  'syms': { 'conkernelclient.core': { 'conkernelclient.core.ConKernelClient': ('core.html#conkernelclient', 'conkernelclient/core.py'),
9
9
  'conkernelclient.core.ConKernelClient.__init__': ( 'core.html#conkernelclient.__init__',
10
10
  'conkernelclient/core.py'),
11
- 'conkernelclient.core.ConKernelClient._async_recv_reply': ( 'core.html#conkernelclient._async_recv_reply',
12
- 'conkernelclient/core.py'),
13
11
  'conkernelclient.core.ConKernelClient._check_alive': ( 'core.html#conkernelclient._check_alive',
14
12
  'conkernelclient/core.py'),
15
- 'conkernelclient.core.ConKernelClient._fail_pending': ( 'core.html#conkernelclient._fail_pending',
16
- 'conkernelclient/core.py'),
13
+ 'conkernelclient.core.ConKernelClient._pump': ( 'core.html#conkernelclient._pump',
14
+ 'conkernelclient/core.py'),
15
+ 'conkernelclient.core.ConKernelClient._route': ( 'core.html#conkernelclient._route',
16
+ 'conkernelclient/core.py'),
17
+ 'conkernelclient.core.ConKernelClient._watch_hb': ( 'core.html#conkernelclient._watch_hb',
18
+ 'conkernelclient/core.py'),
17
19
  'conkernelclient.core.ConKernelClient.astop_channels': ( 'core.html#conkernelclient.astop_channels',
18
20
  'conkernelclient/core.py'),
19
21
  'conkernelclient.core.ConKernelClient.execute': ( 'core.html#conkernelclient.execute',
20
22
  'conkernelclient/core.py'),
23
+ 'conkernelclient.core.ConKernelClient.send': ( 'core.html#conkernelclient.send',
24
+ 'conkernelclient/core.py'),
21
25
  'conkernelclient.core.ConKernelClient.start_channels': ( 'core.html#conkernelclient.start_channels',
22
26
  'conkernelclient/core.py'),
23
- 'conkernelclient.core.ConKernelClient.stdin_send': ( 'core.html#conkernelclient.stdin_send',
24
- 'conkernelclient/core.py'),
25
27
  'conkernelclient.core.ConKernelClient.stop_channels': ( 'core.html#conkernelclient.stop_channels',
26
28
  'conkernelclient/core.py'),
27
29
  'conkernelclient.core.ConKernelClient.wait_for_ready': ( 'core.html#conkernelclient.wait_for_ready',
@@ -31,7 +33,7 @@ d = { 'settings': { 'branch': 'main',
31
33
  'conkernelclient/core.py'),
32
34
  'conkernelclient.core.ConKernelManager._transport_encryption_default': ( 'core.html#conkernelmanager._transport_encryption_default',
33
35
  'conkernelclient/core.py'),
34
- 'conkernelclient.core.DeadKernelError': ('core.html#deadkernelerror', 'conkernelclient/core.py'),
36
+ 'conkernelclient.core._dead_status': ('core.html#_dead_status', 'conkernelclient/core.py'),
35
37
  'conkernelclient.core._send': ('core.html#_send', 'conkernelclient/core.py'),
36
38
  'conkernelclient.core.apply_session_patch': ( 'core.html#apply_session_patch',
37
39
  'conkernelclient/core.py')},
@@ -42,14 +44,10 @@ d = { 'settings': { 'branch': 'main',
42
44
  'conkernelclient/ops.py'),
43
45
  'conkernelclient.ops.ConKernelClient.ctl': ('ops.html#conkernelclient.ctl', 'conkernelclient/ops.py'),
44
46
  'conkernelclient.ops.ConKernelClient.dap': ('ops.html#conkernelclient.dap', 'conkernelclient/ops.py'),
45
- 'conkernelclient.ops.ConKernelClient.eval_expr': ( 'ops.html#conkernelclient.eval_expr',
46
- 'conkernelclient/ops.py'),
47
47
  'conkernelclient.ops.ConKernelClient.exec_drain': ( 'ops.html#conkernelclient.exec_drain',
48
48
  'conkernelclient/ops.py'),
49
49
  'conkernelclient.ops.ConKernelClient.exec_ok': ( 'ops.html#conkernelclient.exec_ok',
50
50
  'conkernelclient/ops.py'),
51
- 'conkernelclient.ops.ConKernelClient.exec_outs': ( 'ops.html#conkernelclient.exec_outs',
52
- 'conkernelclient/ops.py'),
53
51
  'conkernelclient.ops.ConKernelClient.input_reply': ( 'ops.html#conkernelclient.input_reply',
54
52
  'conkernelclient/ops.py'),
55
53
  'conkernelclient.ops.ConKernelClient.interrupt': ( 'ops.html#conkernelclient.interrupt',
@@ -58,13 +56,8 @@ d = { 'settings': { 'branch': 'main',
58
56
  'conkernelclient/ops.py'),
59
57
  'conkernelclient.ops.ConKernelClient.iopub_flush': ( 'ops.html#conkernelclient.iopub_flush',
60
58
  'conkernelclient/ops.py'),
61
- 'conkernelclient.ops.ConKernelClient.reply': ( 'ops.html#conkernelclient.reply',
62
- 'conkernelclient/ops.py'),
63
59
  'conkernelclient.ops.ConKernelClient.shell_request': ( 'ops.html#conkernelclient.shell_request',
64
60
  'conkernelclient/ops.py'),
65
- 'conkernelclient.ops.ConKernelClient.user_exprs': ( 'ops.html#conkernelclient.user_exprs',
66
- 'conkernelclient/ops.py'),
67
- 'conkernelclient.ops.EvalError': ('ops.html#evalerror', 'conkernelclient/ops.py'),
68
61
  'conkernelclient.ops._DapProxy': ('ops.html#_dapproxy', 'conkernelclient/ops.py'),
69
62
  'conkernelclient.ops._DapProxy.__getattr__': ( 'ops.html#_dapproxy.__getattr__',
70
63
  'conkernelclient/ops.py'),
@@ -75,6 +68,5 @@ d = { 'settings': { 'branch': 'main',
75
68
  'conkernelclient.ops._ReqProxy.__init__': ('ops.html#_reqproxy.__init__', 'conkernelclient/ops.py'),
76
69
  'conkernelclient.ops.iopub_streams': ('ops.html#iopub_streams', 'conkernelclient/ops.py'),
77
70
  'conkernelclient.ops.iter_timeout': ('ops.html#iter_timeout', 'conkernelclient/ops.py'),
78
- 'conkernelclient.ops.parse_expr': ('ops.html#parse_expr', 'conkernelclient/ops.py'),
79
71
  'conkernelclient.ops.reconnect': ('ops.html#reconnect', 'conkernelclient/ops.py'),
80
72
  'conkernelclient.ops.run_kernel': ('ops.html#run_kernel', 'conkernelclient/ops.py')}}}
@@ -5,7 +5,7 @@ Docs: https://AnswerDotAI.github.io/conkernelclient/core.html.md"""
5
5
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
6
6
 
7
7
  # %% auto #0
8
- __all__ = ['DeadKernelError', 'apply_session_patch', 'ConKernelClient', 'ConKernelManager']
8
+ __all__ = ['apply_session_patch', 'ConKernelClient', 'ConKernelManager']
9
9
 
10
10
  # %% ../nbs/00_core.ipynb #d3b521c0
11
11
  from jupyter_client import AsyncKernelClient, AsyncKernelManager
@@ -14,15 +14,13 @@ from zmq.error import ZMQError
14
14
  from traitlets import Type, default
15
15
  from fastcore.utils import patch
16
16
  from jupywire.ops import EvalOps
17
+ from jupywire.route import RouterOps, DeadKernelError
17
18
  from queue import Empty
18
- import asyncio, zmq.asyncio, time, logging
19
+ import asyncio, inspect, zmq.asyncio, time, logging
19
20
 
20
21
  # %% ../nbs/00_core.ipynb #737a0fc1
21
22
  _log = logging.getLogger(__name__)
22
23
 
23
- # %% ../nbs/00_core.ipynb #a093006c
24
- class DeadKernelError(Exception): pass
25
-
26
24
  # %% ../nbs/00_core.ipynb #374b75d0
27
25
  def _send(self, stream, msg_or_type, content=None, parent=None, ident=None,
28
26
  buffers=None, track=False, header=None, metadata=None):
@@ -48,7 +46,12 @@ def apply_session_patch():
48
46
  Session.send = _send
49
47
 
50
48
  # %% ../nbs/00_core.ipynb #d6a5fa6a
51
- class ConKernelClient(EvalOps, AsyncKernelClient):
49
+ def _dead_status():
50
+ "The dead `status` rustygate broadcasts, synthesized client-side for zmq transport loss and heartbeat silence"
51
+ return dict(channel='iopub', msg_type='status', header={}, parent_header={}, content=dict(execution_state='dead'), buffers=[])
52
+
53
+
54
+ class ConKernelClient(RouterOps, EvalOps, AsyncKernelClient):
52
55
  def __init__(self, *args, **kwargs):
53
56
  apply_session_patch()
54
57
  # jupyter_client's `get_connection_info` returns curve keys as str, but the client traits are `Bytes`
@@ -56,86 +59,81 @@ class ConKernelClient(EvalOps, AsyncKernelClient):
56
59
  if isinstance(kwargs.get(k), str): kwargs[k] = kwargs[k].encode()
57
60
  super().__init__(*args, **kwargs)
58
61
 
59
- def _fail_pending(self, exc:Exception, skip=None):
60
- for k,(q,_) in list(getattr(self, '_pending', {}).items()):
61
- if k != skip:
62
- try: q.put_nowait(exc)
63
- except asyncio.QueueFull: pass
64
-
65
62
  def _check_alive(self):
66
63
  if not self.channels_running: raise RuntimeError("Channels not running")
67
- tk = getattr(self, '_shell_reader_task', None)
68
- return tk is not None and not tk.done()
69
-
70
- async def start_channels(self, shell:bool=True, iopub:bool=True, stdin:bool=True, hb:bool=True, control:bool=True):
71
- "Start channels, wait for ready, and launch background shell-reply reader"
72
- super().start_channels(shell=shell, iopub=iopub, stdin=stdin, hb=hb, control=control)
73
- await self.wait_for_ready()
74
- self._pending = {}
75
- _ready = asyncio.Event()
76
- async def _reader():
77
- _ready.set()
78
- while True:
79
- try: reply = await self.get_shell_msg(timeout=None)
80
- except Exception as e:
81
- if isinstance(e, (AssertionError, OSError, ZMQError)): e = DeadKernelError(f"Kernel socket closed: {e}")
82
- self._fail_pending(e)
83
- _log.warning(f"_reader died, pending={list(self._pending)}: {e}")
84
- break
85
- mid = reply["parent_header"].get("msg_id")
86
- pend = self._pending.get(mid)
87
- if pend:
88
- q, soe = pend
89
- try: q.put_nowait(reply)
90
- except asyncio.QueueFull: pass
91
- cts = reply.get("content", {})
92
- if cts.get("status") in ("error", "aborted") and pend and soe:
93
- exc = RuntimeError(f"Kernel error aborted: {cts.get('ename')}: {cts.get('evalue')}")
94
- self._fail_pending(exc, skip=mid)
95
- self._shell_reader_task = asyncio.create_task(_reader())
96
- await _ready.wait()
97
- return self
98
-
99
- def stop_channels(self):
100
- "Stop channels and cancel the background shell-reply reader task"
101
- self._fail_pending(RuntimeError("Shell channels stopped before reply"))
102
- super().stop_channels()
103
- if (tk := getattr(self, '_shell_reader_task', None)):
104
- tk.cancel()
105
- self._shell_reader_task = None
106
-
107
- async def _async_recv_reply(self, msg_id, timeout=None, channel="shell"):
108
- if channel == "control": return await self._async_get_control_msg(timeout=timeout)
109
- q, _ = self._pending[msg_id]
110
- try:
111
- res = await asyncio.wait_for(q.get(), timeout)
112
- if isinstance(res, Exception): raise res
113
- return res
114
- finally: self._pending.pop(msg_id, None)
115
-
116
- def execute(self, code, user_expressions=None, allow_stdin=None, reply=False, subsh_id=None,
117
- cts_typ='code', timeout=60, msg_id=None, stop_on_error=True, fail_pending=False, **kw):
118
- "Send an execute request, returning a coroutine for the reply if `reply`, else the msg_id"
119
- if not self._check_alive(): return asyncio.sleep(0) if reply else None
120
- if user_expressions is None: user_expressions = {}
121
- if allow_stdin is None: allow_stdin = self.allow_stdin
122
- content = dict(user_expressions=user_expressions, allow_stdin=allow_stdin, subsh_id=subsh_id, stop_on_error=stop_on_error, **kw)
123
- content[cts_typ] = code
124
- msg = self.session.msg("execute_request", content)
125
- if msg_id is not None: msg["header"]["msg_id"] = msg_id
126
- if subsh_id is not None: msg["header"]["subshell_id"] = subsh_id
127
- msg_id = msg["header"]["msg_id"]
128
- if reply: self._pending[msg_id] = (asyncio.Queue(maxsize=1), stop_on_error if fail_pending is None else fail_pending)
129
- try: self.shell_channel.send(msg)
130
- except (AssertionError, OSError, ZMQError) as e: raise DeadKernelError(f"Kernel socket closed: {e}") from e
131
- if not reply: return msg_id
132
- return self._async_recv_reply(msg_id, timeout=timeout)
133
-
134
- async def stdin_send(self, msg:dict):
135
- "Send on `stdin_channel` then ping event loop"
136
- try: self.stdin_channel.send(msg)
137
- except (AssertionError, OSError, ZMQError) as e: raise DeadKernelError(f"Kernel socket closed: {e}") from e
138
- await asyncio.sleep(0.01)
64
+ pumps = getattr(self, '_pumps', None)
65
+ if not pumps or any(t.done() for t in pumps): raise DeadKernelError('kernel died')
66
+
67
+ # %% ../nbs/00_core.ipynb #fd6e6aa9
68
+ @patch
69
+ async def _route(self:ConKernelClient, msg):
70
+ r = self.route(msg)
71
+ if inspect.isawaitable(r): await r
72
+
73
+ @patch
74
+ async def _pump(self:ConKernelClient, name):
75
+ "Drain one zmq channel into the router; a socket error is transport loss, delivered as the dead status."
76
+ ch = getattr(self, f'{name}_channel')
77
+ try:
78
+ while True: await self._route(dict(await ch.get_msg(timeout=None), channel=name))
79
+ except asyncio.CancelledError: raise
80
+ except Exception as e:
81
+ _log.warning(f"{name} pump died: {e}")
82
+ await self._route(_dead_status())
83
+
84
+ @patch
85
+ async def _watch_hb(self:ConKernelClient):
86
+ "Declare death after three consecutive unanswered heartbeat echoes; the kernel echoes from a dedicated thread, so sustained silence means the process is gone."
87
+ misses = 0
88
+ while True:
89
+ await asyncio.sleep(self.hb_channel.time_to_dead)
90
+ misses = 0 if self.hb_channel.is_beating() else misses + 1
91
+ if misses >= 3: return await self._route(_dead_status())
92
+
93
+ # %% ../nbs/00_core.ipynb #394db3b2
94
+ @patch
95
+ async def start_channels(self:ConKernelClient, shell:bool=True, iopub:bool=True, stdin:bool=True, hb:bool=True, control:bool=True):
96
+ "Start channels, wait for ready, then launch the router pumps and the heartbeat watcher"
97
+ super(ConKernelClient, self).start_channels(shell=shell, iopub=iopub, stdin=stdin, hb=hb, control=control)
98
+ await self.wait_for_ready()
99
+ self._init_router()
100
+ chans = [c for c, on in (('shell',shell), ('iopub',iopub), ('stdin',stdin), ('control',control)) if on]
101
+ self._pumps = [asyncio.create_task(self._pump(c)) for c in chans]
102
+ if hb: self._pumps.append(asyncio.create_task(self._watch_hb()))
103
+ return self
104
+
105
+ @patch
106
+ def stop_channels(self:ConKernelClient):
107
+ "Stop channels and cancel the router pumps"
108
+ if getattr(self, '_pumps', None): self.fail_waiters(RuntimeError("Channels stopped before reply"))
109
+ super(ConKernelClient, self).stop_channels()
110
+ for t in getattr(self, '_pumps', []): t.cancel()
111
+ self._pumps = []
112
+
113
+ # %% ../nbs/00_core.ipynb #a930c688
114
+ @patch
115
+ def send(self:ConKernelClient, msg, channel):
116
+ "Transmit a built message dict on `channel` (buffers ride as extra frames); a closed socket raises `DeadKernelError`."
117
+ ch = getattr(self, f'{channel}_channel')
118
+ try:
119
+ if msg.get('buffers'): self.session.send(ch.socket, msg, buffers=msg['buffers'])
120
+ else: ch.send(msg)
121
+ except (AssertionError, OSError, ZMQError) as e: raise DeadKernelError(f"Kernel socket closed: {e}") from e
122
+
123
+ @patch
124
+ def execute(self:ConKernelClient, code, silent=False, store_history=True, user_expressions=None, allow_stdin=None, stop_on_error=True,
125
+ msg_id=None, metadata=None, subshell_id=None, buffers=None):
126
+ "Send an `execute_request`, fire-and-forget; returns its msg_id"
127
+ self._check_alive()
128
+ if user_expressions is None: user_expressions = {}
129
+ if allow_stdin is None: allow_stdin = self.allow_stdin
130
+ msg = self.session.msg("execute_request", dict(code=code, silent=silent, store_history=store_history,
131
+ user_expressions=user_expressions, allow_stdin=allow_stdin, stop_on_error=stop_on_error), metadata=metadata)
132
+ if buffers: msg["buffers"] = buffers
133
+ if msg_id is not None: msg["header"]["msg_id"] = msg_id
134
+ if subshell_id is not None: msg["header"]["subshell_id"] = subshell_id
135
+ self.send(msg, 'shell')
136
+ return msg["header"]["msg_id"]
139
137
 
140
138
  # %% ../nbs/00_core.ipynb #3771c654
141
139
  @patch
@@ -174,13 +172,13 @@ async def wait_for_ready(self:ConKernelClient, timeout=None):
174
172
  # %% ../nbs/00_core.ipynb #967c10a1
175
173
  @patch
176
174
  async def astop_channels(self:ConKernelClient):
177
- "Stop channels and cancel the background shell-reply reader, awaiting its exit"
178
- self._fail_pending(RuntimeError("Shell channels stopped before reply"))
179
- if (tk := getattr(self, '_shell_reader_task', None)):
180
- tk.cancel()
181
- try: await tk
175
+ "Stop channels and cancel the router pumps, awaiting their exit"
176
+ if getattr(self, '_pumps', None): self.fail_waiters(RuntimeError("Channels stopped before reply"))
177
+ for t in getattr(self, '_pumps', []):
178
+ t.cancel()
179
+ try: await t
182
180
  except asyncio.CancelledError: pass
183
- self._shell_reader_task = None
181
+ self._pumps = []
184
182
  super(ConKernelClient, self).stop_channels()
185
183
 
186
184
  # %% ../nbs/00_core.ipynb #b828c222
@@ -5,21 +5,17 @@ Docs: https://AnswerDotAI.github.io/conkernelclient/ops.html.md"""
5
5
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_ops.ipynb.
6
6
 
7
7
  # %% auto #0
8
- __all__ = ['default_timeout', 'iter_timeout', 'iopub_streams', 'EvalError', 'parse_expr', 'run_kernel', 'reconnect', 'parent_id',
9
- 'iopub_msgs', 'output_types']
8
+ __all__ = ['default_timeout', 'iter_timeout', 'iopub_streams', 'run_kernel', 'reconnect']
10
9
 
11
10
  # %% ../nbs/01_ops.ipynb #23936f1c
12
11
  from fastcore.utils import *
13
- from fastcore.ansi import strip_ansi
14
- from fastcore.nbio import msgs2outs, preferred_out
15
12
  from .core import ConKernelClient, ConKernelManager, DeadKernelError
16
- from jupywire.ops import parent_id, iopub_msgs, output_types
13
+ from jupywire.ops import parent_id, iopub_msgs
17
14
  from jupyter_client.kernelspec import KernelSpec
18
- _all_ = ['parent_id', 'iopub_msgs', 'output_types']
19
15
  from contextlib import asynccontextmanager
20
- from ast import literal_eval
21
16
  from queue import Empty
22
- import asyncio, time
17
+ import time
18
+
23
19
 
24
20
  # %% ../nbs/01_ops.ipynb #d62f6b51
25
21
  default_timeout = 10
@@ -31,11 +27,19 @@ def iter_timeout(timeout=None, default=default_timeout):
31
27
 
32
28
  # %% ../nbs/01_ops.ipynb #ad596b2b
33
29
  @patch
30
+ async def exec_drain(self:ConKernelClient, code, timeout=default_timeout, **kw):
31
+ "Execute `code`; return `(reply, outputs)` where outputs are the request's iopub messages"
32
+ msgs = await self.run(code, timeout=timeout, **kw)
33
+ reply = next(m for m in msgs if m['msg_type'] == 'execute_reply')
34
+ return reply, [m for m in msgs if m['channel'] == 'iopub']
35
+
36
+ # %% ../nbs/01_ops.ipynb #113e926f
37
+ @patch
34
38
  async def iopub_drain(self:ConKernelClient, msg_id, timeout=default_timeout):
35
- "Collect iopub messages parented to `msg_id` until its idle status arrives. Other requests' messages are discarded: one drain at a time"
39
+ "Collect unmatched iopub messages parented to `msg_id` from the attached `JmsgQueues` until its idle status arrives. Other requests' messages are discarded: one drain at a time"
36
40
  res = []
37
41
  for rem in iter_timeout(timeout):
38
- try: msg = await self.get_iopub_msg(timeout=rem)
42
+ try: msg = await self.jmsgq.get('iopub', timeout=rem)
39
43
  except Empty: continue
40
44
  if parent_id(msg) != msg_id: continue
41
45
  res.append(msg)
@@ -45,9 +49,9 @@ async def iopub_drain(self:ConKernelClient, msg_id, timeout=default_timeout):
45
49
  # %% ../nbs/01_ops.ipynb #e705c283
46
50
  @patch
47
51
  async def iopub_flush(self:ConKernelClient, timeout=0.1):
48
- "Discard all pending iopub messages, e.g. leftovers from fire-and-forget executes"
52
+ "Discard pending iopub messages from the attached `JmsgQueues`, e.g. leftovers from fire-and-forget executes"
49
53
  try:
50
- while True: await self.get_iopub_msg(timeout=timeout)
54
+ while True: await self.jmsgq.get('iopub', timeout=timeout)
51
55
  except Empty: pass
52
56
 
53
57
  # %% ../nbs/01_ops.ipynb #ebdbd6eb
@@ -57,12 +61,6 @@ def iopub_streams(msgs, name=None):
57
61
  return streams if name is None else [m for m in streams if m['content'].get('name') == name]
58
62
 
59
63
  # %% ../nbs/01_ops.ipynb #52e7c8f1
60
- @patch
61
- async def exec_drain(self:ConKernelClient, code, timeout=default_timeout, **kw):
62
- "Execute `code`; return `(reply, outputs)` where outputs are the request's iopub messages"
63
- reply = await self.execute(code, reply=True, timeout=timeout, **kw)
64
- return reply, await self.iopub_drain(parent_id(reply), timeout=timeout)
65
-
66
64
  @patch
67
65
  async def exec_ok(self:ConKernelClient, code, timeout=default_timeout, **kw):
68
66
  "`exec_drain`, asserting the reply status is ok"
@@ -70,49 +68,7 @@ async def exec_ok(self:ConKernelClient, code, timeout=default_timeout, **kw):
70
68
  assert reply['content']['status'] == 'ok', reply['content']
71
69
  return reply, outputs
72
70
 
73
- @patch
74
- async def exec_outs(self:ConKernelClient, code, timeout=default_timeout, **kw):
75
- "Execute `code` and return just its nbformat-style outputs"
76
- reply, outputs = await self.exec_drain(code, timeout=timeout, **kw)
77
- return msgs2outs(outputs)
78
-
79
- # %% ../nbs/01_ops.ipynb #77ab9da4
80
- class EvalError(Exception):
81
- "An `eval_expr` expression raised in the kernel"
82
-
83
- def parse_expr(s):
84
- "The `literal_eval` of `s` when its form allows, else `s` unchanged"
85
- try: return literal_eval(s)
86
- except Exception: return s
87
-
88
- @patch
89
- async def user_exprs(self:ConKernelClient, exprs:dict, code:str='', timeout=default_timeout, **kw):
90
- "Run `code` and evaluate each of the `exprs` expressions in the same round trip; returns the reply content (statuses and mimebundles unparsed)"
91
- return (await self.execute(code, reply=True, timeout=timeout, user_expressions=exprs, store_history=False, **kw))['content']
92
-
93
- @patch
94
- async def eval_expr(self:ConKernelClient, expr:str, code:str='', timeout=default_timeout, **kw):
95
- "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"
96
- cts = await self.user_exprs({'_': expr}, code, timeout=timeout, **kw)
97
- if cts['status'] != 'ok': raise EvalError(f"{cts.get('ename')}: {cts.get('evalue')}")
98
- r = cts['user_expressions']['_']
99
- if r.get('status') != 'ok': raise EvalError(f"{r.get('ename')}: {r.get('evalue')}")
100
- return parse_expr(r['data']['text/plain'])
101
-
102
- # %% ../nbs/01_ops.ipynb #d9bbaa8d
103
- @patch
104
- def reply(self:ConKernelClient,
105
- code, # A string of code in the kernel's language.
106
- user_expressions:NoneType=None, # A dict mapping names to expressions to be evaluated in the user's dict
107
- allow_stdin:NoneType=None, # Flag for whether the kernel can send stdin requests to frontends.
108
- cts_typ:str='code', timeout:int=None, msg_id:NoneType=None, **kw
109
- ):
110
- return self.execute(code, user_expressions=user_expressions, allow_stdin=allow_stdin, reply=True,
111
- cts_typ=cts_typ, timeout=timeout or getattr(self, 'default_timeout', default_timeout), msg_id=msg_id, **kw)
112
-
113
71
  # %% ../nbs/01_ops.ipynb #5de02acb
114
- from jupywire.ops import EvalOps, EvalException, try_eval
115
-
116
72
  @patch
117
73
  def _pre_ipy(self:ConKernelClient):
118
74
  if not self.shell_channel.is_alive(): raise DeadKernelError()
@@ -120,40 +76,21 @@ def _pre_ipy(self:ConKernelClient):
120
76
 
121
77
  # %% ../nbs/01_ops.ipynb #aafc1ab4
122
78
  @patch
123
- def shell_request(
124
- self:ConKernelClient, msg_type, timeout=default_timeout, reply=True, buffers=None,
125
- msg_id=None, # Override the auto-generated message id
126
- subshell_id=None, # Route via this subshell (header field)
127
- metadata=None, # Message metadata (e.g. ipywidgets control-comm version check)
128
- **content
129
- ):
130
- "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)"
131
- if not self._check_alive(): return asyncio.sleep(0) if reply else None
132
- msg = self.session.msg(msg_type, content, metadata=metadata)
133
- if msg_id is not None: msg['header']['msg_id'] = msg_id
134
- if subshell_id is not None: msg['header']['subshell_id'] = subshell_id
135
- mid = msg['header']['msg_id']
136
- if reply: self._pending[mid] = (asyncio.Queue(maxsize=1), False)
137
- if buffers: self.session.send(self.shell_channel.socket, msg, buffers=buffers)
138
- else: self.shell_channel.send(msg)
139
- return self._async_recv_reply(mid, timeout=timeout) if reply else mid
79
+ def shell_request(self:ConKernelClient, msg_type, timeout=default_timeout, msg_id=None, subshell_id=None, metadata=None, buffers=None, **content):
80
+ "A named shell request, content as keyword arguments; returns an awaitable of its reply"
81
+ return self.request(msg_type, content, timeout=timeout, msg_id=msg_id, subshell_id=subshell_id, metadata=metadata, buffers=buffers)
140
82
 
141
83
  @patch
142
- async def control_request(self:ConKernelClient, msg_type, timeout=default_timeout, **content):
143
- "Send a request on the control channel and await its matching reply"
144
- msg = self.session.msg(msg_type, content)
145
- self.control_channel.send(msg)
146
- for rem in iter_timeout(timeout):
147
- try: reply = await self.get_control_msg(timeout=rem)
148
- except Empty: continue
149
- if parent_id(reply) == msg['header']['msg_id']: return reply
150
- raise TimeoutError(f"no reply to {msg_type}")
84
+ def control_request(self:ConKernelClient, msg_type, timeout=default_timeout, **content):
85
+ "A named control request, content as keyword arguments; returns an awaitable of its reply"
86
+ return self.request(msg_type, content, channel='control', timeout=timeout)
151
87
 
152
88
  # %% ../nbs/01_ops.ipynb #4f01d2b6
153
89
  @patch
154
90
  async def input_reply(self:ConKernelClient, value:str):
155
91
  "Answer the kernel's pending input_request (from `input()` in an execute sent with `allow_stdin=True`)"
156
- await self.stdin_send(self.session.msg('input_reply', dict(value=value)))
92
+ self.input(value)
93
+
157
94
 
158
95
  # %% ../nbs/01_ops.ipynb #97d75b1e
159
96
  class _ReqProxy:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conkernelclient
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: Concurrent-safe Jupyter KernelClient
5
5
  Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
6
6
  License: Apache-2.0
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: jupyter_client
16
16
  Requires-Dist: fastcore>=2.1.17
17
- Requires-Dist: jupywire>=0.1.4
17
+ Requires-Dist: jupywire>=0.1.5
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: ipykernel; extra == "dev"
20
20
  Requires-Dist: ipymini>=0.1.17; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  jupyter_client
2
2
  fastcore>=2.1.17
3
- jupywire>=0.1.4
3
+ jupywire>=0.1.5
4
4
 
5
5
  [dev]
6
6
  ipykernel
@@ -18,7 +18,7 @@ classifiers = [
18
18
  dependencies = [
19
19
  'jupyter_client',
20
20
  'fastcore>=2.1.17',
21
- 'jupywire>=0.1.4',
21
+ 'jupywire>=0.1.5',
22
22
  ]
23
23
 
24
24
  [project.optional-dependencies]
@@ -41,7 +41,7 @@ def test_burst_replies_under_send_jitter():
41
41
  "20 pending replies at once, every send jittered: the reader must route each reply to its waiter."
42
42
  async def _run():
43
43
  async with run_kernel("ipymini", ["ipymini", "-f", "{connection_file}"], manager_cls=LockedKernelManager) as (km, kc):
44
- reps = [kc.execute(f"x{i} = {i}; x{i}", reply=True, timeout=30) for i in range(20)]
44
+ reps = [kc.reply(f"x{i} = {i}; x{i}", timeout=30) for i in range(20)]
45
45
  for i, r in enumerate(await asyncio.gather(*reps)): assert r["content"]["status"] == "ok", (i, r["content"])
46
46
  asyncio.run(_run())
47
47
 
@@ -1,3 +0,0 @@
1
- __version__ = "0.0.19"
2
- from .core import *
3
- from .ops import *