google-colab-cli 0.5.4__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.
- colab_cli/auth.py +170 -0
- colab_cli/auto_update.py +226 -0
- colab_cli/cli.py +151 -0
- colab_cli/client.py +324 -0
- colab_cli/commands/__init__.py +14 -0
- colab_cli/commands/automation.py +265 -0
- colab_cli/commands/execution.py +356 -0
- colab_cli/commands/files.py +204 -0
- colab_cli/commands/run.py +471 -0
- colab_cli/commands/session.py +508 -0
- colab_cli/commands/utility.py +365 -0
- colab_cli/common.py +185 -0
- colab_cli/console.py +172 -0
- colab_cli/contents.py +93 -0
- colab_cli/converter.py +184 -0
- colab_cli/history.py +65 -0
- colab_cli/repl.py +173 -0
- colab_cli/runtime.py +262 -0
- colab_cli/state.py +152 -0
- colab_cli/utils.py +85 -0
- google_colab_cli-0.5.4.dist-info/METADATA +117 -0
- google_colab_cli-0.5.4.dist-info/RECORD +25 -0
- google_colab_cli-0.5.4.dist-info/WHEEL +4 -0
- google_colab_cli-0.5.4.dist-info/entry_points.txt +2 -0
- google_colab_cli-0.5.4.dist-info/licenses/LICENSE +202 -0
colab_cli/auth.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import enum
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import warnings
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
import google.auth
|
|
23
|
+
from google.auth.transport import requests
|
|
24
|
+
from google.auth.transport.requests import Request
|
|
25
|
+
from google.oauth2.credentials import Credentials
|
|
26
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthProvider(str, enum.Enum):
|
|
32
|
+
"""Authentication strategy for talking to the Colab backend.
|
|
33
|
+
|
|
34
|
+
Values are the lowercase strings accepted by the global ``--auth`` flag.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
OAUTH2 = "oauth2"
|
|
38
|
+
ADC = "adc"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Standard Scopes for Colab and Drive (Public Auth)
|
|
42
|
+
PUBLIC_SCOPES = [
|
|
43
|
+
"openid",
|
|
44
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
45
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
46
|
+
"https://www.googleapis.com/auth/colaboratory",
|
|
47
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
TOKEN_CONFIG_PATH = os.path.expanduser("~/.config/colab-cli/token.json")
|
|
52
|
+
OAUTH_SERVER_PORT = 8200
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_google_auth_credentials(config_path: str) -> Credentials:
|
|
56
|
+
"""
|
|
57
|
+
Retrieves credentials using standard public OAuth2 flow.
|
|
58
|
+
"""
|
|
59
|
+
client_config = None
|
|
60
|
+
if os.path.exists(config_path):
|
|
61
|
+
with open(config_path, "r") as f:
|
|
62
|
+
client_config = json.load(f)
|
|
63
|
+
if not client_config:
|
|
64
|
+
raise FileNotFoundError(
|
|
65
|
+
f"Client OAuth config not found at {config_path}. "
|
|
66
|
+
"Please provide a valid path via -c/--client-oauth-config."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
creds = None
|
|
70
|
+
|
|
71
|
+
# Ensure config directory exists for the token file
|
|
72
|
+
os.makedirs(os.path.dirname(TOKEN_CONFIG_PATH), exist_ok=True)
|
|
73
|
+
|
|
74
|
+
if os.path.exists(TOKEN_CONFIG_PATH):
|
|
75
|
+
try:
|
|
76
|
+
creds = Credentials.from_authorized_user_file(
|
|
77
|
+
TOKEN_CONFIG_PATH, PUBLIC_SCOPES
|
|
78
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.warning(f"Failed to load token from {TOKEN_CONFIG_PATH}: {e}")
|
|
81
|
+
|
|
82
|
+
if not creds or not creds.valid:
|
|
83
|
+
if creds and creds.expired and creds.refresh_token:
|
|
84
|
+
try:
|
|
85
|
+
creds.refresh(Request())
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Failed to refresh token: {e}")
|
|
88
|
+
creds = None
|
|
89
|
+
|
|
90
|
+
if not creds:
|
|
91
|
+
flow = InstalledAppFlow.from_client_config(client_config, PUBLIC_SCOPES)
|
|
92
|
+
creds = flow.run_local_server(port=OAUTH_SERVER_PORT)
|
|
93
|
+
|
|
94
|
+
# Save the credentials for the next run
|
|
95
|
+
try:
|
|
96
|
+
with open(TOKEN_CONFIG_PATH, "w") as token_file:
|
|
97
|
+
token_file.write(creds.to_json())
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to save token to {TOKEN_CONFIG_PATH}: {e}")
|
|
100
|
+
|
|
101
|
+
return creds
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_adc_credentials() -> Credentials:
|
|
105
|
+
"""Retrieves credentials using Google Application Default Credentials.
|
|
106
|
+
|
|
107
|
+
Honors the standard ADC discovery chain (``GOOGLE_APPLICATION_CREDENTIALS``,
|
|
108
|
+
``gcloud auth application-default login``, GCE/GKE metadata server, etc.).
|
|
109
|
+
|
|
110
|
+
The RuntimeService at colab.pa.googleapis.com requires the
|
|
111
|
+
`colaboratory` scope (otherwise keep-alive returns 403 SCOPE_NOT_PERMITTED).
|
|
112
|
+
Most ADC credential types (service accounts, GCE/GKE, impersonated)
|
|
113
|
+
support `with_scopes`; user credentials minted by
|
|
114
|
+
`gcloud auth application-default login` do not. For the latter, the user
|
|
115
|
+
must re-run `gcloud auth application-default login` with
|
|
116
|
+
`--scopes=openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/colaboratory`
|
|
117
|
+
(`openid` and `cloud-platform` are required by `gcloud` itself; `userinfo.email`
|
|
118
|
+
is required by the session backend; `colaboratory` is required by this RPC).
|
|
119
|
+
"""
|
|
120
|
+
# `google.auth._default` emits a UserWarning when ADC user credentials
|
|
121
|
+
# don't have a quota project pinned ("Your application has authenticated
|
|
122
|
+
# using end user credentials from Google Cloud SDK without a quota
|
|
123
|
+
# project. You might receive a 'quota exceeded' or 'API not enabled'
|
|
124
|
+
# error.").
|
|
125
|
+
#
|
|
126
|
+
# That heuristic does not apply to this CLI: every call we make to
|
|
127
|
+
# `colab.pa.googleapis.com` carries `X-Goog-User-Project: 1014160490159`
|
|
128
|
+
# (Colab's project id) — see AGENTS.md item 18 — so the user's
|
|
129
|
+
# quota-project setting is irrelevant. The warning shows up on every
|
|
130
|
+
# single `colab` invocation under ADC, which is pure noise. Filter it,
|
|
131
|
+
# but keep the scope as tight as possible: only this exact message,
|
|
132
|
+
# only during this one call.
|
|
133
|
+
with warnings.catch_warnings():
|
|
134
|
+
warnings.filterwarnings(
|
|
135
|
+
"ignore",
|
|
136
|
+
message=r"Your application has authenticated using end user credentials.*",
|
|
137
|
+
category=UserWarning,
|
|
138
|
+
)
|
|
139
|
+
creds, _ = google.auth.default(scopes=list(PUBLIC_SCOPES))
|
|
140
|
+
# Some credential subclasses ignore the `scopes=` kwarg in `default()`
|
|
141
|
+
# (e.g. user creds), so re-apply via `with_scopes` when supported.
|
|
142
|
+
if getattr(creds, "requires_scopes", False):
|
|
143
|
+
try:
|
|
144
|
+
creds = creds.with_scopes(list(PUBLIC_SCOPES))
|
|
145
|
+
except Exception as e: # NotImplementedError for non-scopable creds.
|
|
146
|
+
logger.debug(f"Could not augment ADC scopes via with_scopes: {e}")
|
|
147
|
+
return creds
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_credentials(
|
|
151
|
+
config_path: Optional[str] = None,
|
|
152
|
+
provider: AuthProvider = AuthProvider.OAUTH2,
|
|
153
|
+
) -> requests.AuthorizedSession:
|
|
154
|
+
"""Unified entry point for retrieving an authorized session.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
config_path: Path to the OAuth2 client config JSON. Only consulted when
|
|
158
|
+
``provider`` is ``OAUTH2``.
|
|
159
|
+
provider: Which authentication strategy to use.
|
|
160
|
+
"""
|
|
161
|
+
if provider == AuthProvider.OAUTH2:
|
|
162
|
+
if not config_path:
|
|
163
|
+
config_path = os.path.expanduser("~/.colab-cli-oauth-config.json")
|
|
164
|
+
creds = _get_google_auth_credentials(config_path)
|
|
165
|
+
elif provider == AuthProvider.ADC:
|
|
166
|
+
creds = _get_adc_credentials()
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError(f"Unknown auth provider: {provider!r}")
|
|
169
|
+
|
|
170
|
+
return requests.AuthorizedSession(creds)
|
colab_cli/auto_update.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Auto-update subsystem.
|
|
16
|
+
|
|
17
|
+
Owns version detection, the PyPI-style update probe, the on-disk
|
|
18
|
+
``latest_version`` cache, and the upgrade-banner UX. The CLI's global
|
|
19
|
+
callback (``cli.py``) calls ``check_for_updates`` once per day and
|
|
20
|
+
``maybe_show_cached_banner`` on every other invocation; the
|
|
21
|
+
``colab update`` Typer command (``commands/utility.py``) delegates to
|
|
22
|
+
``check_for_updates``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import subprocess
|
|
27
|
+
import urllib.request
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from importlib.metadata import PackageNotFoundError, version as installed_version
|
|
30
|
+
from packaging.version import InvalidVersion, Version
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
import typer
|
|
34
|
+
|
|
35
|
+
from colab_cli.common import state
|
|
36
|
+
from colab_cli.state import Settings
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------- Version detection -------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_app_version() -> str:
|
|
43
|
+
"""Return the installed package version, falling back to the git short hash."""
|
|
44
|
+
try:
|
|
45
|
+
return installed_version("colab")
|
|
46
|
+
except (PackageNotFoundError, InvalidVersion):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return subprocess.check_output(
|
|
51
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
52
|
+
stderr=subprocess.DEVNULL,
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
).strip()
|
|
55
|
+
except Exception:
|
|
56
|
+
return "unknown"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------- Source fetchers ---------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_version(payload: Optional[dict]) -> Optional[str]:
|
|
63
|
+
"""Returns ``info.version`` from a PyPI-style payload, or None."""
|
|
64
|
+
return (payload or {}).get("info", {}).get("version")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fetch_pypi(url: str, quiet: bool) -> Optional[dict]:
|
|
68
|
+
"""Fetches and parses the PyPI-style JSON document at ``url``."""
|
|
69
|
+
try:
|
|
70
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
71
|
+
return json.loads(response.read().decode("utf-8"))
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if not quiet:
|
|
74
|
+
typer.echo(f"[colab] Warning: Failed to fetch update info: {e}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------- Version comparison ------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_newer(candidate: Optional[str], current: str) -> bool:
|
|
82
|
+
"""True when ``candidate`` strictly exceeds ``current`` (PEP 440)."""
|
|
83
|
+
if not candidate:
|
|
84
|
+
return False
|
|
85
|
+
try:
|
|
86
|
+
return Version(candidate) > Version(current)
|
|
87
|
+
except InvalidVersion:
|
|
88
|
+
return candidate != current
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------- UX ----------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def announce_upgrade(
|
|
95
|
+
latest: str,
|
|
96
|
+
current: str,
|
|
97
|
+
install_cmd: str,
|
|
98
|
+
*,
|
|
99
|
+
show_disable_hint: bool = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Print the upgrade banner.
|
|
102
|
+
|
|
103
|
+
``show_disable_hint`` controls whether the trailing line that explains
|
|
104
|
+
how to silence the auto-check is included. It is only added when the
|
|
105
|
+
banner is shown unsolicited (the daily background fetch and the cached
|
|
106
|
+
banner on subsequent invocations); explicit ``colab update`` calls
|
|
107
|
+
omit it because the user already opted in to seeing the result.
|
|
108
|
+
"""
|
|
109
|
+
typer.echo(
|
|
110
|
+
f"\n[colab] A new version of Colab CLI is available: {latest} (current: {current})"
|
|
111
|
+
)
|
|
112
|
+
typer.echo(f"[colab] Run '{install_cmd}' to update.")
|
|
113
|
+
if show_disable_hint:
|
|
114
|
+
typer.echo(
|
|
115
|
+
"[colab] To silence this check, set "
|
|
116
|
+
'"enable_update_check": false in '
|
|
117
|
+
"~/.config/colab-cli/settings.json"
|
|
118
|
+
)
|
|
119
|
+
typer.echo("")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------- Orchestration -----------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_for_updates(quiet: bool = False) -> None:
|
|
126
|
+
"""Check PyPI for updates and print a message if a new version is available.
|
|
127
|
+
|
|
128
|
+
The disable-hint is appended to the banner only when ``quiet`` is True
|
|
129
|
+
(the daily background fetch); explicit ``colab update`` invocations
|
|
130
|
+
(``quiet=False``) omit it because the user asked for the check.
|
|
131
|
+
"""
|
|
132
|
+
settings = state.settings_store.load()
|
|
133
|
+
current = get_app_version()
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
pypi = _fetch_pypi(settings.update_url, quiet)
|
|
137
|
+
pypi_v = _parse_version(pypi)
|
|
138
|
+
|
|
139
|
+
if _is_newer(pypi_v, current):
|
|
140
|
+
announce_upgrade(
|
|
141
|
+
pypi_v,
|
|
142
|
+
current,
|
|
143
|
+
"pip install --upgrade colab",
|
|
144
|
+
show_disable_hint=quiet,
|
|
145
|
+
)
|
|
146
|
+
elif not quiet:
|
|
147
|
+
suffix = f", latest: {pypi_v}" if pypi_v else ""
|
|
148
|
+
typer.echo(f"[colab] Colab CLI is up to date (version: {current}{suffix}).")
|
|
149
|
+
|
|
150
|
+
# Cache the highest observed version; never downgrade.
|
|
151
|
+
cached = settings.latest_version or "0"
|
|
152
|
+
if _is_newer(pypi_v, cached):
|
|
153
|
+
settings.latest_version = pypi_v
|
|
154
|
+
|
|
155
|
+
settings.last_check = datetime.now(timezone.utc)
|
|
156
|
+
state.settings_store.save(settings)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
if not quiet:
|
|
160
|
+
typer.echo(f"[colab] Failed to check for updates: {e}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------- Background hooks (called from cli.py) -----------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _is_throttled(settings: Settings, *, now: Optional[datetime] = None) -> bool:
|
|
167
|
+
"""True if the once-per-day fetch should be skipped."""
|
|
168
|
+
if settings.last_check is None:
|
|
169
|
+
return False
|
|
170
|
+
now = now or datetime.now(timezone.utc)
|
|
171
|
+
return (now - settings.last_check).days < 1
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def maybe_show_cached_banner(settings: Settings) -> None:
|
|
175
|
+
"""Print the cached upgrade banner if the cache reports a newer version.
|
|
176
|
+
|
|
177
|
+
Called from the global CLI callback when the daily fetch is throttled.
|
|
178
|
+
The banner uses a generic ``colab update`` install hint because the
|
|
179
|
+
cache does not record which source supplied the version; the disable
|
|
180
|
+
hint is shown because this is unsolicited output.
|
|
181
|
+
"""
|
|
182
|
+
if not settings.latest_version:
|
|
183
|
+
return
|
|
184
|
+
current = get_app_version()
|
|
185
|
+
if not _is_newer(settings.latest_version, current):
|
|
186
|
+
return
|
|
187
|
+
announce_upgrade(
|
|
188
|
+
settings.latest_version,
|
|
189
|
+
current,
|
|
190
|
+
"colab update",
|
|
191
|
+
show_disable_hint=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_background_check() -> None:
|
|
196
|
+
"""Entry point for the global CLI callback.
|
|
197
|
+
|
|
198
|
+
Performs either the daily fetch (which writes the cache) or, if
|
|
199
|
+
throttled, surfaces the cached banner. Honors the
|
|
200
|
+
``enable_update_check`` master switch.
|
|
201
|
+
"""
|
|
202
|
+
settings = state.settings_store.load()
|
|
203
|
+
if not settings.enable_update_check:
|
|
204
|
+
return
|
|
205
|
+
if _is_throttled(settings):
|
|
206
|
+
maybe_show_cached_banner(settings)
|
|
207
|
+
else:
|
|
208
|
+
check_for_updates(quiet=True)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------- Self-install ------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# PyPI distribution name (different from the importable package name `colab`).
|
|
215
|
+
PYPI_PACKAGE_NAME = "google-colab-cli"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def self_install() -> None:
|
|
219
|
+
"""Run ``pip install -U <PYPI_PACKAGE_NAME>`` to upgrade the CLI in place."""
|
|
220
|
+
import sys
|
|
221
|
+
|
|
222
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", PYPI_PACKAGE_NAME]
|
|
223
|
+
typer.echo(f"[colab] Running: {' '.join(cmd)}")
|
|
224
|
+
result = subprocess.run(cmd)
|
|
225
|
+
if result.returncode != 0:
|
|
226
|
+
raise typer.Exit(code=result.returncode)
|
colab_cli/cli.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
import typer
|
|
20
|
+
from typer.core import TyperGroup
|
|
21
|
+
from typing_extensions import Annotated
|
|
22
|
+
|
|
23
|
+
from colab_cli import auto_update
|
|
24
|
+
from colab_cli.auth import AuthProvider
|
|
25
|
+
from colab_cli.common import state, setup_logging
|
|
26
|
+
from colab_cli.commands import session, execution, files, automation, run, utility
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AlphabeticalGroup(TyperGroup):
|
|
30
|
+
"""A `TyperGroup` that lists subcommands alphabetically in `--help` output.
|
|
31
|
+
|
|
32
|
+
Subcommands are registered in functional groups (session, execution, files,
|
|
33
|
+
automation, utility), but users discovering the CLI via `colab --help` /
|
|
34
|
+
`colab help` benefit from a deterministic, alphabetical listing.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
38
|
+
return sorted(super().list_commands(ctx))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
app = typer.Typer(
|
|
42
|
+
help="Colab CLI",
|
|
43
|
+
no_args_is_help=True,
|
|
44
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
45
|
+
cls=AlphabeticalGroup,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.callback()
|
|
50
|
+
def callback(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
client_oauth_config: Annotated[
|
|
53
|
+
str,
|
|
54
|
+
typer.Option(
|
|
55
|
+
"-c", "--client-oauth-config", help="Path to client OAuth config JSON file"
|
|
56
|
+
),
|
|
57
|
+
] = os.path.expanduser("~/.colab-cli-oauth-config.json"),
|
|
58
|
+
config: Annotated[
|
|
59
|
+
Optional[str],
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--config",
|
|
62
|
+
help="Path to session state file (~/.config/colab-cli/sessions.json)",
|
|
63
|
+
),
|
|
64
|
+
] = None,
|
|
65
|
+
logtostderr: Annotated[
|
|
66
|
+
bool, typer.Option("--logtostderr", help="Log all output to stderr")
|
|
67
|
+
] = False,
|
|
68
|
+
auth: Annotated[
|
|
69
|
+
AuthProvider,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--auth",
|
|
72
|
+
help=(
|
|
73
|
+
"Authentication strategy to use: 'oauth2' (public InstalledAppFlow),"
|
|
74
|
+
" or 'adc' (Application Default Credentials)."
|
|
75
|
+
),
|
|
76
|
+
case_sensitive=False,
|
|
77
|
+
),
|
|
78
|
+
] = AuthProvider.ADC,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Colab CLI global configuration.
|
|
82
|
+
"""
|
|
83
|
+
state.client_oauth_config = client_oauth_config
|
|
84
|
+
state.config_path = config
|
|
85
|
+
state.logtostderr = logtostderr
|
|
86
|
+
state.auth_provider = auth
|
|
87
|
+
setup_logging(logtostderr)
|
|
88
|
+
|
|
89
|
+
# Daily fetch + cached banner on every invocation.
|
|
90
|
+
#
|
|
91
|
+
# Suppress the banner for short-lived informational subcommands so their
|
|
92
|
+
# output stays clean and machine-parseable:
|
|
93
|
+
# - `update`: runs its own check + announce; would duplicate the banner.
|
|
94
|
+
# - `version`, `log`, `pay`, `help`, `url`: pure-display commands whose
|
|
95
|
+
# output users routinely pipe / scrape (e.g. `colab url -s s1 | xclip`);
|
|
96
|
+
# a stochastic upgrade banner injected once a day would corrupt those
|
|
97
|
+
# pipelines.
|
|
98
|
+
# - `whoami`: developer-only debugging tool; banner would obscure the
|
|
99
|
+
# auth/scope info the user invoked it to see.
|
|
100
|
+
_AUTO_UPDATE_SUPPRESSED = {
|
|
101
|
+
"update",
|
|
102
|
+
"version",
|
|
103
|
+
"log",
|
|
104
|
+
"pay",
|
|
105
|
+
"help",
|
|
106
|
+
"url",
|
|
107
|
+
"whoami",
|
|
108
|
+
}
|
|
109
|
+
if ctx.invoked_subcommand not in _AUTO_UPDATE_SUPPRESSED:
|
|
110
|
+
auto_update.run_background_check()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command(name="help")
|
|
114
|
+
def help_command(
|
|
115
|
+
ctx: typer.Context,
|
|
116
|
+
command: Annotated[
|
|
117
|
+
Optional[str], typer.Argument(help="Command to show help for")
|
|
118
|
+
] = None,
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
Show help for a command.
|
|
122
|
+
"""
|
|
123
|
+
if not command:
|
|
124
|
+
typer.echo(ctx.parent.get_help())
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
group = ctx.parent.command
|
|
128
|
+
cmd = group.get_command(ctx, command)
|
|
129
|
+
if cmd is None:
|
|
130
|
+
typer.echo(f"No such command '{command}'.", err=True)
|
|
131
|
+
raise typer.Exit(code=2)
|
|
132
|
+
|
|
133
|
+
with click.Context(cmd, info_name=command, parent=ctx.parent) as cmd_ctx:
|
|
134
|
+
typer.echo(cmd.get_help(cmd_ctx))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Register subcommands
|
|
138
|
+
session.register(app)
|
|
139
|
+
execution.register(app)
|
|
140
|
+
files.register(app)
|
|
141
|
+
automation.register(app)
|
|
142
|
+
run.register(app)
|
|
143
|
+
utility.register(app)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
app()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|