txl-remote-kernels 0.3.7__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.
- txl_remote_kernels/__init__.py +0 -0
- txl_remote_kernels/driver.py +124 -0
- txl_remote_kernels/main.py +88 -0
- txl_remote_kernels/message.py +67 -0
- txl_remote_kernels-0.3.7.dist-info/METADATA +25 -0
- txl_remote_kernels-0.3.7.dist-info/RECORD +9 -0
- txl_remote_kernels-0.3.7.dist-info/WHEEL +4 -0
- txl_remote_kernels-0.3.7.dist-info/entry_points.txt +7 -0
- txl_remote_kernels-0.3.7.dist-info/licenses/LICENSE.txt +9 -0
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
from urllib import parse
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from anyio import Lock, sleep
|
|
9
|
+
from anyioutils import create_task
|
|
10
|
+
from httpx_ws import aconnect_ws
|
|
11
|
+
from txl_kernel.driver import KernelMixin
|
|
12
|
+
from txl_kernel.message import date_to_str
|
|
13
|
+
|
|
14
|
+
from .message import (
|
|
15
|
+
deserialize_msg_from_ws_v1,
|
|
16
|
+
from_binary,
|
|
17
|
+
serialize_msg_to_ws_v1,
|
|
18
|
+
to_binary,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def deadline_to_timeout(deadline: float) -> float:
|
|
23
|
+
return max(0, deadline - time.time())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KernelDriver(KernelMixin):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
task_group,
|
|
30
|
+
url: str,
|
|
31
|
+
kernel_name: str | None = "",
|
|
32
|
+
comm_handlers=[],
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(task_group)
|
|
35
|
+
self.task_group = task_group
|
|
36
|
+
self.kernel_name = kernel_name
|
|
37
|
+
parsed_url = parse.urlparse(url)
|
|
38
|
+
self.base_url = parse.urljoin(url, parsed_url.path).rstrip("/")
|
|
39
|
+
self.query_params = parse.parse_qs(parsed_url.query)
|
|
40
|
+
self.cookies = httpx.Cookies()
|
|
41
|
+
i = self.base_url.find(":")
|
|
42
|
+
self.ws_url = ("wss" if self.base_url[i - 1] == "s" else "ws") + self.base_url[i:]
|
|
43
|
+
self.start_task = create_task(self.start(), task_group)
|
|
44
|
+
self.comm_handlers = comm_handlers
|
|
45
|
+
self.shell_channel = "shell"
|
|
46
|
+
self.control_channel = "control"
|
|
47
|
+
self.iopub_channel = "iopub"
|
|
48
|
+
self.send_lock = Lock()
|
|
49
|
+
self.kernel_id = None
|
|
50
|
+
|
|
51
|
+
async def start(self):
|
|
52
|
+
i = str(uuid.uuid4())
|
|
53
|
+
async with httpx.AsyncClient() as client:
|
|
54
|
+
body = {
|
|
55
|
+
"kernel": {"name": self.kernel_name},
|
|
56
|
+
"name": i,
|
|
57
|
+
"path": i,
|
|
58
|
+
"type": "notebook",
|
|
59
|
+
}
|
|
60
|
+
r = await client.post(
|
|
61
|
+
f"{self.base_url}/api/sessions",
|
|
62
|
+
json=body,
|
|
63
|
+
params={**self.query_params},
|
|
64
|
+
cookies=self.cookies,
|
|
65
|
+
)
|
|
66
|
+
d = r.json()
|
|
67
|
+
self.cookies.update(r.cookies)
|
|
68
|
+
self.session_id = d["id"]
|
|
69
|
+
self.kernel_id = d["kernel"]["id"]
|
|
70
|
+
r = await client.get(
|
|
71
|
+
f"{self.base_url}/api/kernels/{self.kernel_id}",
|
|
72
|
+
cookies=self.cookies,
|
|
73
|
+
)
|
|
74
|
+
if r.status_code != 200 or self.kernel_id != r.json()["id"]:
|
|
75
|
+
return
|
|
76
|
+
async with aconnect_ws(
|
|
77
|
+
f"{self.ws_url}/api/kernels/{self.kernel_id}/channels",
|
|
78
|
+
params={"session_id": self.session_id},
|
|
79
|
+
cookies=self.cookies,
|
|
80
|
+
subprotocols=["v1.kernel.websocket.jupyter.org"],
|
|
81
|
+
) as self.websocket:
|
|
82
|
+
recv_task = create_task(self._recv(), self.task_group)
|
|
83
|
+
try:
|
|
84
|
+
await self.wait_for_ready()
|
|
85
|
+
self.started.set()
|
|
86
|
+
await sleep(float("inf"))
|
|
87
|
+
except BaseException:
|
|
88
|
+
recv_task.cancel()
|
|
89
|
+
self.start_task.cancel()
|
|
90
|
+
|
|
91
|
+
async def _recv(self):
|
|
92
|
+
while True:
|
|
93
|
+
message = await self.websocket.receive()
|
|
94
|
+
if self.websocket.subprotocol == "v1.kernel.websocket.jupyter.org":
|
|
95
|
+
msg = deserialize_msg_from_ws_v1(message.data)
|
|
96
|
+
else:
|
|
97
|
+
if isinstance(message.data, str):
|
|
98
|
+
msg = json.loads(message.data)
|
|
99
|
+
else:
|
|
100
|
+
msg = from_binary(message.data)
|
|
101
|
+
self.recv_queue.put_nowait(msg)
|
|
102
|
+
|
|
103
|
+
async def send_message(
|
|
104
|
+
self,
|
|
105
|
+
msg: Dict[str, Any],
|
|
106
|
+
channel,
|
|
107
|
+
change_date_to_str: bool = False,
|
|
108
|
+
):
|
|
109
|
+
async with self.send_lock:
|
|
110
|
+
_date_to_str = date_to_str if change_date_to_str else lambda x: x
|
|
111
|
+
msg["header"] = _date_to_str(msg["header"])
|
|
112
|
+
msg["parent_header"] = _date_to_str(msg["parent_header"])
|
|
113
|
+
msg["metadata"] = _date_to_str(msg["metadata"])
|
|
114
|
+
msg["content"] = _date_to_str(msg.get("content", {}))
|
|
115
|
+
msg["channel"] = channel
|
|
116
|
+
if self.websocket.subprotocol == "v1.kernel.websocket.jupyter.org":
|
|
117
|
+
bmsg = serialize_msg_to_ws_v1(msg)
|
|
118
|
+
await self.websocket.send_bytes(bmsg)
|
|
119
|
+
else:
|
|
120
|
+
bmsg = to_binary(msg)
|
|
121
|
+
if bmsg is None:
|
|
122
|
+
await self.websocket.send_json(msg)
|
|
123
|
+
else:
|
|
124
|
+
await self.websocket.send_bytes(bmsg)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib import parse
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from anyio import create_task_group, sleep
|
|
8
|
+
from fps import Module
|
|
9
|
+
from pycrdt import Map
|
|
10
|
+
|
|
11
|
+
from txl.base import Kernels, Kernelspecs
|
|
12
|
+
|
|
13
|
+
from .driver import KernelDriver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RemoteKernels(Kernels):
|
|
17
|
+
comm_handlers = []
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
url: str,
|
|
22
|
+
kernel_name: str | None,
|
|
23
|
+
):
|
|
24
|
+
self.kernel = KernelDriver(
|
|
25
|
+
self.task_group, url, kernel_name, comm_handlers=self.comm_handlers
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def execute(self, ycell: Map):
|
|
29
|
+
await self.kernel.execute(ycell)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RemoteKernelspecs(Kernelspecs):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
url: str,
|
|
36
|
+
):
|
|
37
|
+
parsed_url = parse.urlparse(url)
|
|
38
|
+
self.base_url = parse.urljoin(url, parsed_url.path).rstrip("/")
|
|
39
|
+
self.query_params = parse.parse_qs(parsed_url.query)
|
|
40
|
+
self.cookies = httpx.Cookies()
|
|
41
|
+
|
|
42
|
+
async def get(self) -> dict[str, Any]:
|
|
43
|
+
url = f"{self.base_url}/api/kernelspecs"
|
|
44
|
+
try:
|
|
45
|
+
async with httpx.AsyncClient() as client:
|
|
46
|
+
r = await client.get(
|
|
47
|
+
url,
|
|
48
|
+
params={**self.query_params},
|
|
49
|
+
cookies=self.cookies,
|
|
50
|
+
)
|
|
51
|
+
d = r.json()
|
|
52
|
+
self.cookies.update(r.cookies)
|
|
53
|
+
return d
|
|
54
|
+
except httpx.ConnectError:
|
|
55
|
+
raise RuntimeError(f"Could not connect to a Jupyter server at {url}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RemoteKernelsModule(Module):
|
|
59
|
+
def __init__(self, name: str, url: str = "http://127.0.0.1:8000"):
|
|
60
|
+
super().__init__(name)
|
|
61
|
+
self.url = url
|
|
62
|
+
|
|
63
|
+
async def start(self) -> None:
|
|
64
|
+
url = self.url
|
|
65
|
+
|
|
66
|
+
async with create_task_group() as self.tg:
|
|
67
|
+
class _RemoteKernels(RemoteKernels):
|
|
68
|
+
task_group = self.tg
|
|
69
|
+
|
|
70
|
+
def __init__(self, *args, **kwargs):
|
|
71
|
+
super().__init__(url, *args, **kwargs)
|
|
72
|
+
|
|
73
|
+
self.put(_RemoteKernels, Kernels)
|
|
74
|
+
self.done()
|
|
75
|
+
await sleep(float("inf"))
|
|
76
|
+
|
|
77
|
+
async def stop(self) -> None:
|
|
78
|
+
self.tg.cancel_scope.cancel()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RemoteKernelspecsModule(Module):
|
|
82
|
+
def __init__(self, name: str, url: str = "http://127.0.0.1:8000"):
|
|
83
|
+
super().__init__(name)
|
|
84
|
+
self.url = url
|
|
85
|
+
|
|
86
|
+
async def start(self) -> None:
|
|
87
|
+
kernelspecs = RemoteKernelspecs(self.url)
|
|
88
|
+
self.put(kernelspecs, Kernelspecs)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import struct
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def deserialize_msg_from_ws_v1(ws_msg: bytes) -> Dict[str, Any]:
|
|
7
|
+
offset_number = int.from_bytes(ws_msg[:8], "little")
|
|
8
|
+
offsets = [
|
|
9
|
+
int.from_bytes(ws_msg[8 * (i + 1) : 8 * (i + 2)], "little") for i in range(offset_number)
|
|
10
|
+
]
|
|
11
|
+
channel = ws_msg[offsets[0] : offsets[1]].decode("utf-8")
|
|
12
|
+
msg_list = [ws_msg[offsets[i] : offsets[i + 1]] for i in range(1, offset_number - 1)]
|
|
13
|
+
msg = {
|
|
14
|
+
"channel": channel,
|
|
15
|
+
"header": json.loads(msg_list[0]),
|
|
16
|
+
"parent_header": json.loads(msg_list[1]),
|
|
17
|
+
"metadata": json.loads(msg_list[2]),
|
|
18
|
+
"content": json.loads(msg_list[3]),
|
|
19
|
+
}
|
|
20
|
+
msg["buffers"] = msg_list[4:]
|
|
21
|
+
return msg
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def serialize_msg_to_ws_v1(msg: Dict[str, Any]) -> List[bytes]:
|
|
25
|
+
msg_list = [
|
|
26
|
+
json.dumps(msg["header"]).encode("utf-8"),
|
|
27
|
+
json.dumps(msg["parent_header"]).encode("utf-8"),
|
|
28
|
+
json.dumps(msg["metadata"]).encode("utf-8"),
|
|
29
|
+
json.dumps(msg["content"]).encode("utf-8"),
|
|
30
|
+
] + msg.get("buffers", [])
|
|
31
|
+
channel_b = msg["channel"].encode("utf-8")
|
|
32
|
+
offsets = []
|
|
33
|
+
offsets.append(8 * (1 + 1 + len(msg_list) + 1))
|
|
34
|
+
offsets.append(len(channel_b) + offsets[-1])
|
|
35
|
+
for msg in msg_list:
|
|
36
|
+
offsets.append(len(msg) + offsets[-1])
|
|
37
|
+
offset_number = len(offsets).to_bytes(8, byteorder="little")
|
|
38
|
+
offsets_b = [offset.to_bytes(8, byteorder="little") for offset in offsets]
|
|
39
|
+
bin_msg = [offset_number] + offsets_b + [channel_b] + msg_list
|
|
40
|
+
return b"".join(bin_msg)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_binary(msg: Dict[str, Any]) -> Optional[bytes]:
|
|
44
|
+
if not msg["buffers"]:
|
|
45
|
+
return None
|
|
46
|
+
buffers = msg.pop("buffers")
|
|
47
|
+
bmsg = json.dumps(msg).encode("utf8")
|
|
48
|
+
buffers.insert(0, bmsg)
|
|
49
|
+
n = len(buffers)
|
|
50
|
+
offsets = [4 * (n + 1)]
|
|
51
|
+
for b in buffers[:-1]:
|
|
52
|
+
offsets.append(offsets[-1] + len(b))
|
|
53
|
+
header = struct.pack("!" + "I" * (n + 1), n, *offsets)
|
|
54
|
+
buffers.insert(0, header)
|
|
55
|
+
return b"".join(buffers)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def from_binary(bmsg: bytes) -> Dict[str, Any]:
|
|
59
|
+
n = struct.unpack("!i", bmsg[:4])[0]
|
|
60
|
+
offsets = list(struct.unpack("!" + "I" * n, bmsg[4 : 4 * (n + 1)])) # noqa
|
|
61
|
+
offsets.append(None)
|
|
62
|
+
buffers = []
|
|
63
|
+
for start, stop in zip(offsets[:-1], offsets[1:]):
|
|
64
|
+
buffers.append(bmsg[start:stop])
|
|
65
|
+
msg = json.loads(buffers[0].decode("utf8"))
|
|
66
|
+
msg["buffers"] = buffers[1:]
|
|
67
|
+
return msg
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: txl_remote_kernels
|
|
3
|
+
Version: 0.3.7
|
|
4
|
+
Summary: TXL plugin for remote kernels
|
|
5
|
+
Project-URL: Source, https://github.com/davidbrochart/jpterm/tree/main/plugins/remote_kernels
|
|
6
|
+
Author-email: David Brochart <david.brochart@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx-ws>=0.4.2
|
|
19
|
+
Requires-Dist: httpx>=0.23.1
|
|
20
|
+
Requires-Dist: pycrdt<0.13.0,>=0.12.44
|
|
21
|
+
Requires-Dist: txl-kernel
|
|
22
|
+
Requires-Dist: txl==0.3.3
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# TXL plugin for remote kernels
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
txl_remote_kernels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
txl_remote_kernels/driver.py,sha256=gJuGmbshysBo-040NThjm-MamqIME47ejUoeQuoGMmY,4332
|
|
3
|
+
txl_remote_kernels/main.py,sha256=L7oQW6d0WKu35-luLrZwAWoml4X7CH0OjFf6ASrA9nE,2429
|
|
4
|
+
txl_remote_kernels/message.py,sha256=lyO6uPpRlYgdRSOZxam6iA-QH_HjXga3XKNlA_cCs5Q,2400
|
|
5
|
+
txl_remote_kernels-0.3.7.dist-info/METADATA,sha256=NX5tMllWLtay7_AxzMtOkXx_42JxG3h3aW5OEs1uAII,980
|
|
6
|
+
txl_remote_kernels-0.3.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
txl_remote_kernels-0.3.7.dist-info/entry_points.txt,sha256=WWe5nk2g-g6A4T9-ceUej1JR4k4rh4KF4FIbriEFQVM,289
|
|
8
|
+
txl_remote_kernels-0.3.7.dist-info/licenses/LICENSE.txt,sha256=su0IgzSHZ9tMFeCQ_IaCji5Q4VrZSOqDsEFi7lPduc8,1106
|
|
9
|
+
txl_remote_kernels-0.3.7.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[fps.modules]
|
|
2
|
+
remote_kernels = txl_remote_kernels.main:RemoteKernelsModule
|
|
3
|
+
remote_kernelspecs = txl_remote_kernels.main:RemoteKernelspecsModule
|
|
4
|
+
|
|
5
|
+
[txl.modules]
|
|
6
|
+
remote_kernels = txl_remote_kernels.main:RemoteKernelsModule
|
|
7
|
+
remote_kernelspecs = txl_remote_kernels.main:RemoteKernelspecsModule
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-present David Brochart <david.brochart@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|