google-drive-model 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.
- gdm/__init__.py +77 -0
- gdm/auth.py +514 -0
- gdm/backends/__init__.py +15 -0
- gdm/backends/base.py +115 -0
- gdm/backends/drive.py +716 -0
- gdm/backends/memory.py +448 -0
- gdm/backends/registry.py +1133 -0
- gdm/backends/sheets.py +644 -0
- gdm/cli.py +1108 -0
- gdm/client.py +383 -0
- gdm/codecs.py +246 -0
- gdm/collection/__init__.py +23 -0
- gdm/collection/backend.py +515 -0
- gdm/collection/collection.py +350 -0
- gdm/collection/constants.py +59 -0
- gdm/collection/content.py +496 -0
- gdm/collection/inventory.py +285 -0
- gdm/collection/metadata.py +780 -0
- gdm/errors.py +136 -0
- gdm/explore.py +489 -0
- gdm/explore_types.py +64 -0
- gdm/fields.py +166 -0
- gdm/google_api.py +631 -0
- gdm/indexes.py +735 -0
- gdm/infer.py +644 -0
- gdm/migration_target.py +842 -0
- gdm/migrations.py +1774 -0
- gdm/model.py +468 -0
- gdm/permissions.py +278 -0
- gdm/project.py +738 -0
- gdm/py.typed +1 -0
- gdm/query.py +366 -0
- gdm/retry.py +145 -0
- gdm/schema.py +850 -0
- gdm/session.py +623 -0
- gdm/sync.py +236 -0
- gdm/testing/__init__.py +45 -0
- gdm/tooling/__init__.py +3 -0
- gdm/tooling/adopt.py +100 -0
- gdm/tooling/collection_probe.py +170 -0
- gdm/tooling/doctor.py +267 -0
- gdm/tooling/mime.py +62 -0
- google_drive_model-0.1.0.dist-info/METADATA +277 -0
- google_drive_model-0.1.0.dist-info/RECORD +46 -0
- google_drive_model-0.1.0.dist-info/WHEEL +4 -0
- google_drive_model-0.1.0.dist-info/entry_points.txt +3 -0
gdm/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Public package surface for google-drive-model."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from gdm.auth import login_interactive
|
|
6
|
+
from gdm.client import GDM, GDMClient
|
|
7
|
+
from gdm.collection.collection import DriveCollection
|
|
8
|
+
from gdm.collection.content import ContentHandle
|
|
9
|
+
from gdm.errors import (
|
|
10
|
+
AmbiguousResourceError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
ForeignProjectError,
|
|
13
|
+
GDMError,
|
|
14
|
+
InvalidDrivePropertyError,
|
|
15
|
+
InvalidForeignKeyError,
|
|
16
|
+
InvalidModelConfigError,
|
|
17
|
+
MissingOptionalDependencyError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
PartialCommitError,
|
|
20
|
+
PermissionDeniedError,
|
|
21
|
+
QueryEvaluationError,
|
|
22
|
+
ReadOnlySessionError,
|
|
23
|
+
ResultError,
|
|
24
|
+
ScanLimitExceeded,
|
|
25
|
+
UnknownSourceFieldError,
|
|
26
|
+
UnsupportedOperationError,
|
|
27
|
+
)
|
|
28
|
+
from gdm.fields import Field, GDMFieldMetadata
|
|
29
|
+
from gdm.indexes import GdmIndexError, IndexStore, StaleIndexError, StalePolicy
|
|
30
|
+
from gdm.model import DriveObjectMixin, GDMModel
|
|
31
|
+
from gdm.project import ResourceKind
|
|
32
|
+
from gdm.query import Select, select
|
|
33
|
+
from gdm.session import Result, Session
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"GDM",
|
|
37
|
+
"AmbiguousResourceError",
|
|
38
|
+
"ConflictError",
|
|
39
|
+
"ContentHandle",
|
|
40
|
+
"DriveCollection",
|
|
41
|
+
"DriveObjectMixin",
|
|
42
|
+
"Field",
|
|
43
|
+
"ForeignProjectError",
|
|
44
|
+
"GDMClient",
|
|
45
|
+
"GDMError",
|
|
46
|
+
"GDMFieldMetadata",
|
|
47
|
+
"GDMModel",
|
|
48
|
+
"GdmIndexError",
|
|
49
|
+
"IndexStore",
|
|
50
|
+
"InvalidDrivePropertyError",
|
|
51
|
+
"InvalidForeignKeyError",
|
|
52
|
+
"InvalidModelConfigError",
|
|
53
|
+
"MissingOptionalDependencyError",
|
|
54
|
+
"NotFoundError",
|
|
55
|
+
"PartialCommitError",
|
|
56
|
+
"PermissionDeniedError",
|
|
57
|
+
"QueryEvaluationError",
|
|
58
|
+
"ReadOnlySessionError",
|
|
59
|
+
"ResourceKind",
|
|
60
|
+
"Result",
|
|
61
|
+
"ResultError",
|
|
62
|
+
"ScanLimitExceeded",
|
|
63
|
+
"Select",
|
|
64
|
+
"Session",
|
|
65
|
+
"StaleIndexError",
|
|
66
|
+
"StalePolicy",
|
|
67
|
+
"UnknownSourceFieldError",
|
|
68
|
+
"UnsupportedOperationError",
|
|
69
|
+
"__version__",
|
|
70
|
+
"login_interactive",
|
|
71
|
+
"select",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
__version__ = version("google-drive-model")
|
|
76
|
+
except PackageNotFoundError: # pragma: no cover - source tree without installation
|
|
77
|
+
__version__ = "0+unknown"
|
gdm/auth.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""Credential and Google API service construction for GDM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal, overload
|
|
10
|
+
|
|
11
|
+
from google.auth import default as google_auth_default
|
|
12
|
+
from google.auth.credentials import Credentials
|
|
13
|
+
from google.oauth2 import service_account
|
|
14
|
+
from googleapiclient.discovery import Resource, build
|
|
15
|
+
|
|
16
|
+
from gdm.google_api import (
|
|
17
|
+
DriveExplorerAdapter,
|
|
18
|
+
ExecutingDocsService,
|
|
19
|
+
ExecutingDriveService,
|
|
20
|
+
ExecutingSheetsService,
|
|
21
|
+
ExecutingSlidesService,
|
|
22
|
+
wrap_docs_service,
|
|
23
|
+
wrap_drive_service,
|
|
24
|
+
wrap_sheets_service,
|
|
25
|
+
wrap_slides_service,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Least-privilege scopes for GDM-created or user-opened resources.
|
|
31
|
+
SCOPE_DRIVE_FILE = "https://www.googleapis.com/auth/drive.file"
|
|
32
|
+
SCOPE_SPREADSHEETS = "https://www.googleapis.com/auth/spreadsheets"
|
|
33
|
+
|
|
34
|
+
# Broader Drive scope required to discover/adopt arbitrary existing content by path
|
|
35
|
+
# or name. Restricted-scope verification may be required for distributed OAuth apps.
|
|
36
|
+
SCOPE_DRIVE = "https://www.googleapis.com/auth/drive"
|
|
37
|
+
SCOPE_DRIVE_READONLY = "https://www.googleapis.com/auth/drive.readonly"
|
|
38
|
+
|
|
39
|
+
DEFAULT_SCOPES: tuple[str, ...] = (SCOPE_DRIVE_FILE, SCOPE_SPREADSHEETS)
|
|
40
|
+
DISCOVERY_SCOPES: tuple[str, ...] = (SCOPE_DRIVE, SCOPE_SPREADSHEETS)
|
|
41
|
+
|
|
42
|
+
DRIVE_API_VERSION = "v3"
|
|
43
|
+
SHEETS_API_VERSION = "v4"
|
|
44
|
+
DOCS_API_VERSION = "v1"
|
|
45
|
+
SLIDES_API_VERSION = "v1"
|
|
46
|
+
TOKEN_FILENAME = "token.json"
|
|
47
|
+
|
|
48
|
+
SCOPE_ALIASES: dict[str, str] = {
|
|
49
|
+
"drive": SCOPE_DRIVE,
|
|
50
|
+
"drive.readonly": SCOPE_DRIVE_READONLY,
|
|
51
|
+
"drive.file": SCOPE_DRIVE_FILE,
|
|
52
|
+
"spreadsheets": SCOPE_SPREADSHEETS,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OAuthUnavailableError(ImportError):
|
|
57
|
+
"""Raised when installed-app OAuth is requested without the optional extra."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _user_config_dir(appname: str = "gdm") -> Path:
|
|
61
|
+
"""Return a platform-appropriate user config directory."""
|
|
62
|
+
try:
|
|
63
|
+
import platformdirs
|
|
64
|
+
|
|
65
|
+
return Path(platformdirs.user_config_dir(appname, appauthor=False))
|
|
66
|
+
except ImportError:
|
|
67
|
+
home = Path.home()
|
|
68
|
+
xdg = home / ".config" / appname
|
|
69
|
+
if xdg.parent.exists() or not (home / "Library").exists():
|
|
70
|
+
return xdg
|
|
71
|
+
return home / "Library" / "Application Support" / appname
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def default_token_path(*, appname: str = "gdm") -> Path:
|
|
75
|
+
"""Default on-disk location for OAuth refresh tokens (never inside a project)."""
|
|
76
|
+
return _user_config_dir(appname) / TOKEN_FILENAME
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _write_private_token(path: Path, payload: str) -> None:
|
|
80
|
+
"""Write an OAuth token with owner-only permissions."""
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
83
|
+
try:
|
|
84
|
+
fchmod = getattr(os, "fchmod", None)
|
|
85
|
+
if callable(fchmod):
|
|
86
|
+
fchmod(descriptor, 0o600)
|
|
87
|
+
else: # pragma: no cover - Windows fallback
|
|
88
|
+
os.chmod(path, 0o600)
|
|
89
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
90
|
+
descriptor = -1
|
|
91
|
+
handle.write(payload)
|
|
92
|
+
finally:
|
|
93
|
+
if descriptor >= 0:
|
|
94
|
+
os.close(descriptor)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_client_secrets_path(
|
|
98
|
+
explicit: str | Path | None = None,
|
|
99
|
+
) -> Path | None:
|
|
100
|
+
"""Resolve OAuth client secrets from explicit path or ``GDM_CLIENT_SECRETS``."""
|
|
101
|
+
if explicit is not None:
|
|
102
|
+
return Path(explicit)
|
|
103
|
+
env = os.environ.get("GDM_CLIENT_SECRETS")
|
|
104
|
+
if env:
|
|
105
|
+
return Path(env)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_oauth_modules() -> tuple[Any, Any, Any]:
|
|
110
|
+
try:
|
|
111
|
+
from google.auth.transport.requests import Request
|
|
112
|
+
from google.oauth2.credentials import Credentials as UserCredentials
|
|
113
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
114
|
+
except ImportError as exc: # pragma: no cover - exercised via OAuthUnavailableError
|
|
115
|
+
raise OAuthUnavailableError(
|
|
116
|
+
"Installed-app OAuth requires the optional dependency: "
|
|
117
|
+
"pip install google-drive-model[oauth]"
|
|
118
|
+
) from exc
|
|
119
|
+
return Request, UserCredentials, InstalledAppFlow
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _normalize_scopes(scopes: Sequence[str] | None) -> list[str]:
|
|
123
|
+
if scopes is None:
|
|
124
|
+
return list(DEFAULT_SCOPES)
|
|
125
|
+
resolved: list[str] = []
|
|
126
|
+
for scope in scopes:
|
|
127
|
+
token = scope.strip()
|
|
128
|
+
if not token:
|
|
129
|
+
continue
|
|
130
|
+
resolved.append(SCOPE_ALIASES.get(token, token))
|
|
131
|
+
return resolved
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_token_credentials(
|
|
135
|
+
token_path: Path,
|
|
136
|
+
scopes: list[str],
|
|
137
|
+
) -> Credentials | None:
|
|
138
|
+
if not token_path.is_file():
|
|
139
|
+
return None
|
|
140
|
+
# Authorized-user files already record the scopes that were granted during
|
|
141
|
+
# consent. Replacing them here can make refresh fail with ``invalid_scope``
|
|
142
|
+
# when a broader token (for example ``drive``) is reused by a narrower
|
|
143
|
+
# client path (for example ``drive.file``).
|
|
144
|
+
del scopes
|
|
145
|
+
_, UserCredentials, _ = _load_oauth_modules()
|
|
146
|
+
creds = UserCredentials.from_authorized_user_file(str(token_path))
|
|
147
|
+
if creds and creds.valid:
|
|
148
|
+
return creds
|
|
149
|
+
if creds and creds.expired and creds.refresh_token:
|
|
150
|
+
Request, _, _ = _load_oauth_modules()
|
|
151
|
+
creds.refresh(Request())
|
|
152
|
+
_write_private_token(token_path, creds.to_json())
|
|
153
|
+
return creds
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_installed_app_flow(
|
|
158
|
+
client_secrets: str | Path | dict[str, Any],
|
|
159
|
+
scopes: list[str],
|
|
160
|
+
token_path: Path,
|
|
161
|
+
) -> Credentials:
|
|
162
|
+
_, _, InstalledAppFlow = _load_oauth_modules()
|
|
163
|
+
if isinstance(client_secrets, dict):
|
|
164
|
+
flow = InstalledAppFlow.from_client_config(client_secrets, scopes)
|
|
165
|
+
else:
|
|
166
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(client_secrets), scopes)
|
|
167
|
+
creds = flow.run_local_server(port=0)
|
|
168
|
+
_write_private_token(token_path, creds.to_json())
|
|
169
|
+
logger.info("Stored OAuth token at %s", token_path)
|
|
170
|
+
return creds
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_credentials(
|
|
174
|
+
*,
|
|
175
|
+
credentials: Credentials | None = None,
|
|
176
|
+
use_adc: bool = False,
|
|
177
|
+
service_account_file: str | Path | None = None,
|
|
178
|
+
service_account_info: dict[str, Any] | None = None,
|
|
179
|
+
oauth_client_secrets: str | Path | dict[str, Any] | None = None,
|
|
180
|
+
token_path: str | Path | None = None,
|
|
181
|
+
scopes: Sequence[str] | None = None,
|
|
182
|
+
allow_oauth_interactive: bool = False,
|
|
183
|
+
) -> Credentials:
|
|
184
|
+
"""Build Google credentials from explicit inputs, ADC, or optional OAuth.
|
|
185
|
+
|
|
186
|
+
Precedence: injected ``credentials`` > service account > on-disk OAuth token
|
|
187
|
+
> interactive OAuth (when enabled) > Application Default Credentials.
|
|
188
|
+
"""
|
|
189
|
+
normalized = _normalize_scopes(scopes)
|
|
190
|
+
if credentials is not None:
|
|
191
|
+
return credentials
|
|
192
|
+
|
|
193
|
+
if service_account_file is not None:
|
|
194
|
+
return service_account.Credentials.from_service_account_file(
|
|
195
|
+
str(service_account_file),
|
|
196
|
+
scopes=normalized,
|
|
197
|
+
)
|
|
198
|
+
if service_account_info is not None:
|
|
199
|
+
return service_account.Credentials.from_service_account_info(
|
|
200
|
+
service_account_info,
|
|
201
|
+
scopes=normalized,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
resolved_token = (
|
|
205
|
+
Path(token_path) if token_path is not None else default_token_path()
|
|
206
|
+
)
|
|
207
|
+
token_creds = _load_token_credentials(resolved_token, normalized)
|
|
208
|
+
if token_creds is not None:
|
|
209
|
+
has_scopes = getattr(token_creds, "has_scopes", None)
|
|
210
|
+
scopes_satisfied = (
|
|
211
|
+
not normalized or not callable(has_scopes) or bool(has_scopes(normalized))
|
|
212
|
+
)
|
|
213
|
+
if scopes_satisfied:
|
|
214
|
+
return token_creds
|
|
215
|
+
if not allow_oauth_interactive:
|
|
216
|
+
msg = (
|
|
217
|
+
f"Stored OAuth token {resolved_token} does not grant all required "
|
|
218
|
+
"scopes. Run `gdm auth login` to authorize the current scope set."
|
|
219
|
+
)
|
|
220
|
+
raise PermissionDeniedError(msg)
|
|
221
|
+
|
|
222
|
+
resolved_secrets = resolve_client_secrets_path(
|
|
223
|
+
oauth_client_secrets if isinstance(oauth_client_secrets, (str, Path)) else None
|
|
224
|
+
)
|
|
225
|
+
secrets = oauth_client_secrets
|
|
226
|
+
if secrets is None and resolved_secrets is not None:
|
|
227
|
+
secrets = resolved_secrets
|
|
228
|
+
|
|
229
|
+
if secrets is not None and allow_oauth_interactive:
|
|
230
|
+
return _run_installed_app_flow(secrets, normalized, resolved_token)
|
|
231
|
+
|
|
232
|
+
if use_adc or (
|
|
233
|
+
service_account_file is None
|
|
234
|
+
and service_account_info is None
|
|
235
|
+
and secrets is None
|
|
236
|
+
):
|
|
237
|
+
adc_creds, _ = google_auth_default(scopes=normalized)
|
|
238
|
+
return adc_creds
|
|
239
|
+
|
|
240
|
+
if secrets is not None:
|
|
241
|
+
raise OAuthUnavailableError(
|
|
242
|
+
"OAuth client secrets were provided but no token exists. "
|
|
243
|
+
"Pass allow_oauth_interactive=True to run the installed-app flow, "
|
|
244
|
+
"or install google-drive-model[oauth]."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
msg = "No credential source configured."
|
|
248
|
+
raise ValueError(msg)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def login_interactive(
|
|
252
|
+
*,
|
|
253
|
+
scopes: Sequence[str] | None = None,
|
|
254
|
+
client_secrets: str | Path | dict[str, Any] | None = None,
|
|
255
|
+
token_path: str | Path | None = None,
|
|
256
|
+
) -> Credentials:
|
|
257
|
+
"""Run installed-app OAuth and persist the refresh token locally."""
|
|
258
|
+
resolved_secrets = client_secrets or resolve_client_secrets_path()
|
|
259
|
+
if resolved_secrets is None:
|
|
260
|
+
msg = (
|
|
261
|
+
"OAuth client secrets are required. Pass client_secrets= or set "
|
|
262
|
+
"GDM_CLIENT_SECRETS to a client_secret JSON path."
|
|
263
|
+
)
|
|
264
|
+
raise ValueError(msg)
|
|
265
|
+
requested_scopes = list(scopes) if scopes is not None else list(DISCOVERY_SCOPES)
|
|
266
|
+
return build_credentials(
|
|
267
|
+
oauth_client_secrets=resolved_secrets,
|
|
268
|
+
token_path=token_path,
|
|
269
|
+
scopes=requested_scopes,
|
|
270
|
+
allow_oauth_interactive=True,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@overload
|
|
275
|
+
def build_drive_service(
|
|
276
|
+
credentials: Credentials,
|
|
277
|
+
*,
|
|
278
|
+
cache_discovery: bool = False,
|
|
279
|
+
wrap: Literal[True] = True,
|
|
280
|
+
supports_all_drives: bool = True,
|
|
281
|
+
) -> ExecutingDriveService: ...
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
def build_drive_service(
|
|
286
|
+
credentials: Credentials,
|
|
287
|
+
*,
|
|
288
|
+
cache_discovery: bool = False,
|
|
289
|
+
wrap: Literal[False],
|
|
290
|
+
supports_all_drives: bool = True,
|
|
291
|
+
) -> Resource: ...
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def build_drive_service(
|
|
295
|
+
credentials: Credentials,
|
|
296
|
+
*,
|
|
297
|
+
cache_discovery: bool = False,
|
|
298
|
+
wrap: bool = True,
|
|
299
|
+
supports_all_drives: bool = True,
|
|
300
|
+
) -> ExecutingDriveService | Resource:
|
|
301
|
+
"""Construct a Drive v3 discovery client."""
|
|
302
|
+
raw = build(
|
|
303
|
+
"drive",
|
|
304
|
+
DRIVE_API_VERSION,
|
|
305
|
+
credentials=credentials,
|
|
306
|
+
cache_discovery=cache_discovery,
|
|
307
|
+
)
|
|
308
|
+
if wrap:
|
|
309
|
+
return wrap_drive_service(raw, supports_all_drives=supports_all_drives)
|
|
310
|
+
return raw
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@overload
|
|
314
|
+
def build_sheets_service(
|
|
315
|
+
credentials: Credentials,
|
|
316
|
+
*,
|
|
317
|
+
cache_discovery: bool = False,
|
|
318
|
+
wrap: Literal[True] = True,
|
|
319
|
+
) -> ExecutingSheetsService: ...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@overload
|
|
323
|
+
def build_sheets_service(
|
|
324
|
+
credentials: Credentials,
|
|
325
|
+
*,
|
|
326
|
+
cache_discovery: bool = False,
|
|
327
|
+
wrap: Literal[False],
|
|
328
|
+
) -> Resource: ...
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def build_sheets_service(
|
|
332
|
+
credentials: Credentials,
|
|
333
|
+
*,
|
|
334
|
+
cache_discovery: bool = False,
|
|
335
|
+
wrap: bool = True,
|
|
336
|
+
) -> ExecutingSheetsService | Resource:
|
|
337
|
+
"""Construct a Sheets v4 discovery client."""
|
|
338
|
+
raw = build(
|
|
339
|
+
"sheets",
|
|
340
|
+
SHEETS_API_VERSION,
|
|
341
|
+
credentials=credentials,
|
|
342
|
+
cache_discovery=cache_discovery,
|
|
343
|
+
)
|
|
344
|
+
if wrap:
|
|
345
|
+
return wrap_sheets_service(raw)
|
|
346
|
+
return raw
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@overload
|
|
350
|
+
def build_docs_service(
|
|
351
|
+
credentials: Credentials,
|
|
352
|
+
*,
|
|
353
|
+
cache_discovery: bool = False,
|
|
354
|
+
wrap: Literal[True] = True,
|
|
355
|
+
) -> ExecutingDocsService: ...
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@overload
|
|
359
|
+
def build_docs_service(
|
|
360
|
+
credentials: Credentials,
|
|
361
|
+
*,
|
|
362
|
+
cache_discovery: bool = False,
|
|
363
|
+
wrap: Literal[False],
|
|
364
|
+
) -> Resource: ...
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def build_docs_service(
|
|
368
|
+
credentials: Credentials,
|
|
369
|
+
*,
|
|
370
|
+
cache_discovery: bool = False,
|
|
371
|
+
wrap: bool = True,
|
|
372
|
+
) -> ExecutingDocsService | Resource:
|
|
373
|
+
"""Construct a Google Docs v1 discovery client."""
|
|
374
|
+
raw = build(
|
|
375
|
+
"docs",
|
|
376
|
+
DOCS_API_VERSION,
|
|
377
|
+
credentials=credentials,
|
|
378
|
+
cache_discovery=cache_discovery,
|
|
379
|
+
)
|
|
380
|
+
if wrap:
|
|
381
|
+
return wrap_docs_service(raw)
|
|
382
|
+
return raw
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@overload
|
|
386
|
+
def build_slides_service(
|
|
387
|
+
credentials: Credentials,
|
|
388
|
+
*,
|
|
389
|
+
cache_discovery: bool = False,
|
|
390
|
+
wrap: Literal[True] = True,
|
|
391
|
+
) -> ExecutingSlidesService: ...
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@overload
|
|
395
|
+
def build_slides_service(
|
|
396
|
+
credentials: Credentials,
|
|
397
|
+
*,
|
|
398
|
+
cache_discovery: bool = False,
|
|
399
|
+
wrap: Literal[False],
|
|
400
|
+
) -> Resource: ...
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def build_slides_service(
|
|
404
|
+
credentials: Credentials,
|
|
405
|
+
*,
|
|
406
|
+
cache_discovery: bool = False,
|
|
407
|
+
wrap: bool = True,
|
|
408
|
+
) -> ExecutingSlidesService | Resource:
|
|
409
|
+
"""Construct a Google Slides v1 discovery client."""
|
|
410
|
+
raw = build(
|
|
411
|
+
"slides",
|
|
412
|
+
SLIDES_API_VERSION,
|
|
413
|
+
credentials=credentials,
|
|
414
|
+
cache_discovery=cache_discovery,
|
|
415
|
+
)
|
|
416
|
+
if wrap:
|
|
417
|
+
return wrap_slides_service(raw)
|
|
418
|
+
return raw
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def build_explorer_service(
|
|
422
|
+
*,
|
|
423
|
+
credentials: Credentials | None = None,
|
|
424
|
+
use_adc: bool = True,
|
|
425
|
+
token_path: str | Path | None = None,
|
|
426
|
+
scopes: Sequence[str] | None = None,
|
|
427
|
+
) -> DriveExplorerAdapter:
|
|
428
|
+
"""Build a read-only Drive explorer adapter using token, ADC, or OAuth."""
|
|
429
|
+
creds = build_credentials(
|
|
430
|
+
credentials=credentials,
|
|
431
|
+
use_adc=use_adc,
|
|
432
|
+
token_path=token_path,
|
|
433
|
+
scopes=scopes or [SCOPE_DRIVE_READONLY, SCOPE_SPREADSHEETS],
|
|
434
|
+
)
|
|
435
|
+
drive = build_drive_service(creds, wrap=True)
|
|
436
|
+
return DriveExplorerAdapter(drive)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def save_token(credentials: Credentials, token_path: str | Path | None = None) -> Path:
|
|
440
|
+
"""Persist authorized-user credentials to disk without logging secrets."""
|
|
441
|
+
to_json = getattr(credentials, "to_json", None)
|
|
442
|
+
if not callable(to_json):
|
|
443
|
+
msg = "Only authorized-user OAuth credentials can be saved as tokens."
|
|
444
|
+
raise TypeError(msg)
|
|
445
|
+
path = Path(token_path) if token_path is not None else default_token_path()
|
|
446
|
+
_write_private_token(path, to_json())
|
|
447
|
+
logger.info("Saved OAuth token at %s", path)
|
|
448
|
+
return path
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def scopes_for_discovery() -> list[str]:
|
|
452
|
+
"""Return scopes needed for arbitrary Drive discovery/adoption."""
|
|
453
|
+
return list(DISCOVERY_SCOPES)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def scopes_for_created_resources() -> list[str]:
|
|
457
|
+
"""Return least-privilege scopes for GDM-managed resources."""
|
|
458
|
+
return list(DEFAULT_SCOPES)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def explain_scope_requirements() -> str:
|
|
462
|
+
"""Human-readable scope guidance for operators."""
|
|
463
|
+
return (
|
|
464
|
+
"Use drive.file + spreadsheets for resources GDM creates or the user "
|
|
465
|
+
"opens through the app. Discovering or adopting arbitrary existing Drive "
|
|
466
|
+
"content by path requires the broader drive scope (or drive.readonly for "
|
|
467
|
+
"read-only adoption), which may trigger Google's restricted-scope review "
|
|
468
|
+
"for distributed OAuth clients."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# Backward-compatible re-exports for modules that imported transport types from auth.
|
|
473
|
+
from gdm.errors import ( # noqa: E402
|
|
474
|
+
AmbiguousResourceError,
|
|
475
|
+
ForeignProjectError,
|
|
476
|
+
MappingMode,
|
|
477
|
+
NotFoundError,
|
|
478
|
+
PermissionDeniedError,
|
|
479
|
+
UnsupportedOperationError,
|
|
480
|
+
)
|
|
481
|
+
from gdm.retry import RetryPolicy, execute_with_retry # noqa: E402
|
|
482
|
+
|
|
483
|
+
__all__ = [
|
|
484
|
+
"DEFAULT_SCOPES",
|
|
485
|
+
"DISCOVERY_SCOPES",
|
|
486
|
+
"DOCS_API_VERSION",
|
|
487
|
+
"SCOPE_DRIVE",
|
|
488
|
+
"SCOPE_DRIVE_FILE",
|
|
489
|
+
"SCOPE_DRIVE_READONLY",
|
|
490
|
+
"SCOPE_SPREADSHEETS",
|
|
491
|
+
"SLIDES_API_VERSION",
|
|
492
|
+
"AmbiguousResourceError",
|
|
493
|
+
"ForeignProjectError",
|
|
494
|
+
"MappingMode",
|
|
495
|
+
"NotFoundError",
|
|
496
|
+
"OAuthUnavailableError",
|
|
497
|
+
"PermissionDeniedError",
|
|
498
|
+
"RetryPolicy",
|
|
499
|
+
"UnsupportedOperationError",
|
|
500
|
+
"build_credentials",
|
|
501
|
+
"build_docs_service",
|
|
502
|
+
"build_drive_service",
|
|
503
|
+
"build_explorer_service",
|
|
504
|
+
"build_sheets_service",
|
|
505
|
+
"build_slides_service",
|
|
506
|
+
"default_token_path",
|
|
507
|
+
"execute_with_retry",
|
|
508
|
+
"explain_scope_requirements",
|
|
509
|
+
"login_interactive",
|
|
510
|
+
"resolve_client_secrets_path",
|
|
511
|
+
"save_token",
|
|
512
|
+
"scopes_for_created_resources",
|
|
513
|
+
"scopes_for_discovery",
|
|
514
|
+
]
|
gdm/backends/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Storage backends shipped with GDM."""
|
|
2
|
+
|
|
3
|
+
from gdm.backends.base import Backend
|
|
4
|
+
from gdm.backends.drive import DriveBackend
|
|
5
|
+
from gdm.backends.memory import MemoryBackend
|
|
6
|
+
from gdm.backends.registry import RegistryBackend
|
|
7
|
+
from gdm.backends.sheets import SheetsBackend
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Backend",
|
|
11
|
+
"DriveBackend",
|
|
12
|
+
"MemoryBackend",
|
|
13
|
+
"RegistryBackend",
|
|
14
|
+
"SheetsBackend",
|
|
15
|
+
]
|