notebook-terminal 0.1.1__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.
@@ -0,0 +1,26 @@
1
+ from .session import TerminalSession
2
+ from .styling import Color, ansi_text
3
+ from .web import TerminalManager
4
+
5
+ try:
6
+ from .notebook import ManagedTerminalWidget, TerminalWidget, terminal
7
+ except ImportError:
8
+ TerminalWidget = None
9
+ ManagedTerminalWidget = None
10
+
11
+ def terminal(*args, **kwargs):
12
+ raise RuntimeError(
13
+ "Jupyter UI requires anywidget, ipython and traitlets. "
14
+ "Install the normal package dependencies."
15
+ )
16
+
17
+ __all__ = [
18
+ "TerminalSession",
19
+ "Color",
20
+ "ansi_text",
21
+ "TerminalWidget",
22
+ "ManagedTerminalWidget",
23
+ "TerminalManager",
24
+ "terminal",
25
+ ]
26
+ __version__ = "0.1.1"
@@ -0,0 +1,403 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import base64
5
+ import contextlib
6
+ import io
7
+ import sys
8
+ import threading
9
+ import traceback
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import anywidget
14
+ import ipywidgets as widgets
15
+ import traitlets
16
+ from IPython.display import display
17
+
18
+ from .session import TerminalSession
19
+ from .styling import Color, ansi_text
20
+
21
+ _STATIC = Path(__file__).with_name("static")
22
+
23
+
24
+ class _TerminalCanvas(anywidget.AnyWidget):
25
+ _esm = _STATIC / "widget.js"
26
+ _css = _STATIC / "all.css"
27
+
28
+ height = traitlets.Int(420).tag(sync=True)
29
+ font_size = traitlets.Int(14).tag(sync=True)
30
+ scrollback = traitlets.Int(5000).tag(sync=True)
31
+ interactive = traitlets.Bool(True).tag(sync=True)
32
+ allow_copy = traitlets.Bool(True).tag(sync=True)
33
+ allow_paste = traitlets.Bool(True).tag(sync=True)
34
+
35
+ def push(self, data: bytes | str) -> None:
36
+ if isinstance(data, str):
37
+ data = data.encode("utf-8", "replace")
38
+ if data:
39
+ self.send({"type": "output", "data": base64.b64encode(data).decode("ascii")})
40
+
41
+
42
+
43
+
44
+ class _DataFrameView(anywidget.AnyWidget):
45
+ _esm = _STATIC / "dataframe.js"
46
+ _css = _STATIC / "dataframe.css"
47
+ columns = traitlets.List(trait=traitlets.Unicode()).tag(sync=True)
48
+ rows = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
49
+
50
+
51
+ class _PlotlyView(anywidget.AnyWidget):
52
+ _esm = _STATIC / "plotly_widget.js"
53
+ spec = traitlets.Dict().tag(sync=True)
54
+
55
+
56
+ class _TerminalStream(io.TextIOBase):
57
+ def __init__(self, widget: "TerminalWidget"):
58
+ self.widget = widget
59
+ def write(self, value: str) -> int:
60
+ self.widget._queue_output(value)
61
+ return len(value)
62
+ def flush(self) -> None:
63
+ self.widget._flush_output()
64
+
65
+
66
+ class TerminalWidget(widgets.VBox):
67
+ """Terminal plus native Jupyter tabs for rich output and widgets."""
68
+
69
+ def __init__(
70
+ self,
71
+ session: TerminalSession | None = None,
72
+ *,
73
+ interactive: bool = True,
74
+ auto_display: bool = True,
75
+ command: str | None = None,
76
+ height: int = 420,
77
+ font_size: int = 14,
78
+ scrollback: int = 5000,
79
+ allow_copy: bool = True,
80
+ allow_paste: bool = True,
81
+ rich_tabs: bool = True,
82
+ **session_kwargs,
83
+ ):
84
+ self.session = session or TerminalSession(**session_kwargs)
85
+ self.rich_tabs = bool(rich_tabs)
86
+ self._canvas = _TerminalCanvas(
87
+ height=height, font_size=font_size, scrollback=scrollback,
88
+ interactive=interactive, allow_copy=allow_copy, allow_paste=allow_paste,
89
+ )
90
+ self._tab_items: list[tuple[str, widgets.Widget]] = []
91
+ self._tab_buttons = widgets.HBox(layout=widgets.Layout(display="none", flex_flow="row wrap"))
92
+ self._tab_stack = widgets.Stack(children=())
93
+ self._tab_stack.layout.display = "none"
94
+ self._tabs_box = widgets.VBox([self._tab_buttons, self._tab_stack])
95
+ super().__init__([self._canvas, self._tabs_box])
96
+
97
+ self._output_buffer: list[str] = []
98
+ self._flush_timer: threading.Timer | None = None
99
+ self._flush_lock = threading.Lock()
100
+ self._namespace: dict[str, Any] = {"__name__": "__main__"}
101
+ self._unsubscribe = self.session.subscribe(self._on_output, replay=True)
102
+ self._canvas.on_msg(self._on_message)
103
+ self._initial_command = command
104
+ if auto_display:
105
+ display(self)
106
+
107
+ @property
108
+ def interactive(self) -> bool: return bool(self._canvas.interactive)
109
+ @property
110
+ def height(self) -> int: return int(self._canvas.height)
111
+ @property
112
+ def font_size(self) -> int: return int(self._canvas.font_size)
113
+ @property
114
+ def scrollback(self) -> int: return int(self._canvas.scrollback)
115
+ @property
116
+ def allow_copy(self) -> bool: return bool(self._canvas.allow_copy)
117
+ @property
118
+ def allow_paste(self) -> bool: return bool(self._canvas.allow_paste)
119
+
120
+ def _queue_output(self, data: bytes | str) -> None:
121
+ text = data.decode("utf-8", "replace") if isinstance(data, bytes) else data
122
+ if not text: return
123
+ with self._flush_lock:
124
+ self._output_buffer.append(text)
125
+ size = sum(map(len, self._output_buffer))
126
+ if size >= 65536:
127
+ self._flush_output_locked()
128
+ elif self._flush_timer is None:
129
+ self._flush_timer = threading.Timer(.025, self._flush_output)
130
+ self._flush_timer.daemon = True
131
+ self._flush_timer.start()
132
+
133
+ def _flush_output_locked(self) -> None:
134
+ text = "".join(self._output_buffer)
135
+ self._output_buffer.clear()
136
+ self._flush_timer = None
137
+ if text: self._canvas.push(text)
138
+
139
+ def _flush_output(self) -> None:
140
+ with self._flush_lock: self._flush_output_locked()
141
+
142
+ def _on_output(self, data: bytes) -> None:
143
+ self._queue_output(data)
144
+
145
+ def _on_message(self, _widget, message, _buffers) -> None:
146
+ kind = message.get("type")
147
+ if kind == "ready":
148
+ self.session.resize(int(message.get("cols", 80)), int(message.get("rows", 24)))
149
+ if self._initial_command:
150
+ self.session.run(self._initial_command); self._initial_command = None
151
+ elif kind == "input" and self.interactive:
152
+ self.session.write(base64.b64decode(message.get("data", "")))
153
+ elif kind == "resize":
154
+ self.session.resize(int(message.get("cols", 80)), int(message.get("rows", 24)))
155
+
156
+ def _refresh_tabs(self, selected: int | None = None) -> None:
157
+ buttons = []
158
+ for idx, (title, _child) in enumerate(self._tab_items):
159
+ select = widgets.Button(description=title, layout=widgets.Layout(width="auto"))
160
+ close = widgets.Button(description="×", tooltip=f"Zamknij {title}", layout=widgets.Layout(width="32px"))
161
+ select.on_click(lambda _b, i=idx: setattr(self._tab_stack, "selected_index", i))
162
+ close.on_click(lambda _b, i=idx: self.close_tab(i))
163
+ buttons.append(widgets.HBox([select, close], layout=widgets.Layout(width="auto")))
164
+ self._tab_buttons.children = tuple(buttons)
165
+ self._tab_stack.children = tuple(child for _title, child in self._tab_items)
166
+ visible = bool(self._tab_items)
167
+ self._tab_buttons.layout.display = "flex" if visible else "none"
168
+ self._tab_stack.layout.display = "" if visible else "none"
169
+ if visible:
170
+ self._tab_stack.selected_index = min(selected if selected is not None else len(self._tab_items)-1, len(self._tab_items)-1)
171
+
172
+ def close_tab(self, index: int) -> None:
173
+ if 0 <= index < len(self._tab_items):
174
+ _title, child = self._tab_items.pop(index)
175
+ try: child.close()
176
+ except Exception: pass
177
+ self._refresh_tabs(max(0, index-1))
178
+
179
+ def _add_tab(self, value: Any, title: str | None = None) -> None:
180
+ if not self.rich_tabs:
181
+ return
182
+ if isinstance(value, dict) and value.get("kind") == "dataframe":
183
+ child = _DataFrameView(columns=[str(x) for x in value.get("columns", [])], rows=value.get("rows", []))
184
+ title = title or value.get("title") or "DataFrame"
185
+ elif isinstance(value, dict) and value.get("kind") == "plotly":
186
+ child = _PlotlyView(spec=value.get("spec", {}), layout=widgets.Layout(width="100%", min_height="340px"))
187
+ title = title or value.get("title") or "Plotly"
188
+ elif isinstance(value, dict) and value.get("kind") == "image":
189
+ raw = base64.b64decode(value.get("data", "")); mime = str(value.get("mime", "image/png")); fmt=mime.split("/",1)[-1].replace("jpeg","jpg")
190
+ child = widgets.Image(value=raw, format=fmt, layout=widgets.Layout(max_width="100%", width="auto")); title=title or value.get("title") or "Plot"
191
+ elif isinstance(value, dict) and value.get("kind") == "html":
192
+ child = widgets.HTML(value=str(value.get("html", "")), layout=widgets.Layout(width="100%", overflow="auto")); title=title or value.get("title") or "Output"
193
+ elif isinstance(value, widgets.Widget):
194
+ child=value; title=title or type(value).__name__
195
+ else:
196
+ try:
197
+ import pandas as pd
198
+ if isinstance(value, pd.DataFrame):
199
+ child=_DataFrameView(columns=[str(c) for c in value.reset_index().columns], rows=value.reset_index().where(value.reset_index().notna(), None).to_dict(orient="records")); title=title or "DataFrame"
200
+ elif isinstance(value, pd.Series):
201
+ frame=value.to_frame().reset_index(); child=_DataFrameView(columns=[str(c) for c in frame.columns], rows=frame.where(frame.notna(),None).to_dict(orient="records")); title=title or "Series"
202
+ else: raise TypeError
203
+ except Exception:
204
+ try:
205
+ import plotly.graph_objects as go
206
+ if isinstance(value, go.Figure):
207
+ import json
208
+ from plotly.utils import PlotlyJSONEncoder
209
+ spec=json.loads(json.dumps(value.to_plotly_json(), cls=PlotlyJSONEncoder))
210
+ child=_PlotlyView(spec=spec); title=title or "Plotly"
211
+ else: raise TypeError
212
+ except Exception:
213
+ out=widgets.Output(layout=widgets.Layout(width="100%"))
214
+ with out: display(value)
215
+ child=out; title=title or type(value).__name__
216
+ self._tab_items.append((str(title or f"Output {len(self._tab_items)+1}"), child))
217
+ self._refresh_tabs(len(self._tab_items)-1)
218
+
219
+ def show(self): display(self); return self
220
+
221
+ def _new_window(self, command: str | None = None):
222
+ return TerminalWidget(shell=self.session.shell, cwd=self.session.cwd, env=self.session.env,
223
+ cols=self.session.cols, rows=self.session.rows, height=self.height,
224
+ font_size=self.font_size, scrollback=self.scrollback, interactive=self.interactive,
225
+ allow_copy=self.allow_copy, allow_paste=self.allow_paste, rich_tabs=self.rich_tabs,
226
+ command=command, auto_display=True)
227
+
228
+ def run(self, command: str, *, new_window: bool = False):
229
+ if new_window: return self._new_window(command)
230
+ self.session.run(command); return None
231
+
232
+ def send_text(self, text: str, *, new_window: bool = False):
233
+ if new_window:
234
+ w=self._new_window(); w.session.send(text); return w
235
+ self.session.send(text); return None
236
+
237
+ def run_python(self, code: str, executable: str | None = None, *, rich_output: bool = False,
238
+ new_window: bool = False, clear_previous: bool = False):
239
+ if new_window:
240
+ w=self._new_window(); w.run_python(code, executable, rich_output=rich_output, clear_previous=clear_previous); return w
241
+ if clear_previous: self.clear_tabs()
242
+ self.session.run_python(code, executable, rich_output=rich_output,
243
+ artifact_callback=self._add_tab if rich_output else None)
244
+ return None
245
+
246
+ def run_python_file(self, path: str, executable: str | None = None, *, rich_output: bool = False,
247
+ args: list[str] | None = None, new_window: bool = False,
248
+ clear_previous: bool = False):
249
+ if new_window:
250
+ w=self._new_window(); w.run_python_file(path, executable, rich_output=rich_output, args=args, clear_previous=clear_previous); return w
251
+ if clear_previous: self.clear_tabs()
252
+ self.session.run_python_file(path, executable, rich_output=rich_output, args=args,
253
+ artifact_callback=self._add_tab if rich_output else None)
254
+ return None
255
+
256
+ def run_kernel(self, code: str, *, clear_previous: bool = False, background: bool = False,
257
+ namespace: dict[str, Any] | None = None):
258
+ """Execute in the current kernel. Supports live ipywidgets in tabs.
259
+
260
+ This is intentionally separate from subprocess execution: Jupyter widgets
261
+ need the current kernel's Comm channel and cannot be made interactive from
262
+ an unrelated child Python process.
263
+ """
264
+ if clear_previous: self.clear_tabs()
265
+ ns = namespace if namespace is not None else self._namespace
266
+
267
+ def execute():
268
+ old_display = None
269
+ try:
270
+ import IPython.display as ipd
271
+ old_display = ipd.display
272
+ def tab_display(*objects, **kwargs):
273
+ title = kwargs.get("display_id")
274
+ for obj in objects: self._add_tab(obj, str(title) if title else None)
275
+ ipd.display = tab_display
276
+ ns["display"] = tab_display
277
+ tree = ast.parse(code, mode="exec")
278
+ if tree.body and isinstance(tree.body[-1], ast.Expr):
279
+ last = tree.body[-1]
280
+ tree.body[-1] = ast.Expr(value=ast.Call(func=ast.Name("display", ast.Load()), args=[last.value], keywords=[]))
281
+ ast.fix_missing_locations(tree)
282
+ stream = _TerminalStream(self)
283
+ with contextlib.redirect_stdout(stream), contextlib.redirect_stderr(stream):
284
+ exec(compile(tree, "<notebook-terminal>", "exec"), ns, ns)
285
+ # Capture standard matplotlib figures not explicitly displayed.
286
+ try:
287
+ import matplotlib.pyplot as plt
288
+ for idx, num in enumerate(list(plt.get_fignums()), 1):
289
+ self._add_tab(plt.figure(num), f"Plot {idx}")
290
+ if plt.get_fignums(): plt.close("all")
291
+ except Exception: pass
292
+ except BaseException:
293
+ self._queue_output(traceback.format_exc())
294
+ finally:
295
+ if old_display is not None:
296
+ try:
297
+ import IPython.display as ipd
298
+ ipd.display = old_display
299
+ except Exception: pass
300
+ self._flush_output()
301
+ if background:
302
+ threading.Thread(target=execute, daemon=True).start(); return None
303
+ execute(); return None
304
+
305
+ def write_line(self, text: object = "", *, color=None, bold: bool = False,
306
+ icon: str | None = None):
307
+ """Render a display-only line directly in xterm.js.
308
+
309
+ This path never touches the PowerShell/CMD/Bash input stream, preventing
310
+ ANSI reset sequences from being interpreted as commands on Windows.
311
+ """
312
+ prefix = f"{icon} " if icon else ""
313
+ value = ansi_text(prefix + str(text), color=color, bold=bold) + "\r\n"
314
+ self._queue_output(value)
315
+ return None
316
+
317
+ def success(self, text: object, *, bold: bool = False, icon: bool | str = True):
318
+ marker = "✔" if icon is True else (str(icon) if icon else None)
319
+ return self.write_line(text, color=Color.BRIGHT_GREEN, bold=bold, icon=marker)
320
+
321
+ def error(self, text: object, *, bold: bool = False, icon: bool | str = True):
322
+ marker = "✖" if icon is True else (str(icon) if icon else None)
323
+ return self.write_line(text, color=Color.BRIGHT_RED, bold=bold, icon=marker)
324
+
325
+ def warning(self, text: object, *, bold: bool = False, icon: bool | str = True):
326
+ marker = "⚠" if icon is True else (str(icon) if icon else None)
327
+ return self.write_line(text, color=Color.BRIGHT_YELLOW, bold=bold, icon=marker)
328
+
329
+ def info(self, text: object, *, bold: bool = False, icon: bool | str = True):
330
+ marker = "ℹ" if icon is True else (str(icon) if icon else None)
331
+ return self.write_line(text, color=Color.BRIGHT_CYAN, bold=bold, icon=marker)
332
+
333
+ def debug(self, text: object, *, bold: bool = False, icon: bool | str = True):
334
+ marker = "•" if icon is True else (str(icon) if icon else None)
335
+ return self.write_line(text, color=Color.BRIGHT_BLACK, bold=bold, icon=marker)
336
+
337
+ @property
338
+ def command_history(self) -> tuple[str, ...]:
339
+ return self.session.command_history
340
+
341
+ @property
342
+ def last_command(self) -> str | None:
343
+ return self.session.last_command
344
+
345
+ def history(self, limit: int | None = None) -> list[str]:
346
+ return self.session.history(limit)
347
+
348
+ def clear_history(self):
349
+ self.session.clear_history()
350
+ return None
351
+
352
+ def rerun(self, index: int = -1):
353
+ self.session.rerun(index)
354
+ return None
355
+
356
+ def run_many(self, commands, *, stop_on_error: bool = False):
357
+ self.session.run_many(commands, stop_on_error=stop_on_error)
358
+ return None
359
+
360
+ def send_key(self, key: str):
361
+ self.session.send_key(key)
362
+ return None
363
+
364
+ def wait_for(self, text: str, timeout: float = 10.0) -> bool:
365
+ return self.session.wait_for(text, timeout)
366
+
367
+ def wait_until_idle(self, idle_for: float = 0.25, timeout: float = 30.0) -> bool:
368
+ return self.session.wait_until_idle(idle_for, timeout)
369
+
370
+ def restart(self, *, clear: bool = True):
371
+ self.session.restart()
372
+ if clear:
373
+ self.clear()
374
+ return None
375
+
376
+ def interrupt(self): self.session.interrupt(); return None
377
+ def clear(self, *, clear_tabs: bool=False):
378
+ self._canvas.send({"type":"clear"})
379
+ if clear_tabs: self.clear_tabs()
380
+ return None
381
+ def clear_tabs(self):
382
+ for _title, child in self._tab_items:
383
+ try: child.close()
384
+ except Exception: pass
385
+ self._tab_items.clear(); self._refresh_tabs(); return None
386
+ def focus(self):
387
+ if self.interactive: self._canvas.send({"type":"focus"})
388
+ return None
389
+ def set_interactive(self, enabled: bool): self._canvas.interactive=bool(enabled); return None
390
+ @property
391
+ def output(self) -> str: return self.session.output
392
+ def close_terminal(self):
393
+ self._flush_output(); self._unsubscribe(); self.session.close(); self.close()
394
+
395
+
396
+ class ManagedTerminalWidget(TerminalWidget):
397
+ def __init__(self, *args, **kwargs): kwargs["interactive"] = False; super().__init__(*args, **kwargs)
398
+
399
+
400
+ def terminal(command: str | None = None, *, interactive: bool = True,
401
+ auto_display: bool = True, **kwargs):
402
+ return TerminalWidget(command=command, interactive=interactive,
403
+ auto_display=auto_display, **kwargs)
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import base64
5
+ import html
6
+ import json
7
+ import os
8
+ import runpy
9
+ import sys
10
+ import traceback
11
+ from pathlib import Path
12
+ from typing import Any, TextIO
13
+
14
+
15
+ def _writer(path: Path):
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ handle: TextIO = path.open("a", encoding="utf-8", buffering=1)
18
+
19
+ def emit(payload: dict[str, Any]) -> None:
20
+ handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
21
+ handle.flush()
22
+
23
+ return handle, emit
24
+
25
+
26
+ def _capture_factory(emit):
27
+ def emit_html(value: str, title: str = "Output") -> None:
28
+ emit({"kind": "html", "title": title, "html": value})
29
+
30
+ def emit_png(data: bytes, title: str = "Plot") -> None:
31
+ emit({
32
+ "kind": "image",
33
+ "title": title,
34
+ "mime": "image/png",
35
+ "data": base64.b64encode(data).decode("ascii"),
36
+ })
37
+
38
+ def capture(value: Any, *, title: str | None = None) -> None:
39
+ if value is None:
40
+ return
41
+ try:
42
+ import pandas as pd # type: ignore
43
+ if isinstance(value, pd.DataFrame):
44
+ frame = value.reset_index()
45
+ frame = frame.where(frame.notna(), None)
46
+ emit({"kind": "dataframe", "title": title or "DataFrame", "columns": [str(c) for c in frame.columns], "rows": frame.to_dict(orient="records")})
47
+ return
48
+ if isinstance(value, pd.Series):
49
+ frame = value.to_frame().reset_index().where(lambda x: x.notna(), None)
50
+ emit({"kind": "dataframe", "title": title or "Series", "columns": [str(c) for c in frame.columns], "rows": frame.to_dict(orient="records")})
51
+ return
52
+ except Exception:
53
+ pass
54
+ try:
55
+ import plotly.graph_objects as go # type: ignore
56
+ if isinstance(value, go.Figure):
57
+ from plotly.utils import PlotlyJSONEncoder # type: ignore
58
+ spec = json.loads(json.dumps(value.to_plotly_json(), cls=PlotlyJSONEncoder))
59
+ emit({"kind": "plotly", "title": title or "Plotly", "spec": spec})
60
+ return
61
+ except Exception:
62
+ pass
63
+ try:
64
+ from PIL import Image # type: ignore
65
+ if isinstance(value, Image.Image):
66
+ import io
67
+ buf = io.BytesIO(); value.save(buf, format="PNG")
68
+ emit_png(buf.getvalue(), title or "Image")
69
+ return
70
+ except Exception:
71
+ pass
72
+ repr_html = getattr(value, "_repr_html_", None)
73
+ if callable(repr_html):
74
+ try:
75
+ rendered = repr_html()
76
+ if rendered:
77
+ emit_html(str(rendered), title or type(value).__name__)
78
+ return
79
+ except Exception:
80
+ pass
81
+ repr_png = getattr(value, "_repr_png_", None)
82
+ if callable(repr_png):
83
+ try:
84
+ rendered = repr_png()
85
+ if rendered:
86
+ emit_png(rendered, title or type(value).__name__)
87
+ return
88
+ except Exception:
89
+ pass
90
+ emit_html(f"<pre>{html.escape(repr(value))}</pre>", title or type(value).__name__)
91
+
92
+ return capture, emit_png
93
+
94
+
95
+ def _execute_code(path: Path, capture) -> None:
96
+ source = path.read_text(encoding="utf-8")
97
+ tree = ast.parse(source, filename=str(path), mode="exec")
98
+ if tree.body and isinstance(tree.body[-1], ast.Expr):
99
+ last = tree.body[-1]
100
+ tree.body[-1] = ast.Expr(value=ast.Call(
101
+ func=ast.Name(id="__nbterm_display__", ctx=ast.Load()),
102
+ args=[last.value], keywords=[]))
103
+ ast.fix_missing_locations(tree)
104
+ namespace = {
105
+ "__name__": "__main__", "__file__": str(path),
106
+ "__nbterm_display__": capture, "display": capture,
107
+ }
108
+ exec(compile(tree, str(path), "exec"), namespace, namespace)
109
+
110
+
111
+ def main() -> int:
112
+ if len(sys.argv) < 5 or sys.argv[1] not in {"--code", "--file"} or sys.argv[3] != "--artifacts":
113
+ print("Usage: rich_runner.py (--code|--file) PATH --artifacts JSONL [args...]", file=sys.stderr)
114
+ return 2
115
+ mode, target, artifact_path = sys.argv[1], Path(sys.argv[2]).resolve(), Path(sys.argv[4]).resolve()
116
+ handle, emit = _writer(artifact_path)
117
+ capture, emit_png = _capture_factory(emit)
118
+
119
+ try:
120
+ import IPython.display as ipd # type: ignore
121
+ ipd.display = lambda *objects, **kwargs: [capture(obj, title=str(kwargs.get("display_id")) if kwargs.get("display_id") else None) for obj in objects]
122
+ except Exception:
123
+ pass
124
+
125
+ try:
126
+ import matplotlib
127
+ matplotlib.use("Agg", force=True)
128
+ import matplotlib.pyplot as plt # type: ignore
129
+ def show(*args: Any, **kwargs: Any) -> None:
130
+ import io
131
+ nums = list(plt.get_fignums())
132
+ for index, num in enumerate(nums, 1):
133
+ fig = plt.figure(num); buf = io.BytesIO()
134
+ fig.savefig(buf, format="png", bbox_inches="tight")
135
+ emit_png(buf.getvalue(), f"Plot {index}")
136
+ if nums: plt.close("all")
137
+ plt.show = show
138
+ except Exception:
139
+ pass
140
+
141
+ try:
142
+ if mode == "--code":
143
+ _execute_code(target, capture)
144
+ else:
145
+ old_argv = sys.argv[:]
146
+ sys.argv = [str(target), *sys.argv[5:]]
147
+ try: runpy.run_path(str(target), run_name="__main__")
148
+ finally: sys.argv = old_argv
149
+ return 0
150
+ except SystemExit as exc:
151
+ return int(exc.code or 0)
152
+ except BaseException:
153
+ traceback.print_exc(); return 1
154
+ finally:
155
+ handle.close()
156
+
157
+
158
+ if __name__ == "__main__":
159
+ raise SystemExit(main())