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.
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.