pdit 0.6.0__py3-none-any.whl → 0.7.0__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.
pdit/_static/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>pdit</title>
8
- <script type="module" crossorigin src="/assets/index-DbESVfT-.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-WLaVrqLo.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-rS96z8hq.css">
10
10
  </head>
11
11
  <body>
pdit/exporter.py CHANGED
@@ -1,13 +1,15 @@
1
1
  """Export functionality for pdit scripts."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  from pathlib import Path
5
- from typing import Any
6
+ from threading import Thread
7
+ from typing import Any, Callable, Coroutine
6
8
 
7
9
  from .ipython_executor import IPythonExecutor
8
10
 
9
11
 
10
- def execute_script(script_content: str, script_name: str) -> list[dict[str, Any]]:
12
+ async def _execute_script_async(script_content: str, script_name: str) -> list[dict[str, Any]]:
11
13
  """Execute a script and return expressions in frontend format.
12
14
 
13
15
  Args:
@@ -22,7 +24,7 @@ def execute_script(script_content: str, script_name: str) -> list[dict[str, Any]
22
24
  expression_id = 0
23
25
 
24
26
  try:
25
- for event in executor.execute_script(script_content, script_name=script_name):
27
+ async for event in executor.execute_script(script_content, script_name=script_name):
26
28
  # Skip the expressions list event
27
29
  if event.get("type") == "expressions":
28
30
  continue
@@ -40,11 +42,44 @@ def execute_script(script_content: str, script_name: str) -> list[dict[str, Any]
40
42
  })
41
43
  expression_id += 1
42
44
  finally:
43
- executor.shutdown()
45
+ await executor.shutdown()
44
46
 
45
47
  return expressions
46
48
 
47
49
 
50
+ def _run_async(
51
+ async_fn: Callable[..., Coroutine[Any, Any, Any]],
52
+ *args: Any,
53
+ **kwargs: Any,
54
+ ) -> Any:
55
+ try:
56
+ asyncio.get_running_loop()
57
+ except RuntimeError:
58
+ return asyncio.run(async_fn(*args, **kwargs))
59
+
60
+ result: dict[str, Any] = {}
61
+ error: dict[str, BaseException] = {}
62
+
63
+ def runner() -> None:
64
+ try:
65
+ result["value"] = asyncio.run(async_fn(*args, **kwargs))
66
+ except BaseException as exc:
67
+ error["value"] = exc
68
+
69
+ thread = Thread(target=runner, daemon=True)
70
+ thread.start()
71
+ thread.join()
72
+
73
+ if "value" in error:
74
+ raise error["value"]
75
+ return result["value"]
76
+
77
+
78
+ def execute_script(script_content: str, script_name: str) -> list[dict[str, Any]]:
79
+ """Execute a script and return expressions in frontend format."""
80
+ return _run_async(_execute_script_async, script_content, script_name)
81
+
82
+
48
83
  def generate_html(script_content: str, expressions: list[dict[str, Any]]) -> str:
49
84
  """Generate self-contained HTML from script and execution results.
50
85
 
pdit/ipython_executor.py CHANGED
@@ -12,7 +12,7 @@ import json
12
12
  import logging
13
13
  import re
14
14
  import traceback
15
- from typing import Any, AsyncGenerator, Optional
15
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
16
16
 
17
17
  from jupyter_client import AsyncKernelManager
18
18
 
@@ -234,8 +234,17 @@ del _register_pdit_runtime_hooks
234
234
 
235
235
  return output
236
236
 
237
- async def _execute_code(self, code: str) -> list[dict]:
238
- """Execute code in kernel and collect output."""
237
+ async def _execute_code(
238
+ self,
239
+ code: str,
240
+ on_stream: Callable[[list[dict]], Awaitable[None]] | None = None,
241
+ ) -> list[dict]:
242
+ """Execute code in kernel and collect output.
243
+
244
+ Args:
245
+ code: Python source to execute in the kernel.
246
+ on_stream: Optional callback invoked when stdout/stderr updates arrive.
247
+ """
239
248
  if self.kc is None:
240
249
  return [{"type": "error", "content": "Kernel not started"}]
241
250
 
@@ -265,6 +274,8 @@ del _register_pdit_runtime_hooks
265
274
  output[-1] = {"type": stream_name, "content": output[-1]["content"] + text}
266
275
  else:
267
276
  output.append({"type": stream_name, "content": text})
277
+ if on_stream:
278
+ await on_stream(output)
268
279
  elif msg_type == 'execute_result':
269
280
  # Expression result
270
281
  data = content['data']
@@ -291,7 +302,8 @@ del _register_pdit_runtime_hooks
291
302
  self,
292
303
  script: str,
293
304
  line_range: tuple[int, int] | None = None,
294
- script_name: str | None = None
305
+ script_name: str | None = None,
306
+ on_stream: Callable[[int, int, list[dict]], Awaitable[None]] | None = None,
295
307
  ) -> AsyncGenerator[dict, None]:
296
308
  """Execute Python script, yielding event dicts as each statement completes.
