batchgrid 0.2.0b2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: batchgrid
3
- Version: 0.2.0b2
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
@@ -112,9 +112,10 @@ sheet as the tab last showed it. For a DataFrame, that is a copy of yours with t
112
112
  any cells you edited; untouched columns keep their dtypes. `ui()` with no data starts on an empty
113
113
  sheet.
114
114
 
115
- The workspace runs on your machine and uses the keys from `batchgrid config` or your environment. Your
116
- browser has to be able to reach the machine running Python, so this does not work for a notebook on
117
- 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.
118
119
 
119
120
  From a terminal, the `batchgrid` command this package installs does the same:
120
121
 
@@ -194,7 +195,7 @@ for the CLI in this order:
194
195
  1. the `cli=[...]` argument
195
196
  2. the `BATCHGRID_CLI` environment variable, e.g. `node /path/to/cli/dist/index.js`
196
197
  3. the npm `batchgrid` on the PATH (never this package's own command)
197
- 4. `npx "batchgrid@>=0.3.0 <1"`
198
+ 4. `npx "batchgrid@>=0.3.1 <1"`
198
199
 
199
200
  ## Development
200
201
 
@@ -86,9 +86,10 @@ sheet as the tab last showed it. For a DataFrame, that is a copy of yours with t
86
86
  any cells you edited; untouched columns keep their dtypes. `ui()` with no data starts on an empty
87
87
  sheet.
88
88
 
89
- The workspace runs on your machine and uses the keys from `batchgrid config` or your environment. Your
90
- browser has to be able to reach the machine running Python, so this does not work for a notebook on
91
- 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.
92
93
 
93
94
  From a terminal, the `batchgrid` command this package installs does the same:
94
95
 
@@ -168,7 +169,7 @@ for the CLI in this order:
168
169
  1. the `cli=[...]` argument
169
170
  2. the `BATCHGRID_CLI` environment variable, e.g. `node /path/to/cli/dist/index.js`
170
171
  3. the npm `batchgrid` on the PATH (never this package's own command)
171
- 4. `npx "batchgrid@>=0.3.0 <1"`
172
+ 4. `npx "batchgrid@>=0.3.1 <1"`
172
173
 
173
174
  ## Development
174
175
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "batchgrid"
7
- version = "0.2.0b2"
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"
@@ -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.0b2"
19
+ __version__ = "0.2.0b3"
20
20
 
21
21
  __all__ = [
22
22
  "BatchgridError",
@@ -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.0 <1"
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
 
@@ -3,9 +3,11 @@
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
@@ -40,10 +42,12 @@ def ui(
40
42
  a DataFrame you passed in, a copy with the new columns added and your
41
43
  edits in it. None when nothing was opened, or pandas is not installed.
42
44
 
43
- It runs on this machine, so it needs a browser that can reach it - not a
44
- notebook running on a remote server.
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.
45
48
  """
46
49
  embed = _in_notebook() if inline is None else inline
50
+ colab = _in_colab()
47
51
  frame_view = _Frame(height) if embed else None
48
52
  printer = _Printer(link=not embed) if log else None
49
53
  with _input_file(data) as (path, frame):
@@ -51,8 +55,15 @@ def ui(
51
55
  try:
52
56
  sheet = os.path.join(out.get(), "sheet.csv")
53
57
  args: List[str] = ["ui", *([path] if path else []), "--output", sheet, "--json"]
54
- if embed or not open_browser:
58
+ # On Colab the browser that would open is on Google's machine, not yours
59
+ if embed or colab or not open_browser:
55
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)]
56
67
  if port is not None:
57
68
  args += ["--port", str(port)]
58
69
 
@@ -65,6 +76,8 @@ def ui(
65
76
  failure = event
66
77
  elif event.get("type") == "ui":
67
78
  started = True
79
+ if proxy is not None:
80
+ event = {**event, "url": _through(proxy, str(event.get("url")))}
68
81
  if frame_view is not None:
69
82
  frame_view.show(str(event.get("url")))
70
83
  elif event.get("type") == "exit":
@@ -120,6 +133,31 @@ def _read_sheet(frame: Any, sheet: Path, sent: Optional[Path]) -> Optional["pd.D
120
133
  return merged
121
134
 
122
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
+
123
161
  def _in_notebook() -> bool:
124
162
  """Whether this runs in a Jupyter kernel, whose front end can show the workspace in a cell"""
125
163
  try:
@@ -169,6 +169,44 @@ def test_in_a_notebook_the_workspace_shows_in_the_cell(monkeypatch: pytest.Monke
169
169
  assert "Finished." in printed
170
170
 
171
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
+
172
210
  def test_nothing_synced_returns_none() -> None:
173
211
  result = batchgrid.ui(open_browser=False, log=False, on_event=open_tab(lambda tab: tab.call("finish", {})))
174
212
  assert result is None
File without changes