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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conkernelclient
3
- Version: 0.0.11
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
+
@@ -105,3 +105,4 @@ if await km.is_alive():
105
105
  kc.stop_channels()
106
106
  await km.shutdown_kernel()
107
107
  ```
108
+
@@ -0,0 +1,3 @@
1
+ __version__ = "0.0.13"
2
+ from .core import *
3
+ from .ops import *
@@ -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/conkernelclientcore.html.md"""
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 queue import Empty
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
- buffers=None, track=False, header=None, metadata=None):
25
+ buffers=None, track=False, header=None, metadata=None):
33
26
  msg = self._orig_send(stream, msg_or_type, content=content, parent=parent,
34
- ident=ident, buffers=buffers, track=track, header=header, metadata=metadata)
35
- # Force a sync, ensuring the send is fully registered internally
36
- # Avoids a race where the lock releases, another thread immediately calls send(),
37
- # and now 2 threads are interacting with the internal state before I/O thread has caught up
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
- elif hasattr(stream, 'getsockopt'): stream.getsockopt(zmq.EVENTS)
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
- Session.send = _send
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
- cts_typ='code', timeout=60, msg_id=None, stop_on_error=True, **kw):
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.11
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
+
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  conkernelclient/__init__.py
6
6
  conkernelclient/_modidx.py
7
7
  conkernelclient/core.py
8
+ conkernelclient/ops.py
8
9
  conkernelclient.egg-info/PKG-INFO
9
10
  conkernelclient.egg-info/SOURCES.txt
10
11
  conkernelclient.egg-info/dependency_links.txt
@@ -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,2 +0,0 @@
1
- __version__ = "0.0.11"
2
- from .core import *
@@ -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')}}}
@@ -1,5 +1,5 @@
1
1
  jupyter_client
2
+ fastcore
2
3
 
3
4
  [dev]
4
- fastcore
5
5
  ipykernel