297
309
 
@@ -299,6 +311,7 @@ del _register_pdit_runtime_hooks
299
311
  First: {"type": "expressions", "expressions": [{"lineStart": N, "lineEnd": N}, ...]}
300
312
  Then for each statement: {"lineStart": N, "lineEnd": N, "output": [...], "isInvisible": bool}
301
313
  On error during execution, the final result dict will have output with type="error"
314
+ If on_stream is provided, it is invoked on stdout/stderr updates with the current output list.
302
315
  """
303
316
  # Wait for kernel to be ready
304
317
  await self.wait_ready()
@@ -344,19 +357,27 @@ del _register_pdit_runtime_hooks
344
357
 
345
358
  # Execute each statement
346
359
  for stmt in statements:
360
+ stream_cb = None
361
+ if on_stream is not None:
362
+ line_start = stmt["lineStart"]
363
+ line_end = stmt["lineEnd"]
364
+
365
+ async def stream_cb(updated_output: list[dict]) -> None:
366
+ await on_stream(line_start, line_end, updated_output)
367
+
347
368
  if stmt["isMarkdownCell"]:
348
369
  # For markdown cells, just return the string content
349
370
  try:
350
371
  value = ast.literal_eval(stmt["source"])
351
372
  output = [{"type": "text/markdown", "content": str(value).strip()}]
352
373
  except (ValueError, SyntaxError):
353
- output = await self._execute_code(stmt["source"])
374
+ output = await self._execute_code(stmt["source"], on_stream=stream_cb)
354
375
  elif stmt["isFStringMarkdown"]:
355
376
  # Wrap f-string in Markdown() so it returns text/markdown directly
356
377
  wrapper_code = f"__import__('IPython').display.Markdown({stmt['source']})"
357
- output = await self._execute_code(wrapper_code)
378
+ output = await self._execute_code(wrapper_code, on_stream=stream_cb)
358
379
  else:
359
- output = await self._execute_code(stmt["source"])
380
+ output = await self._execute_code(stmt["source"], on_stream=stream_cb)
360
381
 
361
382
  yield {
362
383
  "lineStart": stmt["lineStart"],
pdit/server.py CHANGED
@@ -174,7 +174,7 @@ async def websocket_session(websocket: WebSocket, sessionId: str, token: Optiona
174
174
 
175
175
  Message Protocol (Server -> Client):
176
176
  File events: {"type": "initial/fileChanged/fileDeleted", "path": "...", "content": "...", "timestamp": N}
177
- Execution: {"type": "expressions/result/cancelled/complete/busy", ...}
177
+ Execution: {"type": "expressions/result/stream/cancelled/complete/busy", ...}
178
178
  Errors: {"type": "error", "message": "..."}
179
179
  """
180
180
  # Validate token if configured
@@ -290,7 +290,26 @@ async def _handle_ws_execute(websocket: WebSocket, session: Session, data: dict)
290
290
  expressions: list[dict] = []
291
291
  executed_count = 0
292
292
 
293
- async for event in session.executor.execute_script(script, line_range, script_name):
293
+ async def _send_stream_update(line_start: int, line_end: int, output: list[dict]) -> None:
294
+ if shutdown_event.is_set():
295
+ return
296
+ try:
297
+ await websocket.send_json({
298
+ "type": "stream",
299
+ "lineStart": line_start,
300
+ "lineEnd": line_end,
301
+ "output": output,
302
+ })
303
+ except Exception:
304
+ # Connection may have closed; ignore streaming failures
305
+ pass
306
+
307
+ async for event in session.executor.execute_script(
308
+ script,
309
+ line_range,
310
+ script_name,
311
+ on_stream=_send_stream_update,
312
+ ):
294
313
  # Add type field to result messages (executor yields without type)
295
314
  if "output" in event and "type" not in event:
296
315
  event = {"type": "result", **event}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdit
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Interactive Python code editor with inline execution results
5
5
  Author: Harry Vangberg
6
6
  License: MIT
