google-colab-cli 0.5.8__tar.gz → 0.5.9__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.
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/PKG-INFO +1 -1
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/02_execution_and_interactive.md +1 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/04_automation_and_utility.md +4 -3
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/05_run_command.md +1 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/README.md +2 -0
- google_colab_cli-0.5.9/integration/repro_bundled_oauth/test.sh +93 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/auth.py +41 -1
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/cli.py +1 -1
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/execution.py +6 -1
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/run.py +8 -1
- google_colab_cli-0.5.9/src/colab_cli/oauth_config.json +11 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_auth.py +30 -2
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_auth_adc.py +5 -6
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_exec.py +23 -2
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_ipynb_exec.py +2 -2
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_run.py +25 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/.githooks/pre-commit +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/.gitignore +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/.pre-commit-config.yaml +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/.python-version +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/AGENTS.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/COLAB_SKILL.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/CONTRIBUTING.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/LICENSE +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/README.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/cloudbuild.yaml +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/01_session_management.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/03_file_management.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/3042ab12-2026-05-07.png +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/docs/demos.md +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_keep_alive/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_keep_alive_scope/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_piped_console/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_plot_redirection/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_run_command/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_variable_persistence/test.sh +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/pyproject.toml +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/auto_update.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/client.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/__init__.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/automation.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/files.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/session.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/commands/utility.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/common.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/console.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/contents.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/converter.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/history.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/repl.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/runtime.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/state.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/src/colab_cli/utils.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/conftest.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_automation.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_cli.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_cli_log.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_client.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_console.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_contents.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_history.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_keep_alive.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_log_export.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_pay.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_readme.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_repl.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_resolution_logic.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_runtime.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_state.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_streaming.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_update.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_url.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_utils.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_version.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/tests/test_whoami.py +0 -0
- {google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/uv.lock +0 -0
|
@@ -26,6 +26,7 @@ Execution involves sending Python code (or shell commands) to the Jupyter kernel
|
|
|
26
26
|
- If file path is local: Read content, send as code.
|
|
27
27
|
- If file path is remote: Execute `!python <path>`.
|
|
28
28
|
- **Multi-Modal Output**: Handle `display_data` messages (e.g., `image/png`, `text/html`). For the CLI, we'll save images to temporary files and print their paths, or if the terminal supports it (e.g., iTerm2), inline them.
|
|
29
|
+
- **Timeout Configuration**: Exposes a `--timeout` flag (default 10s) to allow long-running silent tasks (like model compilation or data downloading) to execute without being prematurely killed.
|
|
29
30
|
|
|
30
31
|
### 3. Console (`colab console`)
|
|
31
32
|
- **Implementation**: Connects directly to the backend terminal endpoint (`/colab/tty`) via WebSockets using `websocket-client`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
log:
|
|
3
3
|
2026-06-01: Enabled `colab update --install` self-update on macOS in addition to Linux. Refactored platform check logic to keep the implementation DRY and updated both tests and documentation. Also, on these platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
|
|
4
|
+
2026-05-29: Added default OAuth2 client config (`oauth_config.json`) as a bundled package resource and restored fallback loading logic in `get_credentials()`. The CLI now falls back to using these default credentials when no explicit local config is found. Added `integration/repro_bundled_oauth` integration test.
|
|
4
5
|
2026-05-27: Refactored `colab README` and `colab AGENT` to bundle `README.md` and `AGENTS.md` via Hatchling's `force-include` and read them using `importlib.resources` instead of `importlib.metadata`. `colab AGENT` now correctly prints `AGENTS.md`.
|
|
5
6
|
2026-05-27: Extended `colab update --install` to detect if the CLI was installed via `uv tool install` (by checking if `sys.executable` contains `/uv/`) and if so, use `uv tool install -U google-colab-cli` to upgrade.
|
|
6
7
|
2026-05-27: Updated auto-update upgrade hint to recommend `pip install --upgrade google-colab-cli` instead of `colab`, aligning with the PyPI package name.
|
|
@@ -24,9 +25,9 @@ backend, selected via the global `--auth=<provider>` flag:
|
|
|
24
25
|
|
|
25
26
|
1. **`oauth2`** (default): Standard public InstalledAppFlow via
|
|
26
27
|
`google-auth-oauthlib`. Opens a browser for consent, caches the refresh
|
|
27
|
-
token at `~/.config/colab-cli/token.json`.
|
|
28
|
-
config at `~/.colab-cli-oauth-config.json
|
|
29
|
-
|
|
28
|
+
token at `~/.config/colab-cli/token.json`. If no local config is provided
|
|
29
|
+
via `-c/--client-oauth-config` or found at `~/.colab-cli-oauth-config.json`,
|
|
30
|
+
it falls back to a bundled `oauth_config.json` containing default OAuth credentials.
|
|
30
31
|
2. **`adc`**: Application Default Credentials via `google.auth.default()`.
|
|
31
32
|
Honors the standard ADC discovery chain
|
|
32
33
|
(`GOOGLE_APPLICATION_CREDENTIALS`, `gcloud auth application-default
|
|
@@ -29,6 +29,7 @@ colab run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
|
|
|
29
29
|
| `--gpu` | str | None | Same set as `colab new --gpu` (T4, L4, G4, H100, A100). |
|
|
30
30
|
| `--tpu` | str | None | Same set as `colab new --tpu` (v5e1, v6e1). |
|
|
31
31
|
| `--keep` | bool | False | Do **not** stop the session after the script finishes. |
|
|
32
|
+
| `--timeout` | float | 10.0 | Timeout in seconds for code execution to prevent hanging on silent tasks. |
|
|
32
33
|
|
|
33
34
|
### Shebang usage
|
|
34
35
|
With `--keep` and `--gpu` baked into the shebang line, an entire one-file workload becomes:
|
|
@@ -16,6 +16,8 @@ End-to-end tests that run against a **live Colab backend** (unlike the mocked un
|
|
|
16
16
|
| `repro_keep_alive_scope/` | Slow soak test (~95s): runs the daemon long enough for one ping past the pre-flight, asserts no `keep_alive_error` events. |
|
|
17
17
|
| `repro_variable_persistence/` | Variables persist across `colab exec` calls in the same session. |
|
|
18
18
|
| `repro_piped_console/` | Fast smoke test (~5s including session creation): `echo cmd \| colab console -s s` runs the command and exits within 30s. Regression test for the 2026-05-07 EOF-handler fix. |
|
|
19
|
+
| `repro_bundled_oauth/` | Fast smoke test (~5s): verifies that the fallback OAuth configuration is loaded and starts the OAuth flow with the default client ID when local config is missing. |
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
## Running
|
|
21
23
|
```bash
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Copyright 2026 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Integration Test: Fallback OAuth Configuration Verification
|
|
17
|
+
#
|
|
18
|
+
# This test verifies that the CLI correctly falls back to using the bundled
|
|
19
|
+
# oauth_config.json containing the default client ID when no local config is present.
|
|
20
|
+
# It simulates a fresh user flow by temporarily hiding existing tokens/configs,
|
|
21
|
+
# starting the OAuth flow, and asserting that the printed authorization URL
|
|
22
|
+
# contains the correct Client ID.
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
set -e
|
|
26
|
+
|
|
27
|
+
TOKEN_PATH="$HOME/.config/colab-cli/token.json"
|
|
28
|
+
CONFIG_PATH="$HOME/.colab-cli-oauth-config.json"
|
|
29
|
+
BACKUP_SUFFIX=".backup.$(date +%s)"
|
|
30
|
+
|
|
31
|
+
TOKEN_BACKUP=""
|
|
32
|
+
CONFIG_BACKUP=""
|
|
33
|
+
|
|
34
|
+
cleanup() {
|
|
35
|
+
echo "[*] Cleaning up backups..."
|
|
36
|
+
if [ -n "$TOKEN_BACKUP" ] && [ -f "$TOKEN_BACKUP" ]; then
|
|
37
|
+
mv "$TOKEN_BACKUP" "$TOKEN_PATH"
|
|
38
|
+
echo "[*] Restored token.json"
|
|
39
|
+
fi
|
|
40
|
+
if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then
|
|
41
|
+
mv "$CONFIG_BACKUP" "$CONFIG_PATH"
|
|
42
|
+
echo "[*] Restored .colab-cli-oauth-config.json"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
trap cleanup EXIT
|
|
46
|
+
|
|
47
|
+
# 1. Back up existing configuration files if they exist
|
|
48
|
+
if [ -f "$TOKEN_PATH" ]; then
|
|
49
|
+
TOKEN_BACKUP="${TOKEN_PATH}${BACKUP_SUFFIX}"
|
|
50
|
+
mv "$TOKEN_PATH" "$TOKEN_BACKUP"
|
|
51
|
+
echo "[*] Temporarily backed up token.json to $TOKEN_BACKUP"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -f "$CONFIG_PATH" ]; then
|
|
55
|
+
CONFIG_BACKUP="${CONFIG_PATH}${BACKUP_SUFFIX}"
|
|
56
|
+
mv "$CONFIG_PATH" "$CONFIG_BACKUP"
|
|
57
|
+
echo "[*] Temporarily backed up .colab-cli-oauth-config.json to $CONFIG_BACKUP"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# 2. Run colab sessions command to trigger the OAuth flow
|
|
61
|
+
echo "[*] Running 'colab --auth=oauth2 sessions' (expecting to trigger browser flow)..."
|
|
62
|
+
# We expect the command to block waiting for authorization, so we run it with a timeout.
|
|
63
|
+
# We redirect output to a file so we can inspect it.
|
|
64
|
+
OUTPUT_LOG=$(mktemp)
|
|
65
|
+
set +e
|
|
66
|
+
PYTHONUNBUFFERED=1 timeout 5 uv run colab --auth=oauth2 sessions > "$OUTPUT_LOG" 2>&1
|
|
67
|
+
EXIT_CODE=$?
|
|
68
|
+
set -e
|
|
69
|
+
|
|
70
|
+
echo "[*] Command exited with code: $EXIT_CODE"
|
|
71
|
+
echo "----------------- CLI Output -----------------"
|
|
72
|
+
cat "$OUTPUT_LOG"
|
|
73
|
+
echo "----------------------------------------------"
|
|
74
|
+
|
|
75
|
+
# 3. Assertions
|
|
76
|
+
EXPECTED_CLIENT_ID="764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
|
|
77
|
+
|
|
78
|
+
# The command should have printed the authorization URL
|
|
79
|
+
if ! grep -q "Please visit this URL to authorize this application" "$OUTPUT_LOG"; then
|
|
80
|
+
echo "[FAILURE] OAuth prompt message not found in output."
|
|
81
|
+
rm -f "$OUTPUT_LOG"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# The URL should contain the correct client ID
|
|
86
|
+
if ! grep -q "client_id=$EXPECTED_CLIENT_ID" "$OUTPUT_LOG"; then
|
|
87
|
+
echo "[FAILURE] Authorization URL does not contain the expected client ID: $EXPECTED_CLIENT_ID"
|
|
88
|
+
rm -f "$OUTPUT_LOG"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
echo "[SUCCESS] Verified that CLI correctly initiated OAuth flow with the fallback Client ID: $EXPECTED_CLIENT_ID"
|
|
93
|
+
rm -f "$OUTPUT_LOG"
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
import enum
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
from importlib import resources
|
|
18
19
|
import os
|
|
19
20
|
import warnings
|
|
20
21
|
from typing import Optional
|
|
21
22
|
|
|
22
23
|
import google.auth
|
|
24
|
+
import typer
|
|
23
25
|
from google.auth.transport import requests
|
|
24
26
|
from google.auth.transport.requests import Request
|
|
25
27
|
from google.oauth2.credentials import Credentials
|
|
@@ -43,6 +45,7 @@ PUBLIC_SCOPES = [
|
|
|
43
45
|
"openid",
|
|
44
46
|
"https://www.googleapis.com/auth/userinfo.profile",
|
|
45
47
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
48
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
46
49
|
"https://www.googleapis.com/auth/colaboratory",
|
|
47
50
|
"https://www.googleapis.com/auth/drive.file",
|
|
48
51
|
]
|
|
@@ -60,9 +63,18 @@ def _get_google_auth_credentials(config_path: str) -> Credentials:
|
|
|
60
63
|
if os.path.exists(config_path):
|
|
61
64
|
with open(config_path, "r") as f:
|
|
62
65
|
client_config = json.load(f)
|
|
66
|
+
else:
|
|
67
|
+
# Last resort: try inlined config
|
|
68
|
+
try:
|
|
69
|
+
config_resource = resources.files("colab_cli").joinpath("oauth_config.json")
|
|
70
|
+
if config_resource.is_file():
|
|
71
|
+
client_config = json.loads(config_resource.read_text())
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Failed to load inlined config: {e}")
|
|
74
|
+
|
|
63
75
|
if not client_config:
|
|
64
76
|
raise FileNotFoundError(
|
|
65
|
-
f"Client OAuth config not found at {config_path}. "
|
|
77
|
+
f"Client OAuth config not found at {config_path} and no inlined config available. "
|
|
66
78
|
"Please provide a valid path via -c/--client-oauth-config."
|
|
67
79
|
)
|
|
68
80
|
|
|
@@ -137,6 +149,34 @@ def _get_adc_credentials() -> Credentials:
|
|
|
137
149
|
category=UserWarning,
|
|
138
150
|
)
|
|
139
151
|
creds, _ = google.auth.default(scopes=list(PUBLIC_SCOPES))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if not creds.valid:
|
|
155
|
+
from google.auth import compute_engine
|
|
156
|
+
|
|
157
|
+
if isinstance(creds, compute_engine.Credentials):
|
|
158
|
+
creds = None
|
|
159
|
+
else:
|
|
160
|
+
logger.warning("Failed to obtain valid ADC credentials.")
|
|
161
|
+
try:
|
|
162
|
+
logger.warning("Trying to refresh ADC credentials")
|
|
163
|
+
creds.refresh(Request())
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Failed to refresh token: {e}")
|
|
166
|
+
creds = None
|
|
167
|
+
|
|
168
|
+
if not creds:
|
|
169
|
+
typer.echo(
|
|
170
|
+
"No valid default credentials found. To authenticate, run:\n\n"
|
|
171
|
+
" gcloud auth application-default login \\\n"
|
|
172
|
+
" --scopes=openid,"
|
|
173
|
+
"https://www.googleapis.com/auth/cloud-platform,"
|
|
174
|
+
"https://www.googleapis.com/auth/userinfo.email,"
|
|
175
|
+
"https://www.googleapis.com/auth/colaboratory\n",
|
|
176
|
+
err=True,
|
|
177
|
+
)
|
|
178
|
+
exit(1)
|
|
179
|
+
|
|
140
180
|
# Some credential subclasses ignore the `scopes=` kwarg in `default()`
|
|
141
181
|
# (e.g. user creds), so re-apply via `with_scopes` when supported.
|
|
142
182
|
if getattr(creds, "requires_scopes", False):
|
|
@@ -108,6 +108,9 @@ def exec_command(
|
|
|
108
108
|
output_image: Annotated[
|
|
109
109
|
Optional[str], typer.Option("--output-image", help="Path to save plot")
|
|
110
110
|
] = None,
|
|
111
|
+
timeout: Annotated[
|
|
112
|
+
Optional[float], typer.Option("--timeout", help="Timeout in seconds for code execution")
|
|
113
|
+
] = 10.0,
|
|
111
114
|
):
|
|
112
115
|
"""Execute code in a session"""
|
|
113
116
|
from colab_cli.common import state
|
|
@@ -205,7 +208,9 @@ def exec_command(
|
|
|
205
208
|
state.store.add(s)
|
|
206
209
|
|
|
207
210
|
outputs = runtime.execute_code(
|
|
208
|
-
code,
|
|
211
|
+
code,
|
|
212
|
+
output_hook=lambda o: display_output(o, output_image),
|
|
213
|
+
timeout=timeout
|
|
209
214
|
)
|
|
210
215
|
if "cell" in block:
|
|
211
216
|
save_output(outputs, block["cell"])
|
|
@@ -233,6 +233,9 @@ def run_command(
|
|
|
233
233
|
),
|
|
234
234
|
),
|
|
235
235
|
] = False,
|
|
236
|
+
timeout: Annotated[
|
|
237
|
+
Optional[float], typer.Option("--timeout", help="Timeout in seconds for code execution")
|
|
238
|
+
] = 10.0,
|
|
236
239
|
):
|
|
237
240
|
"""Run a Python script on a fresh Colab VM, then release the VM
|
|
238
241
|
|
|
@@ -386,7 +389,11 @@ def run_command(
|
|
|
386
389
|
state.store.add(s)
|
|
387
390
|
|
|
388
391
|
try:
|
|
389
|
-
outputs = runtime.execute_code(
|
|
392
|
+
outputs = runtime.execute_code(
|
|
393
|
+
payload,
|
|
394
|
+
output_hook=_make_run_output_hook(),
|
|
395
|
+
timeout=timeout
|
|
396
|
+
)
|
|
390
397
|
except Exception:
|
|
391
398
|
# Genuine transport-level failure. Cleanup still happens via the
|
|
392
399
|
# outer finally; surface non-zero exit so callers/CI notice.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"installed": {
|
|
3
|
+
"client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
|
|
4
|
+
"project_id": "colab-cli",
|
|
5
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
6
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
7
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
8
|
+
"client_secret": "d-FL95Q19q7MQmFpd7hHD0Ty",
|
|
9
|
+
"redirect_uris": ["http://localhost"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -27,9 +27,11 @@ def mock_deps(mocker):
|
|
|
27
27
|
m_flow_cls = mocker.patch("colab_cli.auth.InstalledAppFlow")
|
|
28
28
|
m_request = mocker.patch("colab_cli.auth.Request")
|
|
29
29
|
m_session = mocker.patch("colab_cli.auth.requests.AuthorizedSession")
|
|
30
|
+
m_resources = mocker.patch("colab_cli.auth.resources")
|
|
30
31
|
|
|
31
|
-
# By default, pretend oauth config doesn't exist
|
|
32
|
+
# By default, pretend oauth config doesn't exist anywhere
|
|
32
33
|
m_exists.return_value = False
|
|
34
|
+
m_resources.files.return_value.joinpath.return_value.is_file.return_value = False
|
|
33
35
|
|
|
34
36
|
return {
|
|
35
37
|
"exists": m_exists,
|
|
@@ -38,11 +40,15 @@ def mock_deps(mocker):
|
|
|
38
40
|
"flow_cls": m_flow_cls,
|
|
39
41
|
"request": m_request,
|
|
40
42
|
"session": m_session,
|
|
43
|
+
"resources": m_resources,
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
def test_get_credentials_no_config(mock_deps):
|
|
45
|
-
with pytest.raises(
|
|
48
|
+
with pytest.raises(
|
|
49
|
+
FileNotFoundError,
|
|
50
|
+
match="Client OAuth config not found.*and no inlined config available",
|
|
51
|
+
):
|
|
46
52
|
get_credentials("missing_config.json", provider=AuthProvider.OAUTH2)
|
|
47
53
|
|
|
48
54
|
|
|
@@ -105,3 +111,25 @@ def test_get_credentials_no_token(mock_deps):
|
|
|
105
111
|
|
|
106
112
|
mock_deps["flow_cls"].from_client_config.assert_called_once()
|
|
107
113
|
mock_flow.run_local_server.assert_called_once()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_get_credentials_fallback_config(mock_deps):
|
|
117
|
+
# Setup: config_path doesn't exist, but fallback file does
|
|
118
|
+
mock_deps["exists"].return_value = False
|
|
119
|
+
m_file = mock_deps["resources"].files.return_value.joinpath.return_value
|
|
120
|
+
m_file.is_file.return_value = True
|
|
121
|
+
m_file.read_text.return_value = '{"installed":{"client_id":"fallback_id"}}'
|
|
122
|
+
|
|
123
|
+
# Valid creds in token
|
|
124
|
+
mock_deps["exists"].side_effect = lambda path: path == TOKEN_CONFIG_PATH
|
|
125
|
+
mock_creds = MagicMock()
|
|
126
|
+
mock_creds.valid = True
|
|
127
|
+
mock_deps["creds_cls"].from_authorized_user_file.return_value = mock_creds
|
|
128
|
+
|
|
129
|
+
res = get_credentials("missing_config.json", provider=AuthProvider.OAUTH2)
|
|
130
|
+
|
|
131
|
+
mock_deps["resources"].files.assert_called_once_with("colab_cli")
|
|
132
|
+
m_file.is_file.assert_called_once()
|
|
133
|
+
m_file.read_text.assert_called_once()
|
|
134
|
+
mock_deps["creds_cls"].from_authorized_user_file.assert_called_once()
|
|
135
|
+
assert res == mock_deps["session"].return_value
|
|
@@ -70,10 +70,8 @@ def test_get_credentials_adc_does_not_invoke_other_providers(mocker):
|
|
|
70
70
|
mock_flow.from_client_config.assert_not_called()
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def
|
|
74
|
-
"""
|
|
75
|
-
`colaboratory` scope. ADC must request it via google.auth.default().
|
|
76
|
-
"""
|
|
73
|
+
def test_get_credentials_adc_requests_required_scopes(mocker):
|
|
74
|
+
"""ADC must request required scopes via google.auth.default()."""
|
|
77
75
|
mock_creds = MagicMock()
|
|
78
76
|
# Pretend creds don't need re-scoping (e.g., user creds from gcloud).
|
|
79
77
|
mock_creds.requires_scopes = False
|
|
@@ -87,12 +85,13 @@ def test_get_credentials_adc_requests_colaboratory_scope(mocker):
|
|
|
87
85
|
scopes = mock_default.call_args.kwargs.get("scopes")
|
|
88
86
|
assert scopes is not None, "google.auth.default() must be called with scopes="
|
|
89
87
|
assert "https://www.googleapis.com/auth/colaboratory" in scopes
|
|
88
|
+
assert "https://www.googleapis.com/auth/cloud-platform" in scopes
|
|
90
89
|
assert "https://www.googleapis.com/auth/userinfo.email" in scopes
|
|
91
90
|
|
|
92
91
|
|
|
93
92
|
def test_get_credentials_adc_reapplies_scopes_for_scopable_creds(mocker):
|
|
94
93
|
"""For credential subclasses that support `with_scopes` (service accounts,
|
|
95
|
-
GCE/GKE, etc.), we must call it so the
|
|
94
|
+
GCE/GKE, etc.), we must call it so the scopes stick even if
|
|
96
95
|
google.auth.default() ignored the kwarg.
|
|
97
96
|
"""
|
|
98
97
|
rescoped = MagicMock(name="rescoped_creds")
|
|
@@ -106,7 +105,7 @@ def test_get_credentials_adc_reapplies_scopes_for_scopable_creds(mocker):
|
|
|
106
105
|
|
|
107
106
|
mock_creds.with_scopes.assert_called_once()
|
|
108
107
|
applied_scopes = mock_creds.with_scopes.call_args.args[0]
|
|
109
|
-
assert "https://www.googleapis.com/auth/
|
|
108
|
+
assert "https://www.googleapis.com/auth/cloud-platform" in applied_scopes
|
|
110
109
|
# The session is built from the *rescoped* creds, not the original.
|
|
111
110
|
mock_session_cls.assert_called_once_with(rescoped)
|
|
112
111
|
|
|
@@ -58,7 +58,7 @@ def test_cli_exec_file(mock_store, mock_runtime_class, mock_common_state, tmp_pa
|
|
|
58
58
|
mock_runtime.execute_code.assert_any_call(
|
|
59
59
|
"import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
|
|
60
60
|
)
|
|
61
|
-
mock_runtime.execute_code.assert_any_call("print('hello')", output_hook=ANY)
|
|
61
|
+
mock_runtime.execute_code.assert_any_call("print('hello')", output_hook=ANY, timeout=10.0)
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def test_cli_exec_stdin(mock_store, mock_runtime_class, mock_common_state):
|
|
@@ -80,7 +80,7 @@ def test_cli_exec_stdin(mock_store, mock_runtime_class, mock_common_state):
|
|
|
80
80
|
assert mock_session.last_execution[1] is None
|
|
81
81
|
assert mock_session.last_execution[2] is not None
|
|
82
82
|
mock_store.add.assert_called_with(mock_session)
|
|
83
|
-
mock_runtime.execute_code.assert_any_call("print(42)", output_hook=ANY)
|
|
83
|
+
mock_runtime.execute_code.assert_any_call("print(42)", output_hook=ANY, timeout=10.0)
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def test_cli_exec_not_found(mock_common_state):
|
|
@@ -176,3 +176,24 @@ def test_cli_exec_lost_session_prunes(
|
|
|
176
176
|
assert result.exit_code == 1
|
|
177
177
|
assert "appears to be lost" in result.output
|
|
178
178
|
mock_common_state.prune_session.assert_called_once_with("lost-sess")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_cli_exec_timeout(mock_store, mock_runtime_class, mock_common_state, tmp_path):
|
|
182
|
+
mock_session = MagicMock()
|
|
183
|
+
mock_session.url = "http://url"
|
|
184
|
+
mock_session.token = "token123"
|
|
185
|
+
mock_session.name = "s1"
|
|
186
|
+
mock_session.kernel_id = None
|
|
187
|
+
mock_session.session_id = None
|
|
188
|
+
mock_store.get.return_value = mock_session
|
|
189
|
+
|
|
190
|
+
mock_common_state.resolve_session.return_value = "s1"
|
|
191
|
+
mock_runtime = mock_runtime_class.return_value
|
|
192
|
+
mock_runtime.execute_code.return_value = [{"text": "hello\n"}]
|
|
193
|
+
|
|
194
|
+
script = tmp_path / "script.py"
|
|
195
|
+
script.write_text("print('hello')")
|
|
196
|
+
|
|
197
|
+
result = runner.invoke(app, ["exec", "-s", "s1", "-f", str(script), "--timeout", "3600"])
|
|
198
|
+
assert result.exit_code == 0
|
|
199
|
+
mock_runtime.execute_code.assert_any_call("print('hello')", output_hook=ANY, timeout=3600.0)
|
|
@@ -98,10 +98,10 @@ class TestIpynbExec(unittest.TestCase):
|
|
|
98
98
|
"os.chdir", mock_runtime.execute_code.call_args_list[0].args[0]
|
|
99
99
|
)
|
|
100
100
|
mock_runtime.execute_code.assert_any_call(
|
|
101
|
-
"print('cell 1')", output_hook=ANY
|
|
101
|
+
"print('cell 1')", output_hook=ANY, timeout=10.0
|
|
102
102
|
)
|
|
103
103
|
mock_runtime.execute_code.assert_any_call(
|
|
104
|
-
"print('cell 2')", output_hook=ANY
|
|
104
|
+
"print('cell 2')", output_hook=ANY, timeout=10.0
|
|
105
105
|
)
|
|
106
106
|
|
|
107
107
|
@patch("colab_cli.commands.execution.ColabRuntime")
|
|
@@ -504,3 +504,28 @@ def test_run_prelude_suppresses_ipython_exit_warning(
|
|
|
504
504
|
# Look for the warnings filter targeting IPython's exit-warning text.
|
|
505
505
|
assert "warnings.filterwarnings" in body
|
|
506
506
|
assert "To exit: use" in body
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def test_run_with_timeout_flag(
|
|
510
|
+
mock_client,
|
|
511
|
+
mock_store,
|
|
512
|
+
mock_runtime_class,
|
|
513
|
+
mock_spawn_keep_alive,
|
|
514
|
+
assign_response,
|
|
515
|
+
script_path,
|
|
516
|
+
):
|
|
517
|
+
"""`colab run --timeout 3600 script.py` must pass timeout down to the runtime."""
|
|
518
|
+
mock_client.assign.return_value = assign_response
|
|
519
|
+
mock_runtime = mock_runtime_class.return_value
|
|
520
|
+
mock_runtime.execute_code.return_value = []
|
|
521
|
+
|
|
522
|
+
persisted = {}
|
|
523
|
+
mock_store.add.side_effect = lambda s: persisted.setdefault("s", s)
|
|
524
|
+
mock_store.get.side_effect = lambda name: persisted.get("s")
|
|
525
|
+
|
|
526
|
+
result = runner.invoke(app, ["run", "--timeout", "3600", str(script_path)])
|
|
527
|
+
assert result.exit_code == 0, result.output
|
|
528
|
+
|
|
529
|
+
code_calls = mock_runtime.execute_code.call_args_list
|
|
530
|
+
body_call = next(c for c in code_calls if "hello from script" in c.args[0])
|
|
531
|
+
assert body_call.kwargs.get("timeout") == 3600.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_keep_alive_scope/test.sh
RENAMED
|
File without changes
|
|
File without changes
|
{google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_plot_redirection/test.sh
RENAMED
|
File without changes
|
|
File without changes
|
{google_colab_cli-0.5.8 → google_colab_cli-0.5.9}/integration/repro_variable_persistence/test.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|