batchgrid 0.2.0b1__tar.gz → 0.2.0b2__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.5
2
2
  Name: batchgrid
3
- Version: 0.2.0b1
3
+ Version: 0.2.0b2
4
4
  Summary: Run a prompt, or a whole pipeline, over every row of a DataFrame or spreadsheet - with cost estimates, retries and resumable runs.
5
5
  Project-URL: Homepage, https://github.com/mertguvencli/batchgrid
6
6
  Project-URL: Source, https://github.com/mertguvencli/batchgrid/tree/main/python
@@ -83,7 +83,8 @@ The file is the same one `batchgrid --save-plan` writes and `batchgrid --plan` r
83
83
 
84
84
  ## In the browser
85
85
 
86
- `batchgrid.ui()` opens the batchgrid workspace in your browser with your data in the sheet. You can
86
+ `batchgrid.ui()` opens the batchgrid workspace with your data in the sheet. In a Jupyter notebook it
87
+ shows in the cell's output; elsewhere, or with `inline=False`, it opens in your browser. You can
87
88
  chat, check the plan and run it there, while the log prints in the notebook cell or terminal as it
88
89
  happens:
89
90
 
@@ -105,7 +106,8 @@ batchgrid: 100%|██████████| 1000/1000 [00:42<00:00, failed=0
105
106
  Finished in the browser.
106
107
  ```
107
108
 
108
- To come back, press **Finish** in the tab, close the tab, or interrupt the cell. `ui()` returns the
109
+ To come back, press **Finish** in the workspace, close its tab, or interrupt the cell. Once it stops,
110
+ a note takes the workspace's place in the cell. `ui()` returns the
109
111
  sheet as the tab last showed it. For a DataFrame, that is a copy of yours with the new columns and
110
112
  any cells you edited; untouched columns keep their dtypes. `ui()` with no data starts on an empty
111
113
  sheet.
@@ -163,9 +165,10 @@ except KeyboardInterrupt:
163
165
  result = batchgrid.resume(df)
164
166
  ```
165
167
 
166
- ### `batchgrid.ui(data=None, *, open_browser=True, port=None, log=True) -> DataFrame | None`
168
+ ### `batchgrid.ui(data=None, *, inline=None, height=720, open_browser=True, port=None, log=True) -> DataFrame | None`
167
169
 
168
- See [In the browser](#in-the-browser). `on_event` receives the same events as in `run()`, plus `ui`,
170
+ See [In the browser](#in-the-browser). `inline` shows the workspace in the cell, `height` pixels tall;
171
+ by default it does in a Jupyter notebook and opens a browser tab anywhere else. `on_event` receives the same events as in `run()`, plus `ui`,
169
172
  `sheet`, `browser_error` and `closed`.
170
173
 
171
174
  ### Errors
@@ -57,7 +57,8 @@ The file is the same one `batchgrid --save-plan` writes and `batchgrid --plan` r
57
57
 
58
58
  ## In the browser
59
59
 
60
- `batchgrid.ui()` opens the batchgrid workspace in your browser with your data in the sheet. You can
60
+ `batchgrid.ui()` opens the batchgrid workspace with your data in the sheet. In a Jupyter notebook it
61
+ shows in the cell's output; elsewhere, or with `inline=False`, it opens in your browser. You can
61
62
  chat, check the plan and run it there, while the log prints in the notebook cell or terminal as it
62
63
  happens:
63
64
 
@@ -79,7 +80,8 @@ batchgrid: 100%|██████████| 1000/1000 [00:42<00:00, failed=0
79
80
  Finished in the browser.
80
81
  ```
81
82
 
82
- To come back, press **Finish** in the tab, close the tab, or interrupt the cell. `ui()` returns the
83
+ To come back, press **Finish** in the workspace, close its tab, or interrupt the cell. Once it stops,
84
+ a note takes the workspace's place in the cell. `ui()` returns the
83
85
  sheet as the tab last showed it. For a DataFrame, that is a copy of yours with the new columns and
84
86
  any cells you edited; untouched columns keep their dtypes. `ui()` with no data starts on an empty
85
87
  sheet.
@@ -137,9 +139,10 @@ except KeyboardInterrupt:
137
139
  result = batchgrid.resume(df)
138
140
  ```
139
141
 
140
- ### `batchgrid.ui(data=None, *, open_browser=True, port=None, log=True) -> DataFrame | None`
142
+ ### `batchgrid.ui(data=None, *, inline=None, height=720, open_browser=True, port=None, log=True) -> DataFrame | None`
141
143
 
142
- See [In the browser](#in-the-browser). `on_event` receives the same events as in `run()`, plus `ui`,
144
+ See [In the browser](#in-the-browser). `inline` shows the workspace in the cell, `height` pixels tall;
145
+ by default it does in a Jupyter notebook and opens a browser tab anywhere else. `on_event` receives the same events as in `run()`, plus `ui`,
143
146
  `sheet`, `browser_error` and `closed`.
144
147
 
145
148
  ### Errors
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "batchgrid"
7
- version = "0.2.0b1"
7
+ version = "0.2.0b2"
8
8
  description = "Run a prompt, or a whole pipeline, over every row of a DataFrame or spreadsheet - with cost estimates, retries and resumable runs."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,7 +16,7 @@ from ._ui import ui
16
16
  from ._cli import CLI_SPEC, find_cli
17
17
  from .errors import BatchgridError, CostLimitError, MissingSecretsError, NoPlanError
18
18
 
19
- __version__ = "0.2.0b1"
19
+ __version__ = "0.2.0b2"
20
20
 
21
21
  __all__ = [
22
22
  "BatchgridError",
@@ -1,4 +1,4 @@
1
- """ui() - the web workspace in the browser, with its log here and its sheet coming back."""
1
+ """ui() - the web workspace in the notebook or the browser, with its log here and its sheet coming back."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -18,6 +18,8 @@ if TYPE_CHECKING:
18
18
  def ui(
19
19
  data: Optional[Data] = None,
20
20
  *,
21
+ inline: Optional[bool] = None,
22
+ height: int = 720,
21
23
  open_browser: bool = True,
22
24
  port: Optional[int] = None,
23
25
  log: bool = True,
@@ -25,27 +27,31 @@ def ui(
25
27
  cli: Optional[Sequence[str]] = None,
26
28
  env: Optional[Mapping[str, str]] = None,
27
29
  ) -> Optional["pd.DataFrame"]:
28
- """Open the batchgrid workspace in the browser and wait until you are done there.
30
+ """Open the batchgrid workspace and wait until you are done there.
29
31
 
30
- ``data`` - a file path or a pandas DataFrame - opens in the sheet. Chat,
31
- plan and run in the browser; what happens there is printed here as it
32
- happens, with a progress bar while rows run.
32
+ ``data`` - a file path or a pandas DataFrame - opens in the sheet. In a
33
+ Jupyter notebook the workspace shows in the cell's output, ``height``
34
+ pixels tall; ``inline=False`` opens it in a browser tab instead, which is
35
+ what happens outside a notebook. Chat, plan and run there; what happens is
36
+ printed here as it happens, with a progress bar while rows run.
33
37
 
34
- Press Finish in the tab, close it, or interrupt the cell (Ctrl+C) to come
35
- back. The sheet as the tab last showed it is returned as a DataFrame: for
38
+ Press Finish in the workspace, close its tab, or interrupt the cell
39
+ (Ctrl+C) to come back. The sheet as the tab last showed it is returned as a DataFrame: for
36
40
  a DataFrame you passed in, a copy with the new columns added and your
37
41
  edits in it. None when nothing was opened, or pandas is not installed.
38
42
 
39
43
  It runs on this machine, so it needs a browser that can reach it - not a
40
44
  notebook running on a remote server.
41
45
  """
42
- printer = _Printer() if log else None
46
+ embed = _in_notebook() if inline is None else inline
47
+ frame_view = _Frame(height) if embed else None
48
+ printer = _Printer(link=not embed) if log else None
43
49
  with _input_file(data) as (path, frame):
44
50
  out = _TempDir()
45
51
  try:
46
52
  sheet = os.path.join(out.get(), "sheet.csv")
47
53
  args: List[str] = ["ui", *([path] if path else []), "--output", sheet, "--json"]
48
- if not open_browser:
54
+ if embed or not open_browser:
49
55
  args.append("--no-open")
50
56
  if port is not None:
51
57
  args += ["--port", str(port)]
@@ -59,6 +65,8 @@ def ui(
59
65
  failure = event
60
66
  elif event.get("type") == "ui":
61
67
  started = True
68
+ if frame_view is not None:
69
+ frame_view.show(str(event.get("url")))
62
70
  elif event.get("type") == "exit":
63
71
  ended = event
64
72
  if printer is not None:
@@ -71,6 +79,8 @@ def ui(
71
79
  finally:
72
80
  if printer is not None:
73
81
  printer.close()
82
+ if frame_view is not None:
83
+ frame_view.close()
74
84
  if failure is not None:
75
85
  raise BatchgridError(str(failure.get("code")), str(failure.get("message")))
76
86
  if not started and ended is not None and ended.get("code"):
@@ -110,11 +120,56 @@ def _read_sheet(frame: Any, sheet: Path, sent: Optional[Path]) -> Optional["pd.D
110
120
  return merged
111
121
 
112
122
 
123
+ def _in_notebook() -> bool:
124
+ """Whether this runs in a Jupyter kernel, whose front end can show the workspace in a cell"""
125
+ try:
126
+ from IPython import get_ipython
127
+
128
+ shell = get_ipython()
129
+ return shell is not None and "IPKernelApp" in shell.config
130
+ except Exception: # noqa: BLE001 - no IPython means a plain console
131
+ return False
132
+
133
+
134
+ class _Frame:
135
+ """The workspace in the cell's output; once it stops, a note takes its place"""
136
+
137
+ def __init__(self, height: int) -> None:
138
+ self.height = height
139
+ self.handle: Any = None
140
+
141
+ def show(self, url: str) -> None:
142
+ from IPython.display import IFrame, display
143
+
144
+ self.handle = display(
145
+ IFrame(
146
+ url,
147
+ width="100%",
148
+ height=self.height,
149
+ extras=[
150
+ 'style="border: 1px solid rgba(128, 128, 128, 0.35); border-radius: 6px"',
151
+ 'allow="clipboard-read; clipboard-write"',
152
+ ],
153
+ ),
154
+ display_id=True,
155
+ )
156
+
157
+ def close(self) -> None:
158
+ if self.handle is None:
159
+ return
160
+ from IPython.display import HTML
161
+
162
+ # The server behind the frame is gone, so a saved notebook keeps no dead workspace
163
+ self.handle.update(HTML("<em>batchgrid closed. Run the cell again to reopen it.</em>"))
164
+ self.handle = None
165
+
166
+
113
167
  class _Printer:
114
168
  """The terminal's log, in a notebook cell or a Python console"""
115
169
 
116
- def __init__(self) -> None:
170
+ def __init__(self, link: bool = True) -> None:
117
171
  self.bar: Any = None
172
+ self.link = link
118
173
  try:
119
174
  from tqdm.auto import tqdm
120
175
 
@@ -130,7 +185,7 @@ class _Printer:
130
185
  # Anything else ends the run's bar, so its lines are not drawn over it
131
186
  if kind in ("result", "closed", "user", "plan"):
132
187
  self.close()
133
- if kind == "ui":
188
+ if kind == "ui" and self.link:
134
189
  self._link(str(event.get("url")))
135
190
  elif kind == "user":
136
191
  self._line(f"\n❯ {event.get('text')}")
@@ -155,7 +210,7 @@ class _Printer:
155
210
  elif kind == "error":
156
211
  self._line(f"✖ {event.get('message')}")
157
212
  elif kind == "closed":
158
- why = {"tab_closed": "The tab was closed", "finished": "Finished in the browser"}.get(
213
+ why = {"tab_closed": "The tab was closed", "finished": "Finished in the browser" if self.link else "Finished"}.get(
159
214
  str(event.get("reason")), "Stopped"
160
215
  )
161
216
  self._line(f"\n{why}.")
@@ -173,15 +228,11 @@ class _Printer:
173
228
  self.bar.refresh()
174
229
 
175
230
  def _link(self, url: str) -> None:
176
- try:
177
- from IPython import get_ipython
231
+ if _in_notebook():
178
232
  from IPython.display import HTML, display
179
233
 
180
- if get_ipython() is not None and "IPKernelApp" in get_ipython().config:
181
- display(HTML(f'batchgrid is open at <a href="{url}" target="_blank">{url.split("?")[0]}</a>'))
182
- return
183
- except Exception: # noqa: BLE001 - no IPython means a plain console
184
- pass
234
+ display(HTML(f'batchgrid is open at <a href="{url}" target="_blank">{url.split("?")[0]}</a>'))
235
+ return
185
236
  self._line(f"batchgrid is open at {url}")
186
237
 
187
238
  def _line(self, text: str) -> None:
@@ -14,6 +14,7 @@ import stat
14
14
  import sys
15
15
  import threading
16
16
  import time
17
+ import types
17
18
  import urllib.request
18
19
  from pathlib import Path
19
20
  from typing import Any, Dict, List
@@ -129,6 +130,45 @@ def test_interrupting_the_cell_returns_instead_of_raising() -> None:
129
130
  assert result.loc[0, "sentiment"] == "positive"
130
131
 
131
132
 
133
+ def test_in_a_notebook_the_workspace_shows_in_the_cell(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
134
+ shown: List[Any] = []
135
+
136
+ class Handle:
137
+ def update(self, obj: Any) -> None:
138
+ shown.append(obj)
139
+
140
+ def display(obj: Any, display_id: bool = False) -> Handle:
141
+ shown.append(obj)
142
+ return Handle()
143
+
144
+ # A stand-in for the IPython a Jupyter kernel runs
145
+ ipython = types.ModuleType("IPython")
146
+ ipython.get_ipython = lambda: types.SimpleNamespace(config={"IPKernelApp": {}}) # type: ignore[attr-defined]
147
+ ipython_display = types.ModuleType("IPython.display")
148
+ ipython_display.HTML = lambda html: html # type: ignore[attr-defined]
149
+ ipython_display.IFrame = lambda src, **options: {"src": src, **options} # type: ignore[attr-defined]
150
+ ipython_display.display = display # type: ignore[attr-defined]
151
+ monkeypatch.setitem(sys.modules, "IPython", ipython)
152
+ monkeypatch.setitem(sys.modules, "IPython.display", ipython_display)
153
+
154
+ urls: List[str] = []
155
+
156
+ def act(tab: FakeTab) -> None:
157
+ urls.append(tab.url)
158
+ tab.call("finish", {})
159
+
160
+ # No open_browser=False: showing it in the cell never opens a tab
161
+ batchgrid.ui(on_event=open_tab(act))
162
+
163
+ assert len(shown) == 2
164
+ assert shown[0]["src"] == urls[0] and shown[0]["height"] == 720
165
+ # The server behind it stopped, so a note takes the workspace's place
166
+ assert "batchgrid closed" in shown[1]
167
+ printed = capsys.readouterr().out
168
+ assert "is open at" not in printed
169
+ assert "Finished." in printed
170
+
171
+
132
172
  def test_nothing_synced_returns_none() -> None:
133
173
  result = batchgrid.ui(open_browser=False, log=False, on_event=open_tab(lambda tab: tab.call("finish", {})))
134
174
  assert result is None
File without changes