@@ -0,0 +1,17 @@
1
+ pdit/__init__.py,sha256=QpiMnN_x5G3QaeWMXQlCRUtCo35IEYZLPj-cz87WG_g,185
2
+ pdit/_demo.py,sha256=SMOGoiHBdMDq8pXRVK-uuTRFCFbRd47diUyRR8Y3wZY,2026
3
+ pdit/cli.py,sha256=3YkWz2Sg3-OszRssWkXbPEu3dDQAXMS8Z0c07bZ7gx8,9634
4
+ pdit/exporter.py,sha256=xDFkAzigh7dDDCSqPP6nI4WKlZ_xI1XIKC2QkcdpRVg,3792
5
+ pdit/file_watcher.py,sha256=1WlqKWjLCdgMZLdN836MEeWTisGCtZmvgRdOFN-jns8,5200
6
+ pdit/ipython_executor.py,sha256=KMBFg4Xl5M2iiV8kkpSFJUURv9XjKYl5Nf1y5HzivxQ,17244
7
+ pdit/server.py,sha256=V5uFMqPle7JGeMHWRjniiN0m1z7rK-pOBZ1tiPNPU5E,14566
8
+ pdit/_static/export.html,sha256=ITKX3MKoWQYoR8EqkMTfsEYjpNWfhVVi-Mdmw_jpxoY,332339
9
+ pdit/_static/index.html,sha256=iDl5_lp8ailmLfeKWcGvEDY_wI6_hS5uMnDGRGEu1-o,450
10
+ pdit/_static/assets/index-WLaVrqLo.js,sha256=6g0aNVXaBOwAISafLYvgP-0bpEVL-YuQs026ebsFE4w,815277
11
+ pdit/_static/assets/index-rS96z8hq.css,sha256=0EMq2r0to1VxGznyp18tWCd97_sSZhGVC3YF7QdBXEU,22855
12
+ pdit-0.7.0.dist-info/licenses/LICENSE,sha256=Y9tvBURRu-1aHUHY-tnbsDpPBTRYKWpIInVmaoOUkME,1071
13
+ pdit-0.7.0.dist-info/METADATA,sha256=3tr06V3Xyy1c9elzPK1x0ULeowwrUrhJChqef0lsST0,1859
14
+ pdit-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ pdit-0.7.0.dist-info/entry_points.txt,sha256=n9I4wGD7M0NQrPWJcTWOsJAzeS6bQJbTnUP8C3Sl-iY,39
16
+ pdit-0.7.0.dist-info/top_level.txt,sha256=wk6vel1ecJS4EZZ3U6Xue9OwDq-Tw8Pbvq_TRQz9eIQ,5
17
+ pdit-0.7.0.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pdit/__init__.py,sha256=QpiMnN_x5G3QaeWMXQlCRUtCo35IEYZLPj-cz87WG_g,185
2
- pdit/_demo.py,sha256=SMOGoiHBdMDq8pXRVK-uuTRFCFbRd47diUyRR8Y3wZY,2026
3
- pdit/cli.py,sha256=3YkWz2Sg3-OszRssWkXbPEu3dDQAXMS8Z0c07bZ7gx8,9634
4
- pdit/exporter.py,sha256=i2emLqRQNPW92vFBTamLZyOhUTDU45_0QnOWG7PcIu8,2811
5
- pdit/file_watcher.py,sha256=1WlqKWjLCdgMZLdN836MEeWTisGCtZmvgRdOFN-jns8,5200
6
- pdit/ipython_executor.py,sha256=5YEBpJL7IjLfNve8hXa-2_cYawfP6ro5myNAx0n97ug,16325
7
- pdit/server.py,sha256=URabdsLxiLUipkl8PTc5jgDjMlQV0v8QJTMnNNy0XKs,13941
8
- pdit/_static/export.html,sha256=ITKX3MKoWQYoR8EqkMTfsEYjpNWfhVVi-Mdmw_jpxoY,332339
9
- pdit/_static/index.html,sha256=wjUe3SHOs1g4hvQHUiKcwoKSkHyYs2GDdk3OqtdDMSA,450
10
- pdit/_static/assets/index-DbESVfT-.js,sha256=vOC5-SuA-WA-ZxZ1TkbCtOwi0sfSGNu0c1fEF1-382c,814902
11
- pdit/_static/assets/index-rS96z8hq.css,sha256=0EMq2r0to1VxGznyp18tWCd97_sSZhGVC3YF7QdBXEU,22855
12
- pdit-0.6.0.dist-info/licenses/LICENSE,sha256=Y9tvBURRu-1aHUHY-tnbsDpPBTRYKWpIInVmaoOUkME,1071
13
- pdit-0.6.0.dist-info/METADATA,sha256=gqrL2j5vzJcOcpw3R3EKxt_hH1jHTc2ewKBWhK4KcBY,1859
14
- pdit-0.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
- pdit-0.6.0.dist-info/entry_points.txt,sha256=n9I4wGD7M0NQrPWJcTWOsJAzeS6bQJbTnUP8C3Sl-iY,39
16
- pdit-0.6.0.dist-info/top_level.txt,sha256=wk6vel1ecJS4EZZ3U6Xue9OwDq-Tw8Pbvq_TRQz9eIQ,5
17
- pdit-0.6.0.dist-info/RECORD,,
File without changes