batchgrid 0.2.0b1__tar.gz → 0.2.0b3__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.
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/PKG-INFO +13 -9
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/README.md +12 -8
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/pyproject.toml +1 -1
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/__init__.py +1 -1
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/_cli.py +2 -2
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/_ui.py +110 -21
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/tests/test_ui.py +78 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/.gitignore +0 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/_api.py +0 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/errors.py +0 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/src/batchgrid/py.typed +0 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/tests/conftest.py +0 -0
- {batchgrid-0.2.0b1 → batchgrid-0.2.0b3}/tests/test_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: batchgrid
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0b3
|
|
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
|
|
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,14 +106,16 @@ 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
|
|
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.
|
|
112
114
|
|
|
113
|
-
The workspace runs on
|
|
114
|
-
browser has to be able to reach
|
|
115
|
-
a remote server
|
|
115
|
+
The workspace runs on the machine running Python and uses the keys from `batchgrid config` or the
|
|
116
|
+
environment there. Your browser has to be able to reach that machine. On Google Colab it reaches it
|
|
117
|
+
through Colab's own proxy, so `ui()` works there too; a notebook on another remote server is out of
|
|
118
|
+
reach.
|
|
116
119
|
|
|
117
120
|
From a terminal, the `batchgrid` command this package installs does the same:
|
|
118
121
|
|
|
@@ -163,9 +166,10 @@ except KeyboardInterrupt:
|
|
|
163
166
|
result = batchgrid.resume(df)
|
|
164
167
|
```
|
|
165
168
|
|
|
166
|
-
### `batchgrid.ui(data=None, *, open_browser=True, port=None, log=True) -> DataFrame | None`
|
|
169
|
+
### `batchgrid.ui(data=None, *, inline=None, height=720, open_browser=True, port=None, log=True) -> DataFrame | None`
|
|
167
170
|
|
|
168
|
-
See [In the browser](#in-the-browser). `
|
|
171
|
+
See [In the browser](#in-the-browser). `inline` shows the workspace in the cell, `height` pixels tall;
|
|
172
|
+
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
173
|
`sheet`, `browser_error` and `closed`.
|
|
170
174
|
|
|
171
175
|
### Errors
|
|
@@ -191,7 +195,7 @@ for the CLI in this order:
|
|
|
191
195
|
1. the `cli=[...]` argument
|
|
192
196
|
2. the `BATCHGRID_CLI` environment variable, e.g. `node /path/to/cli/dist/index.js`
|
|
193
197
|
3. the npm `batchgrid` on the PATH (never this package's own command)
|
|
194
|
-
4. `npx "batchgrid@>=0.3.
|
|
198
|
+
4. `npx "batchgrid@>=0.3.1 <1"`
|
|
195
199
|
|
|
196
200
|
## Development
|
|
197
201
|
|
|
@@ -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
|
|
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,14 +80,16 @@ 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
|
|
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.
|
|
86
88
|
|
|
87
|
-
The workspace runs on
|
|
88
|
-
browser has to be able to reach
|
|
89
|
-
a remote server
|
|
89
|
+
The workspace runs on the machine running Python and uses the keys from `batchgrid config` or the
|
|
90
|
+
environment there. Your browser has to be able to reach that machine. On Google Colab it reaches it
|
|
91
|
+
through Colab's own proxy, so `ui()` works there too; a notebook on another remote server is out of
|
|
92
|
+
reach.
|
|
90
93
|
|
|
91
94
|
From a terminal, the `batchgrid` command this package installs does the same:
|
|
92
95
|
|
|
@@ -137,9 +140,10 @@ except KeyboardInterrupt:
|
|
|
137
140
|
result = batchgrid.resume(df)
|
|
138
141
|
```
|
|
139
142
|
|
|
140
|
-
### `batchgrid.ui(data=None, *, open_browser=True, port=None, log=True) -> DataFrame | None`
|
|
143
|
+
### `batchgrid.ui(data=None, *, inline=None, height=720, open_browser=True, port=None, log=True) -> DataFrame | None`
|
|
141
144
|
|
|
142
|
-
See [In the browser](#in-the-browser). `
|
|
145
|
+
See [In the browser](#in-the-browser). `inline` shows the workspace in the cell, `height` pixels tall;
|
|
146
|
+
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
147
|
`sheet`, `browser_error` and `closed`.
|
|
144
148
|
|
|
145
149
|
### Errors
|
|
@@ -165,7 +169,7 @@ for the CLI in this order:
|
|
|
165
169
|
1. the `cli=[...]` argument
|
|
166
170
|
2. the `BATCHGRID_CLI` environment variable, e.g. `node /path/to/cli/dist/index.js`
|
|
167
171
|
3. the npm `batchgrid` on the PATH (never this package's own command)
|
|
168
|
-
4. `npx "batchgrid@>=0.3.
|
|
172
|
+
4. `npx "batchgrid@>=0.3.1 <1"`
|
|
169
173
|
|
|
170
174
|
## Development
|
|
171
175
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batchgrid"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.0b3"
|
|
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"
|
|
@@ -19,8 +19,8 @@ import threading
|
|
|
19
19
|
from collections import deque
|
|
20
20
|
from typing import Callable, Deque, Dict, Iterator, List, Mapping, Optional, Sequence
|
|
21
21
|
|
|
22
|
-
#: The CLI releases this package speaks to: 0.3.0 was the first with ``ui``, and its events only gain fields
|
|
23
|
-
CLI_SPEC = "batchgrid@>=0.3.
|
|
22
|
+
#: The CLI releases this package speaks to: 0.3.0 was the first with ``ui``, 0.3.1 the first behind Colab's proxy, and its events only gain fields
|
|
23
|
+
CLI_SPEC = "batchgrid@>=0.3.1 <1"
|
|
24
24
|
|
|
25
25
|
Event = Dict[str, object]
|
|
26
26
|
|
|
@@ -1,11 +1,13 @@
|
|
|
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
|
|
|
5
5
|
import os
|
|
6
|
+
import socket
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional, Sequence
|
|
10
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
9
11
|
|
|
10
12
|
from ._api import Data, EventHandler, _input_file, _is_dataframe, _merge_output, _TempDir
|
|
11
13
|
from ._cli import Event, find_cli, stream_events
|
|
@@ -18,6 +20,8 @@ if TYPE_CHECKING:
|
|
|
18
20
|
def ui(
|
|
19
21
|
data: Optional[Data] = None,
|
|
20
22
|
*,
|
|
23
|
+
inline: Optional[bool] = None,
|
|
24
|
+
height: int = 720,
|
|
21
25
|
open_browser: bool = True,
|
|
22
26
|
port: Optional[int] = None,
|
|
23
27
|
log: bool = True,
|
|
@@ -25,28 +29,41 @@ def ui(
|
|
|
25
29
|
cli: Optional[Sequence[str]] = None,
|
|
26
30
|
env: Optional[Mapping[str, str]] = None,
|
|
27
31
|
) -> Optional["pd.DataFrame"]:
|
|
28
|
-
"""Open the batchgrid workspace
|
|
32
|
+
"""Open the batchgrid workspace and wait until you are done there.
|
|
29
33
|
|
|
30
|
-
``data`` - a file path or a pandas DataFrame - opens in the sheet.
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
``data`` - a file path or a pandas DataFrame - opens in the sheet. In a
|
|
35
|
+
Jupyter notebook the workspace shows in the cell's output, ``height``
|
|
36
|
+
pixels tall; ``inline=False`` opens it in a browser tab instead, which is
|
|
37
|
+
what happens outside a notebook. Chat, plan and run there; what happens is
|
|
38
|
+
printed here as it happens, with a progress bar while rows run.
|
|
33
39
|
|
|
34
|
-
Press Finish in the
|
|
35
|
-
back. The sheet as the tab last showed it is returned as a DataFrame: for
|
|
40
|
+
Press Finish in the workspace, close its tab, or interrupt the cell
|
|
41
|
+
(Ctrl+C) to come back. The sheet as the tab last showed it is returned as a DataFrame: for
|
|
36
42
|
a DataFrame you passed in, a copy with the new columns added and your
|
|
37
43
|
edits in it. None when nothing was opened, or pandas is not installed.
|
|
38
44
|
|
|
39
|
-
It runs
|
|
40
|
-
|
|
45
|
+
It runs where Python runs, so the browser has to reach that machine. On
|
|
46
|
+
Colab it does through Colab's own proxy; a notebook on another remote
|
|
47
|
+
server is out of reach.
|
|
41
48
|
"""
|
|
42
|
-
|
|
49
|
+
embed = _in_notebook() if inline is None else inline
|
|
50
|
+
colab = _in_colab()
|
|
51
|
+
frame_view = _Frame(height) if embed else None
|
|
52
|
+
printer = _Printer(link=not embed) if log else None
|
|
43
53
|
with _input_file(data) as (path, frame):
|
|
44
54
|
out = _TempDir()
|
|
45
55
|
try:
|
|
46
56
|
sheet = os.path.join(out.get(), "sheet.csv")
|
|
47
57
|
args: List[str] = ["ui", *([path] if path else []), "--output", sheet, "--json"]
|
|
48
|
-
|
|
58
|
+
# On Colab the browser that would open is on Google's machine, not yours
|
|
59
|
+
if embed or colab or not open_browser:
|
|
49
60
|
args.append("--no-open")
|
|
61
|
+
proxy: Optional[str] = None
|
|
62
|
+
if colab:
|
|
63
|
+
# The proxy's address names the port, so the port is settled before the server starts
|
|
64
|
+
port = port if port is not None else _free_port()
|
|
65
|
+
proxy = _colab_proxy(port)
|
|
66
|
+
args += ["--allow-host", str(urlsplit(proxy).hostname)]
|
|
50
67
|
if port is not None:
|
|
51
68
|
args += ["--port", str(port)]
|
|
52
69
|
|
|
@@ -59,6 +76,10 @@ def ui(
|
|
|
59
76
|
failure = event
|
|
60
77
|
elif event.get("type") == "ui":
|
|
61
78
|
started = True
|
|
79
|
+
if proxy is not None:
|
|
80
|
+
event = {**event, "url": _through(proxy, str(event.get("url")))}
|
|
81
|
+
if frame_view is not None:
|
|
82
|
+
frame_view.show(str(event.get("url")))
|
|
62
83
|
elif event.get("type") == "exit":
|
|
63
84
|
ended = event
|
|
64
85
|
if printer is not None:
|
|
@@ -71,6 +92,8 @@ def ui(
|
|
|
71
92
|
finally:
|
|
72
93
|
if printer is not None:
|
|
73
94
|
printer.close()
|
|
95
|
+
if frame_view is not None:
|
|
96
|
+
frame_view.close()
|
|
74
97
|
if failure is not None:
|
|
75
98
|
raise BatchgridError(str(failure.get("code")), str(failure.get("message")))
|
|
76
99
|
if not started and ended is not None and ended.get("code"):
|
|
@@ -110,11 +133,81 @@ def _read_sheet(frame: Any, sheet: Path, sent: Optional[Path]) -> Optional["pd.D
|
|
|
110
133
|
return merged
|
|
111
134
|
|
|
112
135
|
|
|
136
|
+
def _in_colab() -> bool:
|
|
137
|
+
"""Whether this is a Colab kernel, on a machine the browser reaches only through Colab's proxy"""
|
|
138
|
+
return "google.colab" in sys.modules
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _colab_proxy(port: int) -> str:
|
|
142
|
+
"""The address Colab's proxy gives a port on the kernel's machine"""
|
|
143
|
+
from google.colab.output import eval_js # type: ignore[import-not-found]
|
|
144
|
+
|
|
145
|
+
return str(eval_js(f"google.colab.kernel.proxyPort({port}, {{cache: false}})"))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _through(proxy: str, url: str) -> str:
|
|
149
|
+
"""The server's link, with the proxy's address in place of 127.0.0.1"""
|
|
150
|
+
base, own = urlsplit(proxy), urlsplit(url)
|
|
151
|
+
query = "&".join(q for q in (base.query, own.query) if q)
|
|
152
|
+
return urlunsplit((base.scheme, base.netloc, own.path or "/", query, ""))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _free_port() -> int:
|
|
156
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
157
|
+
probe.bind(("127.0.0.1", 0))
|
|
158
|
+
return int(probe.getsockname()[1])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _in_notebook() -> bool:
|
|
162
|
+
"""Whether this runs in a Jupyter kernel, whose front end can show the workspace in a cell"""
|
|
163
|
+
try:
|
|
164
|
+
from IPython import get_ipython
|
|
165
|
+
|
|
166
|
+
shell = get_ipython()
|
|
167
|
+
return shell is not None and "IPKernelApp" in shell.config
|
|
168
|
+
except Exception: # noqa: BLE001 - no IPython means a plain console
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _Frame:
|
|
173
|
+
"""The workspace in the cell's output; once it stops, a note takes its place"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, height: int) -> None:
|
|
176
|
+
self.height = height
|
|
177
|
+
self.handle: Any = None
|
|
178
|
+
|
|
179
|
+
def show(self, url: str) -> None:
|
|
180
|
+
from IPython.display import IFrame, display
|
|
181
|
+
|
|
182
|
+
self.handle = display(
|
|
183
|
+
IFrame(
|
|
184
|
+
url,
|
|
185
|
+
width="100%",
|
|
186
|
+
height=self.height,
|
|
187
|
+
extras=[
|
|
188
|
+
'style="border: 1px solid rgba(128, 128, 128, 0.35); border-radius: 6px"',
|
|
189
|
+
'allow="clipboard-read; clipboard-write"',
|
|
190
|
+
],
|
|
191
|
+
),
|
|
192
|
+
display_id=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def close(self) -> None:
|
|
196
|
+
if self.handle is None:
|
|
197
|
+
return
|
|
198
|
+
from IPython.display import HTML
|
|
199
|
+
|
|
200
|
+
# The server behind the frame is gone, so a saved notebook keeps no dead workspace
|
|
201
|
+
self.handle.update(HTML("<em>batchgrid closed. Run the cell again to reopen it.</em>"))
|
|
202
|
+
self.handle = None
|
|
203
|
+
|
|
204
|
+
|
|
113
205
|
class _Printer:
|
|
114
206
|
"""The terminal's log, in a notebook cell or a Python console"""
|
|
115
207
|
|
|
116
|
-
def __init__(self) -> None:
|
|
208
|
+
def __init__(self, link: bool = True) -> None:
|
|
117
209
|
self.bar: Any = None
|
|
210
|
+
self.link = link
|
|
118
211
|
try:
|
|
119
212
|
from tqdm.auto import tqdm
|
|
120
213
|
|
|
@@ -130,7 +223,7 @@ class _Printer:
|
|
|
130
223
|
# Anything else ends the run's bar, so its lines are not drawn over it
|
|
131
224
|
if kind in ("result", "closed", "user", "plan"):
|
|
132
225
|
self.close()
|
|
133
|
-
if kind == "ui":
|
|
226
|
+
if kind == "ui" and self.link:
|
|
134
227
|
self._link(str(event.get("url")))
|
|
135
228
|
elif kind == "user":
|
|
136
229
|
self._line(f"\n❯ {event.get('text')}")
|
|
@@ -155,7 +248,7 @@ class _Printer:
|
|
|
155
248
|
elif kind == "error":
|
|
156
249
|
self._line(f"✖ {event.get('message')}")
|
|
157
250
|
elif kind == "closed":
|
|
158
|
-
why = {"tab_closed": "The tab was closed", "finished": "Finished in the browser"}.get(
|
|
251
|
+
why = {"tab_closed": "The tab was closed", "finished": "Finished in the browser" if self.link else "Finished"}.get(
|
|
159
252
|
str(event.get("reason")), "Stopped"
|
|
160
253
|
)
|
|
161
254
|
self._line(f"\n{why}.")
|
|
@@ -173,15 +266,11 @@ class _Printer:
|
|
|
173
266
|
self.bar.refresh()
|
|
174
267
|
|
|
175
268
|
def _link(self, url: str) -> None:
|
|
176
|
-
|
|
177
|
-
from IPython import get_ipython
|
|
269
|
+
if _in_notebook():
|
|
178
270
|
from IPython.display import HTML, display
|
|
179
271
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return
|
|
183
|
-
except Exception: # noqa: BLE001 - no IPython means a plain console
|
|
184
|
-
pass
|
|
272
|
+
display(HTML(f'batchgrid is open at <a href="{url}" target="_blank">{url.split("?")[0]}</a>'))
|
|
273
|
+
return
|
|
185
274
|
self._line(f"batchgrid is open at {url}")
|
|
186
275
|
|
|
187
276
|
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,83 @@ 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
|
+
|
|
172
|
+
def test_on_colab_the_workspace_goes_through_its_proxy(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
173
|
+
asked: List[str] = []
|
|
174
|
+
|
|
175
|
+
def eval_js(script: str) -> str:
|
|
176
|
+
asked.append(script)
|
|
177
|
+
port = script.split("(")[1].split(",")[0]
|
|
178
|
+
return f"https://{port}-abc-colab.googleusercontent.com/?authuser=0"
|
|
179
|
+
|
|
180
|
+
# A stand-in for the module a Colab kernel has loaded
|
|
181
|
+
colab = types.ModuleType("google.colab")
|
|
182
|
+
colab_output = types.ModuleType("google.colab.output")
|
|
183
|
+
colab_output.eval_js = eval_js # type: ignore[attr-defined]
|
|
184
|
+
monkeypatch.setitem(sys.modules, "google.colab", colab)
|
|
185
|
+
monkeypatch.setitem(sys.modules, "google.colab.output", colab_output)
|
|
186
|
+
|
|
187
|
+
seen: Dict[str, Any] = {}
|
|
188
|
+
|
|
189
|
+
def on_event(event: Dict[str, Any]) -> None:
|
|
190
|
+
if event["type"] != "ui":
|
|
191
|
+
return
|
|
192
|
+
seen["url"] = event["url"]
|
|
193
|
+
port = asked[0].split("(")[1].split(",")[0]
|
|
194
|
+
token = event["url"].split("token=")[1]
|
|
195
|
+
# What Colab's proxy sends on: the request addressed to its own host name
|
|
196
|
+
page = urllib.request.Request(
|
|
197
|
+
f"http://127.0.0.1:{port}/?token={token}", headers={"Host": f"{port}-abc-colab.googleusercontent.com"}
|
|
198
|
+
)
|
|
199
|
+
with urllib.request.urlopen(page, timeout=10) as response:
|
|
200
|
+
seen["status"] = response.status
|
|
201
|
+
tab = FakeTab(f"http://127.0.0.1:{port}/?token={token}")
|
|
202
|
+
threading.Thread(target=tab.call, args=("finish", {}), daemon=True).start()
|
|
203
|
+
|
|
204
|
+
batchgrid.ui(inline=False, log=False, on_event=on_event)
|
|
205
|
+
|
|
206
|
+
assert seen["url"].startswith("https://") and "-abc-colab.googleusercontent.com/?authuser=0&token=" in seen["url"]
|
|
207
|
+
assert seen["status"] == 200
|
|
208
|
+
|
|
209
|
+
|
|
132
210
|
def test_nothing_synced_returns_none() -> None:
|
|
133
211
|
result = batchgrid.ui(open_browser=False, log=False, on_event=open_tab(lambda tab: tab.call("finish", {})))
|
|
134
212
|
assert result is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|