src-py-lib 0.1.0__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.
- src_py_lib/__init__.py +170 -0
- src_py_lib/clients/__init__.py +3 -0
- src_py_lib/clients/github.py +157 -0
- src_py_lib/clients/google_sheets.py +131 -0
- src_py_lib/clients/graphql.py +476 -0
- src_py_lib/clients/linear.py +101 -0
- src_py_lib/clients/one_password.py +95 -0
- src_py_lib/clients/slack.py +146 -0
- src_py_lib/clients/sourcegraph.py +127 -0
- src_py_lib/py.typed +0 -0
- src_py_lib/utils/__init__.py +3 -0
- src_py_lib/utils/config.py +603 -0
- src_py_lib/utils/http.py +279 -0
- src_py_lib/utils/json_cache.py +42 -0
- src_py_lib/utils/json_types.py +54 -0
- src_py_lib/utils/logging.py +950 -0
- src_py_lib/utils/tsv.py +95 -0
- src_py_lib-0.1.0.dist-info/METADATA +163 -0
- src_py_lib-0.1.0.dist-info/RECORD +21 -0
- src_py_lib-0.1.0.dist-info/WHEEL +4 -0
- src_py_lib-0.1.0.dist-info/licenses/LICENSE +21 -0
src_py_lib/__init__.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Public interface for src-py-lib consumers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from contextlib import AbstractContextManager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from src_py_lib.clients.github import GitHubClient, PullRequest, gh_cli_token, pr_ref_from_url
|
|
12
|
+
from src_py_lib.clients.google_sheets import (
|
|
13
|
+
GoogleSheetsClient,
|
|
14
|
+
GoogleSheetsError,
|
|
15
|
+
gcloud_adc_access_token,
|
|
16
|
+
quota_project_from_adc,
|
|
17
|
+
)
|
|
18
|
+
from src_py_lib.clients.graphql import (
|
|
19
|
+
GraphQLClient,
|
|
20
|
+
GraphQLError,
|
|
21
|
+
aliased_batched_query,
|
|
22
|
+
introspect_schema,
|
|
23
|
+
stream_connection_nodes,
|
|
24
|
+
)
|
|
25
|
+
from src_py_lib.clients.linear import (
|
|
26
|
+
LinearClient,
|
|
27
|
+
LinearClientConfig,
|
|
28
|
+
linear_client_from_config,
|
|
29
|
+
)
|
|
30
|
+
from src_py_lib.clients.slack import (
|
|
31
|
+
SlackClient,
|
|
32
|
+
SlackClientConfig,
|
|
33
|
+
SlackError,
|
|
34
|
+
SlackPacer,
|
|
35
|
+
slack_client_from_config,
|
|
36
|
+
)
|
|
37
|
+
from src_py_lib.clients.sourcegraph import (
|
|
38
|
+
SourcegraphClient,
|
|
39
|
+
SourcegraphClientConfig,
|
|
40
|
+
normalize_sourcegraph_endpoint,
|
|
41
|
+
sourcegraph_client_from_config,
|
|
42
|
+
)
|
|
43
|
+
from src_py_lib.utils.config import (
|
|
44
|
+
Config,
|
|
45
|
+
ConfigError,
|
|
46
|
+
config_field,
|
|
47
|
+
config_snapshot,
|
|
48
|
+
)
|
|
49
|
+
from src_py_lib.utils.config import (
|
|
50
|
+
config_parse_args as parse_args,
|
|
51
|
+
)
|
|
52
|
+
from src_py_lib.utils.http import HTTPClient, HTTPClientError
|
|
53
|
+
from src_py_lib.utils.json_cache import load_json_cache, load_json_subset, save_json_cache
|
|
54
|
+
from src_py_lib.utils.json_types import (
|
|
55
|
+
JSONDict,
|
|
56
|
+
json_dict,
|
|
57
|
+
json_dicts,
|
|
58
|
+
json_int,
|
|
59
|
+
json_list,
|
|
60
|
+
json_str,
|
|
61
|
+
json_strs,
|
|
62
|
+
)
|
|
63
|
+
from src_py_lib.utils.logging import (
|
|
64
|
+
LoggingConfig,
|
|
65
|
+
LoggingSettings,
|
|
66
|
+
configure_logging,
|
|
67
|
+
critical,
|
|
68
|
+
debug,
|
|
69
|
+
error,
|
|
70
|
+
event,
|
|
71
|
+
info,
|
|
72
|
+
log,
|
|
73
|
+
log_context,
|
|
74
|
+
logging_context,
|
|
75
|
+
logging_settings_from_config,
|
|
76
|
+
resolve_log_level_name,
|
|
77
|
+
stage,
|
|
78
|
+
startup_event,
|
|
79
|
+
submit_with_log_context,
|
|
80
|
+
warning,
|
|
81
|
+
)
|
|
82
|
+
from src_py_lib.utils.tsv import write_tsv
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def logging(
|
|
86
|
+
config: object | None = None,
|
|
87
|
+
*,
|
|
88
|
+
command: str | None = None,
|
|
89
|
+
git_cwd: Path | str | None = None,
|
|
90
|
+
logging_config: LoggingSettings | None = None,
|
|
91
|
+
run_fields: Mapping[str, Any] | None = None,
|
|
92
|
+
run_summary: Callable[[], Mapping[str, Any]] | None = None,
|
|
93
|
+
) -> AbstractContextManager[Path | None]:
|
|
94
|
+
"""Configure standard CLI logging and emit startup metadata."""
|
|
95
|
+
return logging_context(
|
|
96
|
+
command or _script_name(),
|
|
97
|
+
config,
|
|
98
|
+
git_cwd=git_cwd,
|
|
99
|
+
logging_config=logging_config,
|
|
100
|
+
run_fields=run_fields,
|
|
101
|
+
run_summary=run_summary,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _script_name() -> str:
|
|
106
|
+
return Path(sys.argv[0]).stem or "python"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"Config",
|
|
111
|
+
"ConfigError",
|
|
112
|
+
"GraphQLError",
|
|
113
|
+
"GraphQLClient",
|
|
114
|
+
"GitHubClient",
|
|
115
|
+
"GoogleSheetsClient",
|
|
116
|
+
"GoogleSheetsError",
|
|
117
|
+
"HTTPClient",
|
|
118
|
+
"HTTPClientError",
|
|
119
|
+
"JSONDict",
|
|
120
|
+
"LinearClient",
|
|
121
|
+
"LinearClientConfig",
|
|
122
|
+
"LoggingConfig",
|
|
123
|
+
"LoggingSettings",
|
|
124
|
+
"PullRequest",
|
|
125
|
+
"SlackClient",
|
|
126
|
+
"SlackClientConfig",
|
|
127
|
+
"SlackError",
|
|
128
|
+
"SlackPacer",
|
|
129
|
+
"SourcegraphClient",
|
|
130
|
+
"SourcegraphClientConfig",
|
|
131
|
+
"aliased_batched_query",
|
|
132
|
+
"config_field",
|
|
133
|
+
"config_snapshot",
|
|
134
|
+
"configure_logging",
|
|
135
|
+
"critical",
|
|
136
|
+
"debug",
|
|
137
|
+
"error",
|
|
138
|
+
"event",
|
|
139
|
+
"gh_cli_token",
|
|
140
|
+
"gcloud_adc_access_token",
|
|
141
|
+
"info",
|
|
142
|
+
"introspect_schema",
|
|
143
|
+
"json_dict",
|
|
144
|
+
"json_dicts",
|
|
145
|
+
"json_int",
|
|
146
|
+
"json_list",
|
|
147
|
+
"json_str",
|
|
148
|
+
"json_strs",
|
|
149
|
+
"linear_client_from_config",
|
|
150
|
+
"load_json_cache",
|
|
151
|
+
"load_json_subset",
|
|
152
|
+
"logging",
|
|
153
|
+
"logging_settings_from_config",
|
|
154
|
+
"log",
|
|
155
|
+
"log_context",
|
|
156
|
+
"normalize_sourcegraph_endpoint",
|
|
157
|
+
"parse_args",
|
|
158
|
+
"pr_ref_from_url",
|
|
159
|
+
"quota_project_from_adc",
|
|
160
|
+
"resolve_log_level_name",
|
|
161
|
+
"save_json_cache",
|
|
162
|
+
"slack_client_from_config",
|
|
163
|
+
"sourcegraph_client_from_config",
|
|
164
|
+
"stage",
|
|
165
|
+
"startup_event",
|
|
166
|
+
"stream_connection_nodes",
|
|
167
|
+
"submit_with_log_context",
|
|
168
|
+
"warning",
|
|
169
|
+
"write_tsv",
|
|
170
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""GitHub GraphQL API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TypedDict, cast
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
|
|
12
|
+
from src_py_lib.clients.graphql import GraphQLClient, aliased_batched_query
|
|
13
|
+
from src_py_lib.utils.http import HTTPClient
|
|
14
|
+
from src_py_lib.utils.json_types import JSONDict, json_dict, json_str
|
|
15
|
+
|
|
16
|
+
DEFAULT_GITHUB_URL = "https://github.com"
|
|
17
|
+
DEFAULT_PR_BATCH_SIZE = 50
|
|
18
|
+
GITHUB_VALIDATE_QUERY = """
|
|
19
|
+
query GitHubClientValidate {
|
|
20
|
+
viewer {
|
|
21
|
+
login
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
PR_REF_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/#]+)#(?P<number>\d+)$")
|
|
26
|
+
PR_URL_RE = re.compile(
|
|
27
|
+
r"https?://[^/\s)>|]+/(?P<owner>[^/\s)>|]+)/(?P<repo>[^/\s)>|]+)/pull/(?P<number>\d+)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PullRequest(TypedDict):
|
|
32
|
+
title: str
|
|
33
|
+
url: str
|
|
34
|
+
state: str
|
|
35
|
+
createdAt: str
|
|
36
|
+
mergedAt: str
|
|
37
|
+
closedAt: str
|
|
38
|
+
author: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class GitHubClient:
|
|
43
|
+
token: str
|
|
44
|
+
github_url: str = DEFAULT_GITHUB_URL
|
|
45
|
+
http: HTTPClient = field(default_factory=HTTPClient)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_gh_cli(
|
|
49
|
+
cls, *, github_url: str = DEFAULT_GITHUB_URL, http: HTTPClient | None = None
|
|
50
|
+
) -> GitHubClient:
|
|
51
|
+
token = gh_cli_token(github_url=github_url)
|
|
52
|
+
if not token:
|
|
53
|
+
raise RuntimeError("No GitHub token from `gh auth token`.")
|
|
54
|
+
return cls(token=token, github_url=github_url, http=http or HTTPClient())
|
|
55
|
+
|
|
56
|
+
def graphql(self, query: str, variables: JSONDict | None = None) -> JSONDict:
|
|
57
|
+
return GraphQLClient(
|
|
58
|
+
url=graphql_api_url(self.github_url),
|
|
59
|
+
headers={"Authorization": f"bearer {self.token}"},
|
|
60
|
+
label="GitHub",
|
|
61
|
+
http=self.http,
|
|
62
|
+
tolerate_partial_errors=True,
|
|
63
|
+
).execute(query, variables)
|
|
64
|
+
|
|
65
|
+
def validate(self) -> JSONDict:
|
|
66
|
+
"""Validate the token with a cheap viewer query and return the viewer."""
|
|
67
|
+
viewer = json_dict(self.graphql(GITHUB_VALIDATE_QUERY).get("viewer"))
|
|
68
|
+
if not viewer.get("login"):
|
|
69
|
+
raise RuntimeError("GitHub viewer response did not include viewer.login.")
|
|
70
|
+
return viewer
|
|
71
|
+
|
|
72
|
+
def get_pull_requests(
|
|
73
|
+
self, refs: list[str], *, batch_size: int = DEFAULT_PR_BATCH_SIZE
|
|
74
|
+
) -> dict[str, PullRequest]:
|
|
75
|
+
return cast(
|
|
76
|
+
dict[str, PullRequest],
|
|
77
|
+
aliased_batched_query(
|
|
78
|
+
refs,
|
|
79
|
+
batch_size=batch_size,
|
|
80
|
+
build_alias=_build_pr_alias,
|
|
81
|
+
parse_node=_project_pull_request,
|
|
82
|
+
post=self.graphql,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def graphql_api_url(github_url: str = DEFAULT_GITHUB_URL) -> str:
|
|
88
|
+
"""Return the GraphQL API URL for github.com or a GitHub Enterprise host."""
|
|
89
|
+
normalized = _normalize_github_url(github_url)
|
|
90
|
+
split = urlsplit(normalized)
|
|
91
|
+
if split.hostname == "github.com":
|
|
92
|
+
return f"{split.scheme}://api.github.com/graphql"
|
|
93
|
+
return f"{normalized}/api/graphql"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def gh_cli_token(*, github_url: str = DEFAULT_GITHUB_URL) -> str | None:
|
|
97
|
+
"""Return `gh auth token`, or None when gh is unavailable/not logged in."""
|
|
98
|
+
split = urlsplit(_normalize_github_url(github_url))
|
|
99
|
+
command = ["gh", "auth", "token"]
|
|
100
|
+
if split.hostname and split.hostname != "github.com":
|
|
101
|
+
command.extend(["--hostname", split.netloc])
|
|
102
|
+
try:
|
|
103
|
+
result = subprocess.run(command, capture_output=True, text=True, timeout=5, check=False)
|
|
104
|
+
except OSError:
|
|
105
|
+
return None
|
|
106
|
+
except subprocess.SubprocessError:
|
|
107
|
+
return None
|
|
108
|
+
token = result.stdout.strip()
|
|
109
|
+
return token if result.returncode == 0 and token else None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _normalize_github_url(github_url: str) -> str:
|
|
113
|
+
stripped = github_url.strip().rstrip("/")
|
|
114
|
+
if "://" not in stripped:
|
|
115
|
+
stripped = f"https://{stripped}"
|
|
116
|
+
return stripped
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_pr_ref(ref: str) -> tuple[str, str, int]:
|
|
120
|
+
match = PR_REF_RE.match(ref)
|
|
121
|
+
if not match:
|
|
122
|
+
raise ValueError(f"invalid GitHub PR ref: {ref!r}")
|
|
123
|
+
return match.group("owner"), match.group("repo"), int(match.group("number"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def pr_ref_from_url(url: str) -> str | None:
|
|
127
|
+
match = PR_URL_RE.search(url)
|
|
128
|
+
if not match:
|
|
129
|
+
return None
|
|
130
|
+
return f"{match.group('owner')}/{match.group('repo')}#{match.group('number')}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_pr_alias(_index: int, ref: str) -> str | None:
|
|
134
|
+
try:
|
|
135
|
+
owner, repo, number = parse_pr_ref(ref)
|
|
136
|
+
except ValueError:
|
|
137
|
+
return None
|
|
138
|
+
return (
|
|
139
|
+
f"repository(owner: {json.dumps(owner)}, name: {json.dumps(repo)}) "
|
|
140
|
+
f"{{ pullRequest(number: {number}) "
|
|
141
|
+
"{ title url state createdAt mergedAt closedAt author { login } } }"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _project_pull_request(node: JSONDict) -> PullRequest | None:
|
|
146
|
+
pull_request = json_dict(node.get("pullRequest"))
|
|
147
|
+
if not pull_request:
|
|
148
|
+
return None
|
|
149
|
+
return {
|
|
150
|
+
"title": json_str(pull_request, "title"),
|
|
151
|
+
"url": json_str(pull_request, "url"),
|
|
152
|
+
"state": json_str(pull_request, "state"),
|
|
153
|
+
"createdAt": json_str(pull_request, "createdAt"),
|
|
154
|
+
"mergedAt": json_str(pull_request, "mergedAt"),
|
|
155
|
+
"closedAt": json_str(pull_request, "closedAt"),
|
|
156
|
+
"author": json_str(json_dict(pull_request.get("author")), "login"),
|
|
157
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Google Sheets API client using an already-available OAuth access token."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
from src_py_lib.utils.http import HTTPClient, HTTPClientError
|
|
13
|
+
from src_py_lib.utils.json_types import JSONDict, json_dict, json_int, json_list, json_str
|
|
14
|
+
|
|
15
|
+
SHEETS_API_URL = "https://sheets.googleapis.com/v4/spreadsheets"
|
|
16
|
+
DEFAULT_ADC_FILE = Path("~/.config/gcloud/application_default_credentials.json").expanduser()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GoogleSheetsError(RuntimeError):
|
|
20
|
+
"""Raised for Google Sheets client errors."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class LinkRun:
|
|
25
|
+
start: int
|
|
26
|
+
end: int
|
|
27
|
+
uri: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Cell:
|
|
32
|
+
text: str
|
|
33
|
+
links: tuple[LinkRun, ...] = ()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CellValue = str | Cell
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class GoogleSheetsClient:
|
|
41
|
+
spreadsheet_id: str
|
|
42
|
+
access_token: str
|
|
43
|
+
quota_project: str | None = None
|
|
44
|
+
http: HTTPClient = field(default_factory=HTTPClient)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_gcloud_adc(
|
|
48
|
+
cls,
|
|
49
|
+
spreadsheet_id: str,
|
|
50
|
+
*,
|
|
51
|
+
credentials_file: Path = DEFAULT_ADC_FILE,
|
|
52
|
+
http: HTTPClient | None = None,
|
|
53
|
+
) -> GoogleSheetsClient:
|
|
54
|
+
return cls(
|
|
55
|
+
spreadsheet_id=spreadsheet_id,
|
|
56
|
+
access_token=gcloud_adc_access_token(credentials_file),
|
|
57
|
+
quota_project=quota_project_from_adc(credentials_file),
|
|
58
|
+
http=http or HTTPClient(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def request(self, method: str, path: str, body: JSONDict | None = None) -> JSONDict:
|
|
62
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
63
|
+
if self.quota_project:
|
|
64
|
+
headers["X-Goog-User-Project"] = self.quota_project
|
|
65
|
+
try:
|
|
66
|
+
return self.http.json(
|
|
67
|
+
method,
|
|
68
|
+
f"{SHEETS_API_URL}/{self.spreadsheet_id}{path}",
|
|
69
|
+
headers=headers,
|
|
70
|
+
json_body=body,
|
|
71
|
+
)
|
|
72
|
+
except HTTPClientError as exception:
|
|
73
|
+
raise GoogleSheetsError(
|
|
74
|
+
f"Google Sheets {method} {path} failed: {exception}"
|
|
75
|
+
) from exception
|
|
76
|
+
|
|
77
|
+
def metadata(self) -> JSONDict:
|
|
78
|
+
return self.request("GET", "?fields=sheets.properties(sheetId,title,gridProperties)")
|
|
79
|
+
|
|
80
|
+
def validate(self) -> JSONDict:
|
|
81
|
+
"""Validate spreadsheet access and return spreadsheet metadata."""
|
|
82
|
+
metadata = self.metadata()
|
|
83
|
+
if not isinstance(metadata.get("sheets"), list):
|
|
84
|
+
raise GoogleSheetsError("Google Sheets metadata response did not include sheets.")
|
|
85
|
+
return metadata
|
|
86
|
+
|
|
87
|
+
def tab_ids_by_title(self) -> dict[str, int]:
|
|
88
|
+
return {
|
|
89
|
+
json_str(properties, "title"): json_int(properties, "sheetId")
|
|
90
|
+
for sheet in json_list(self.metadata().get("sheets"))
|
|
91
|
+
if (properties := json_dict(json_dict(sheet).get("properties")))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def batch_update(self, requests: list[JSONDict]) -> JSONDict:
|
|
95
|
+
return self.request("POST", ":batchUpdate", cast(JSONDict, {"requests": requests}))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def hyperlink_cell(url: str, text: str) -> CellValue:
|
|
99
|
+
if not url:
|
|
100
|
+
return ""
|
|
101
|
+
return Cell(text=text, links=(LinkRun(0, len(text), url),))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def quota_project_from_adc(credentials_file: Path = DEFAULT_ADC_FILE) -> str:
|
|
105
|
+
if not credentials_file.exists():
|
|
106
|
+
raise GoogleSheetsError(f"Application Default Credentials not found at {credentials_file}.")
|
|
107
|
+
data = json_dict(json.loads(credentials_file.read_text(encoding="utf-8")))
|
|
108
|
+
quota_project = json_str(data, "quota_project_id")
|
|
109
|
+
if not quota_project:
|
|
110
|
+
raise GoogleSheetsError(f"{credentials_file} does not contain quota_project_id.")
|
|
111
|
+
return quota_project
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def gcloud_adc_access_token(credentials_file: Path = DEFAULT_ADC_FILE) -> str:
|
|
115
|
+
env = os.environ.copy()
|
|
116
|
+
env["GOOGLE_APPLICATION_CREDENTIALS"] = str(credentials_file)
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["gcloud", "auth", "application-default", "print-access-token"],
|
|
120
|
+
env=env,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=10,
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
except (OSError, subprocess.SubprocessError) as exception:
|
|
127
|
+
raise GoogleSheetsError("Could not run gcloud to fetch an ADC access token.") from exception
|
|
128
|
+
token = result.stdout.strip()
|
|
129
|
+
if result.returncode != 0 or not token:
|
|
130
|
+
raise GoogleSheetsError(result.stderr.strip() or "gcloud did not return an access token.")
|
|
131
|
+
return token
|