Skip to content

google_api¤

Wrappers around Google APIs.

GCell ¤

Bases: NamedTuple

The information about a Google Sheets cell.

Attributes:

Name Type Description
value Any

The value of the cell.

type GCellType

The data type of value.

formatted str

The formatted value (i.e., how the value is displayed in the cell).

GCellType ¤

Bases: Enum

The spreadsheet cell data type.

Attributes:

Name Type Description
BOOLEAN str

"BOOLEAN"

CURRENCY str

"CURRENCY"

DATE str

"DATE"

DATE_TIME str

"DATE_TIME"

EMPTY str

"EMPTY"

ERROR str

"ERROR"

NUMBER str

"NUMBER"

PERCENT str

"PERCENT"

SCIENTIFIC str

"SCIENTIFIC"

STRING str

"STRING"

TEXT str

"TEXT"

TIME str

"TIME"

UNKNOWN str

"UNKNOWN"

GDateTimeOption ¤

Bases: Enum

Determines how dates should be returned.

FORMATTED_STRING class-attribute instance-attribute ¤

FORMATTED_STRING = 'FORMATTED_STRING'

Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which is dependent on the spreadsheet locale).

SERIAL_NUMBER class-attribute instance-attribute ¤

SERIAL_NUMBER = 'SERIAL_NUMBER'

Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularised by Lotus 1-2-3. The whole number portion of the value (left of the decimal) counts the days since December 30th 1899. The fractional portion (right of the decimal) counts the time as a fraction of the day. For example, January 1st 1900 at noon would be 2.5, 2 because it's 2 days after December 30st 1899, and .5 because noon is half a day. February 1st 1900 at 3pm would be 33.625. This correctly treats the year 1900 as not a leap year.

GDrive ¤

GDrive(
    *,
    account=None,
    credentials=None,
    scopes=None,
    read_only=True,
)

Bases: GoogleAPI

Interact with Google Drive.

Info

You must follow the instructions in the prerequisites section for setting up the Drive API before you can use this class. It is also useful to be aware of the refresh token expiration policy.

Parameters:

Name Type Description Default
account str | None

Since a person may have multiple Google accounts, and multiple people may run the same code, this parameter decides which token to load to authenticate with the Google API. The value can be any text (or None) that you want to associate with a particular Google account, provided that it contains valid characters for a filename. The value that you chose when you authenticated with your credentials should be used for all future instances of this class to access that particular Google account. You can associate a different value with a Google account at any time (by passing in a different account value), but you may be asked to authenticate with your credentials again, or, alternatively, you can rename the token files located in MSL_IO_DIR to match the new account value.

None
credentials PathLike | None

The path to the client secrets OAuth credential file. This parameter only needs to be specified the first time that you authenticate with a particular Google account or if you delete the token file that was created when you previously authenticated.

None
scopes list[str] | None

The list of scopes to enable for the Google API. See Drive scopes for more details. If not specified, default scopes are chosen based on the value of read_only.

None
read_only bool

Whether to interact with Google Drive in read-only mode.

True
Source code in src/msl/io/google_api.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def __init__(
    self,
    *,
    account: str | None = None,
    credentials: PathLike | None = None,
    scopes: list[str] | None = None,
    read_only: bool = True,
) -> None:
    """Interact with Google Drive.

    !!! info
        You must follow the instructions in the prerequisites section for setting up the
        [Drive API](https://developers.google.com/drive/api/quickstart/python#prerequisites){:target="_blank"}
        before you can use this class. It is also useful to be aware of the
        [refresh token expiration](https://developers.google.com/identity/protocols/oauth2#expiration){:target="_blank"}
        policy.

    Args:
        account: Since a person may have multiple Google accounts, and multiple people
            may run the same code, this parameter decides which token to load to authenticate
            with the Google API. The value can be any text (or `None`) that you want to
            associate with a particular Google account, provided that it contains valid
            characters for a filename. The value that you chose when you authenticated with
            your `credentials` should be used for all future instances of this class to access
            that particular Google account. You can associate a different value with a Google
            account at any time (by passing in a different `account` value), but you may be
            asked to authenticate with your `credentials` again, or, alternatively, you can
            rename the token files located in [MSL_IO_DIR][msl.io.constants.MSL_IO_DIR]
            to match the new `account` value.
        credentials: The path to the *client secrets* OAuth credential file. This parameter only
            needs to be specified the first time that you authenticate with a particular Google
            `account` or if you delete the token file that was created when you previously authenticated.
        scopes: The list of scopes to enable for the Google API. See
            [Drive scopes](https://developers.google.com/identity/protocols/oauth2/scopes#drive){:target="_blank"}
            for more details. If not specified, default scopes are chosen based on the value of `read_only`.
        read_only: Whether to interact with Google Drive in read-only mode.
    """
    if not scopes:
        if read_only:
            scopes = [
                "https://www.googleapis.com/auth/drive.readonly",
                "https://www.googleapis.com/auth/drive.metadata.readonly",
            ]
        else:
            scopes = [
                "https://www.googleapis.com/auth/drive",
                "https://www.googleapis.com/auth/drive.metadata",
            ]

    c = Path(os.fsdecode(credentials)) if credentials else None
    super().__init__(
        service="drive", version="v3", account=account, credentials=c, scopes=scopes, read_only=read_only
    )

    self._files: Any = self._service.files()
    self._drives: Any = self._service.drives()

copy ¤

copy(file_id, folder_id=None, name=None)

Copy a file.

Parameters:

Name Type Description Default
file_id str

The ID of a file to copy. Folders cannot be copied.

required
folder_id str | None

The ID of the destination folder. If not specified then creates a copy in the same folder that the original file is located in. To copy the file to the My Drive root folder then specify 'root' as the folder_id.

None
name str | None

The filename of the destination file.

None

Returns:

Type Description
str

The ID of the destination file.

Source code in src/msl/io/google_api.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
def copy(self, file_id: str, folder_id: str | None = None, name: str | None = None) -> str:
    """Copy a file.

    Args:
        file_id: The ID of a file to copy. Folders cannot be copied.
        folder_id: The ID of the destination folder. If not specified then creates
            a copy in the same folder that the original file is located in. To copy
            the file to the *My Drive* root folder then specify `'root'` as the `folder_id`.
        name: The filename of the destination file.

    Returns:
        The ID of the destination file.
    """
    response = self._files.copy(
        fileId=file_id,
        fields="id",
        supportsAllDrives=True,
        body={
            "name": name,
            "parents": [folder_id] if folder_id else None,
        },
    ).execute()
    return str(response["id"])

create_folder ¤

create_folder(folder, parent_id=None)

Create a folder.

Makes all intermediate-level folders needed to contain the leaf directory.

Parameters:

Name Type Description Default
folder PathLike

The folder(s) to create, for example, 'folder1' or 'folder1/folder2/folder3'.

required
parent_id str | None

The ID of the parent folder that folder is relative to. If not specified, folder is relative to the My Drive root folder. If folder is in a Shared drive then you must specify the ID of the parent folder.

None

Returns:

Type Description
str

The ID of the last (right most) folder that was created.

Source code in src/msl/io/google_api.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def create_folder(self, folder: PathLike, parent_id: str | None = None) -> str:
    """Create a folder.

    Makes all intermediate-level folders needed to contain the leaf directory.

    Args:
        folder: The folder(s) to create, for example, `'folder1'` or `'folder1/folder2/folder3'`.
        parent_id: The ID of the parent folder that `folder` is relative to. If not
            specified, `folder` is relative to the *My Drive* root folder. If `folder`
            is in a *Shared drive* then you must specify the ID of the parent folder.

    Returns:
        The ID of the last (right most) folder that was created.
    """
    names = GDrive._folder_hierarchy(folder)
    response = {"id": parent_id or "root"}
    for name in names:
        request = self._files.create(
            body={
                "name": name,
                "mimeType": GDrive.MIME_TYPE_FOLDER,
                "parents": [response["id"]],
            },
            fields="id",
            supportsAllDrives=True,
        )
        response = request.execute()
    return response["id"]

delete ¤

delete(file_or_folder_id)

Delete a file or a folder.

Files that are in read-only mode cannot be deleted.

Danger

Permanently deletes the file or folder owned by the user without moving it to the trash. If the target is a folder, then all files and sub-folders contained within the folder (that are owned by the user) are also permanently deleted.

Parameters:

Name Type Description Default
file_or_folder_id str

The ID of the file or folder to delete.

required
Source code in src/msl/io/google_api.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
def delete(self, file_or_folder_id: str) -> None:
    """Delete a file or a folder.

    Files that are in read-only mode cannot be deleted.

    !!! danger
        Permanently deletes the file or folder owned by the user without
        moving it to the trash. If the target is a folder, then all files
        and sub-folders contained within the folder (that are owned by the
        user) are also permanently deleted.

    Args:
        file_or_folder_id: The ID of the file or folder to delete.
    """
    if self.is_read_only(file_or_folder_id):
        # The API allows for a file to be deleted if it is in read-only mode,
        # but we will not allow it to be deleted
        msg = "Cannot delete the file since it is in read-only mode"
        raise RuntimeError(msg)

    self._files.delete(
        fileId=file_or_folder_id,
        supportsAllDrives=True,
    ).execute()

download ¤

download(
    file_id,
    *,
    save_to=None,
    num_retries=0,
    chunk_size=DEFAULT_CHUNK_SIZE,
    callback=None,
)

Download a file.

Parameters:

Name Type Description Default
file_id str

The ID of the file to download.

required
save_to PathLike | FileLikeWrite[bytes] | None

The location to save the file to. If a directory is specified, the directory must already exist and the file will be saved to that directory using the filename of the remote file. To save the file with a new filename, also specify the new filename. Default is to save the file to the current working directory using the remote filename.

None
num_retries int

The number of times to retry the download. If zero (default) then attempt the request only once.

0
chunk_size int

The file will be downloaded in chunks of this many bytes.

DEFAULT_CHUNK_SIZE
callback Callable[[MediaDownloadProgress], None] | None

The callback function to call after each chunk of the file is downloaded. The callback accepts one positional argument that is of type MediaDownloadProgress.

None
Source code in src/msl/io/google_api.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def download(
    self,
    file_id: str,
    *,
    save_to: PathLike | FileLikeWrite[bytes] | None = None,
    num_retries: int = 0,
    chunk_size: int = DEFAULT_CHUNK_SIZE,
    callback: Callable[[MediaDownloadProgress], None] | None = None,
) -> None:
    """Download a file.

    Args:
        file_id: The ID of the file to download.
        save_to: The location to save the file to. If a directory is specified, the directory
            must already exist and the file will be saved to that directory using the filename
            of the remote file. To save the file with a new filename, also specify the new filename.
            Default is to save the file to the current working directory using the remote filename.
        num_retries: The number of times to retry the download.
            If zero (default) then attempt the request only once.
        chunk_size: The file will be downloaded in chunks of this many bytes.
        callback: The callback function to call after each chunk of the file is downloaded.
            The `callback` accepts one positional argument that is of type
            [MediaDownloadProgress][msl.io.types.MediaDownloadProgress].
    """
    response = self._files.get(
        fileId=file_id,
        fields="name",
        supportsAllDrives=True,
    ).execute()
    filename: str = response["name"]

    file: BufferedWriter | FileLikeWrite[bytes]
    if save_to is None:
        file = Path(filename).open("wb")  # noqa: SIM115
    elif isinstance(save_to, (str, bytes, os.PathLike)):
        path = Path(os.fsdecode(save_to))
        file = (path / filename).open("wb") if path.is_dir() else path.open("wb")
    else:
        file = save_to

    request = self._files.get_media(fileId=file_id, supportsAllDrives=True)
    downloader = MediaIoBaseDownload(file, request, chunksize=chunk_size)  # pyright: ignore[reportPossiblyUnboundVariable,reportUnknownVariableType]
    done = False
    while not done:
        status, done = downloader.next_chunk(num_retries=num_retries)  # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
        if callback:
            callback(status)  # pyright: ignore[reportUnknownArgumentType]

    if file is not save_to:
        file.close()

empty_trash ¤

empty_trash()

Permanently delete all files in the trash.

Source code in src/msl/io/google_api.py
412
413
414
def empty_trash(self) -> None:
    """Permanently delete all files in the trash."""
    self._files.emptyTrash().execute()

file_id ¤

file_id(file, *, mime_type=None, folder_id=None)

Get the ID of a Google Drive file.

Parameters:

Name Type Description Default
file PathLike

The path to a Google Drive file.

required
mime_type str | None

The Drive MIME type or media type to use to filter the results.

None
folder_id str | None

The ID of the folder that file is relative to. If not specified, file is relative to the My Drive root folder. If file is in a Shared drive then you must specify the ID of the parent folder.

None

Returns:

Type Description
str

The file ID.

Source code in src/msl/io/google_api.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def file_id(self, file: PathLike, *, mime_type: str | None = None, folder_id: str | None = None) -> str:
    """Get the ID of a Google Drive file.

    Args:
        file: The path to a Google Drive file.
        mime_type: The [Drive MIME type](https://developers.google.com/drive/api/guides/mime-types){:target="_blank"}
            or [media type](https://www.iana.org/assignments/media-types/media-types.xhtml){:target="_blank"} to use
            to filter the results.
        folder_id: The ID of the folder that `file` is relative to. If not specified, `file`
            is relative to the *My Drive* root folder. If `file` is in a *Shared drive* then
            you must specify the ID of the parent folder.

    Returns:
        The file ID.
    """
    folders, name = os.path.split(os.fsdecode(file))
    folder_id = self.folder_id(folders, parent_id=folder_id)

    q = f'"{folder_id}" in parents and name="{name}" and trashed=false'
    if not mime_type:
        q += f' and mimeType!="{GDrive.MIME_TYPE_FOLDER}"'
    else:
        q += f' and mimeType="{mime_type}"'

    response = self._files.list(
        q=q,
        fields="files(id,name,mimeType)",
        includeItemsFromAllDrives=True,
        supportsAllDrives=True,
    ).execute()
    files = response["files"]
    if not files:
        msg = f"Not a valid Google Drive file {file!r}"
        raise OSError(msg)
    if len(files) > 1:
        mime_types = "\n  ".join(f["mimeType"] for f in files)
        msg = f"Multiple files exist for {file!r}. Filter by MIME type:\n  {mime_types}"
        raise OSError(msg)

    first = files[0]
    assert name == first["name"], "{name!r} != {first['name']!r}"  # noqa: S101
    return str(first["id"])

folder_id ¤

folder_id(folder, *, parent_id=None)

Get the ID of a Google Drive folder.

Parameters:

Name Type Description Default
folder PathLike

The path to a Google Drive file.

required
parent_id str | None

The ID of the parent folder that folder is relative to. If not specified, folder is relative to the My Drive root folder. If folder is in a Shared drive then you must specify the ID of the parent folder.

None

Returns:

Type Description
str

The folder ID.

Source code in src/msl/io/google_api.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def folder_id(self, folder: PathLike, *, parent_id: str | None = None) -> str:
    """Get the ID of a Google Drive folder.

    Args:
        folder: The path to a Google Drive file.
        parent_id: The ID of the parent folder that `folder` is relative to. If not
            specified, `folder` is relative to the *My Drive* root folder. If `folder`
            is in a *Shared drive* then you must specify the ID of the parent folder.

    Returns:
        The folder ID.
    """
    folder_id = parent_id or "root"
    folder = os.fsdecode(folder)
    names = GDrive._folder_hierarchy(folder)
    for name in names:
        q = f'"{folder_id}" in parents and name="{name}" and trashed=false and mimeType="{GDrive.MIME_TYPE_FOLDER}"'
        response = self._files.list(
            q=q,
            fields="files(id,name)",
            includeItemsFromAllDrives=True,
            supportsAllDrives=True,
        ).execute()
        files = response["files"]
        if not files:
            msg = f"Not a valid Google Drive folder {folder!r}"
            raise OSError(msg)
        if len(files) > 1:
            matches = "\n  ".join(str(file) for file in files)
            msg = f"Multiple folders exist for {name!r}\n  {matches}"
            raise OSError(msg)

        first = files[0]
        assert name == first["name"], f"{name!r} != {first['name']!r}"  # noqa: S101
        folder_id = first["id"]

    return folder_id

is_file ¤

is_file(file, *, mime_type=None, folder_id=None)

Check if a file exists.

Parameters:

Name Type Description Default
file PathLike

The path to a Google Drive file.

required
mime_type str | None

The Drive MIME type or media type to use to filter the results.

None
folder_id str | None

The ID of the folder that file is relative to. If not specified, file is relative to the My Drive root folder. If file is in a Shared drive then you must specify the ID of the parent folder.

None

Returns:

Type Description
bool

Whether the file exists.

Source code in src/msl/io/google_api.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def is_file(self, file: PathLike, *, mime_type: str | None = None, folder_id: str | None = None) -> bool:
    """Check if a file exists.

    Args:
        file: The path to a Google Drive file.
        mime_type: The [Drive MIME type](https://developers.google.com/drive/api/guides/mime-types){target="_blank"}
            or [media type](https://www.iana.org/assignments/media-types/media-types.xhtml){target="_blank"} to use
            to filter the results.
        folder_id: The ID of the folder that `file` is relative to. If not specified, `file`
            is relative to the *My Drive* root folder. If `file` is in a *Shared drive* then
            you must specify the ID of the parent folder.

    Returns:
        Whether the file exists.
    """
    try:
        _ = self.file_id(file, mime_type=mime_type, folder_id=folder_id)
    except OSError as err:
        return str(err).startswith("Multiple files")
    else:
        return True

is_folder ¤

is_folder(folder, parent_id=None)

Check if a folder exists.

Parameters:

Name Type Description Default
folder PathLike

The path to a Google Drive folder.

required
parent_id str | None

The ID of the parent folder that folder is relative to. If not specified, folder is relative to the My Drive root folder. If folder is in a Shared drive then you must specify the ID of the parent folder.

None

Returns:

Type Description
bool

Whether the folder exists.

Source code in src/msl/io/google_api.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
def is_folder(self, folder: PathLike, parent_id: str | None = None) -> bool:
    """Check if a folder exists.

    Args:
        folder: The path to a Google Drive folder.
        parent_id: The ID of the parent folder that `folder` is relative to. If not
            specified, `folder` is relative to the *My Drive* root folder. If `folder`
            is in a *Shared drive* then you must specify the ID of the parent folder.

    Returns:
        Whether the folder exists.
    """
    try:
        _ = self.folder_id(folder, parent_id=parent_id)
    except OSError as err:
        return str(err).startswith("Multiple folders")
    else:
        return True

is_read_only ¤

is_read_only(file_or_folder_id)

Returns whether the file or folder is accessed in read-only mode.

Parameters:

Name Type Description Default
file_or_folder_id str

The ID of a file or folder.

required

Returns:

Type Description
bool

Whether the file or folder is accessed in read-only mode.

Source code in src/msl/io/google_api.py
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
def is_read_only(self, file_or_folder_id: str) -> bool:
    """Returns whether the file or folder is accessed in read-only mode.

    Args:
        file_or_folder_id: The ID of a file or folder.

    Returns:
        Whether the file or folder is accessed in read-only mode.
    """
    response = self._files.get(
        fileId=file_or_folder_id,
        supportsAllDrives=True,
        fields="contentRestrictions",
    ).execute()
    restrictions = response.get("contentRestrictions")
    if not restrictions:
        return False
    r: bool = restrictions[0]["readOnly"]
    return r

move ¤

move(source_id, destination_id)

Move a file or a folder.

When moving a file or folder between My Drive and a Shared drive the access permissions will change.

Moving a file or folder does not change its ID, only the ID of its parent changes (i.e., source_id will remain the same after the move).

Parameters:

Name Type Description Default
source_id str

The ID of a file or folder to move.

required
destination_id str

The ID of the destination folder. To move the file or folder to the My Drive root folder then specify 'root' as the destination_id.

required
Source code in src/msl/io/google_api.py
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
def move(self, source_id: str, destination_id: str) -> None:
    """Move a file or a folder.

    When moving a file or folder between *My Drive* and a *Shared drive*
    the access permissions will change.

    Moving a file or folder does not change its ID, only the ID of
    its *parent* changes (i.e., `source_id` will remain the same
    after the move).

    Args:
        source_id: The ID of a file or folder to move.
        destination_id: The ID of the destination folder. To move the file or folder to the
            *My Drive* root folder then specify `'root'` as the `destination_id`.
    """
    params = {"fileId": source_id, "supportsAllDrives": True}
    try:
        self._files.update(addParents=destination_id, **params).execute()
    except HttpError as e:  # pyright: ignore[reportPossiblyUnboundVariable,reportUnknownVariableType]
        if "exactly one parent" not in str(e):  # pyright: ignore[reportUnknownArgumentType]
            raise

        # Handle the following error:
        #   A shared drive item must have exactly one parent
        response = self._files.get(fields="parents", **params).execute()
        self._files.update(
            addParents=destination_id, removeParents=",".join(response["parents"]), **params
        ).execute()

path ¤

path(file_or_folder_id)

Convert an ID to a path.

Parameters:

Name Type Description Default
file_or_folder_id str

The ID of a file or folder.

required

Returns:

Type Description
str

The corresponding path of the ID.

Source code in src/msl/io/google_api.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
def path(self, file_or_folder_id: str) -> str:
    """Convert an ID to a path.

    Args:
        file_or_folder_id: The ID of a file or folder.

    Returns:
        The corresponding path of the ID.
    """
    names: list[str] = []
    while True:
        request = self._files.get(
            fileId=file_or_folder_id,
            fields="name,parents",
            supportsAllDrives=True,
        )
        response = request.execute()
        names.append(response["name"])
        parents = response.get("parents", [])
        if not parents:
            break
        if len(parents) > 1:
            msg = "Multiple parents exist. This case has not been handled yet. Contact developers."
            raise OSError(msg)
        file_or_folder_id = response["parents"][0]
    return "/".join(names[::-1])

read_only ¤

read_only(file_id, read_only, reason=None)

Set a file to be in read-only or read-write mode.

Parameters:

Name Type Description Default
file_id str

The ID of a file.

required
read_only bool

Whether to set the file to be in read-only mode.

required
reason str | None

The reason for putting the file in read-only mode. Only used if read_only is True.

None
Source code in src/msl/io/google_api.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
def read_only(self, file_id: str, read_only: bool, reason: str | None = None) -> None:  # noqa: FBT001
    """Set a file to be in read-only or read-write mode.

    Args:
        file_id: The ID of a file.
        read_only: Whether to set the file to be in read-only mode.
        reason: The reason for putting the file in read-only mode.
            Only used if `read_only` is `True`.
    """
    restrictions: dict[str, bool | str] = {"readOnly": read_only}
    if read_only:
        restrictions["reason"] = reason or ""

        # If `file_id` is already in read-only mode, and it is being set
        # to read-only mode then the API raises a TimeoutError waiting for
        # a response. To avoid this error, check the mode and if it is
        # already in read-only mode we are done.
        if self.is_read_only(file_id):
            return

    self._files.update(
        fileId=file_id, supportsAllDrives=True, body={"contentRestrictions": [restrictions]}
    ).execute()

rename ¤

rename(file_or_folder_id, new_name)

Rename a file or folder.

Renaming a file or folder does not change its ID.

Parameters:

Name Type Description Default
file_or_folder_id str

The ID of a file or folder.

required
new_name str

The new name of the file or folder.

required
Source code in src/msl/io/google_api.py
607
608
609
610
611
612
613
614
615
616
617
618
619
620
def rename(self, file_or_folder_id: str, new_name: str) -> None:
    """Rename a file or folder.

    Renaming a file or folder does not change its ID.

    Args:
        file_or_folder_id: The ID of a file or folder.
        new_name: The new name of the file or folder.
    """
    self._files.update(
        fileId=file_or_folder_id,
        supportsAllDrives=True,
        body={"name": new_name},
    ).execute()

shared_drives ¤

shared_drives()

Returns the IDs and names of all Shared drives.

Returns:

Type Description
dict[str, str]

The keys are the IDs of the shared drives and the values are the names of the shared drives.

Source code in src/msl/io/google_api.py
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def shared_drives(self) -> dict[str, str]:
    """Returns the IDs and names of all `Shared drives`.

    Returns:
        The keys are the IDs of the shared drives and the values are the names of the shared drives.
    """
    drives: dict[str, str] = {}
    next_page_token = ""
    while True:
        response = self._drives.list(pageSize=100, pageToken=next_page_token).execute()
        drives.update({d["id"]: d["name"] for d in response["drives"]})
        next_page_token = response.get("nextPageToken")
        if not next_page_token:
            break
    return drives

upload ¤

upload(
    file,
    *,
    folder_id=None,
    mime_type=None,
    resumable=False,
    chunk_size=DEFAULT_CHUNK_SIZE,
)

Upload a file.

Parameters:

Name Type Description Default
file PathLike

The file to upload.

required
folder_id str | None

The ID of the folder to upload the file to. If not specified, uploads to the My Drive root folder.

None
mime_type str | None

The Drive MIME type or media type of the file (e.g., 'text/csv'). If not specified then a type will be guessed based on the file extension.

None
resumable bool

Whether the upload can be resumed.

False
chunk_size int

The file will be uploaded in chunks of this many bytes. Only used if resumable is True. Specify a value of -1 if the file is to be uploaded in a single chunk. Note that Google App Engine has a 5MB limit per request size, so you should not set chunk_size to be > 5MB or to the value -1 if the file size is > 5MB.

DEFAULT_CHUNK_SIZE

Returns:

Type Description
str

The ID of the file that was uploaded.

Source code in src/msl/io/google_api.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def upload(
    self,
    file: PathLike,
    *,
    folder_id: str | None = None,
    mime_type: str | None = None,
    resumable: bool = False,
    chunk_size: int = DEFAULT_CHUNK_SIZE,
) -> str:
    """Upload a file.

    Args:
        file: The file to upload.
        folder_id: The ID of the folder to upload the file to. If not specified,
            uploads to the *My Drive* root folder.
        mime_type: The [Drive MIME type](https://developers.google.com/drive/api/guides/mime-types){:target="_blank"}
            or [media type](https://www.iana.org/assignments/media-types/media-types.xhtml){:target="_blank"} of the
            file (e.g., `'text/csv'`). If not specified then a type will be guessed based on the file extension.
        resumable: Whether the upload can be resumed.
        chunk_size: The file will be uploaded in chunks of this many bytes. Only used
            if `resumable` is `True`. Specify a value of -1 if the file is to be uploaded
            in a single chunk. Note that Google App Engine has a 5MB limit per request size,
            so you should not set `chunk_size` to be > 5MB or to the value `-1` if the file size is > 5MB.

    Returns:
        The ID of the file that was uploaded.
    """
    parent_id = folder_id or "root"
    file = os.fsdecode(file)
    filename = Path(file).name

    body = {"name": filename, "parents": [parent_id]}
    if mime_type:
        body["mimeType"] = mime_type

    request = self._files.create(
        body=body,
        media_body=MediaFileUpload(file, mimetype=mime_type, chunksize=chunk_size, resumable=resumable),  # pyright: ignore[reportPossiblyUnboundVariable]
        fields="id",
        supportsAllDrives=True,
    )
    response = request.execute()
    return str(response["id"])

GMail ¤

GMail(account=None, credentials=None, scopes=None)

Bases: GoogleAPI

Interact with Gmail.

Info

You must follow the instructions in the prerequisites section for setting up the Gmail API before you can use this class. It is also useful to be aware of the refresh token expiration policy.

Parameters:

Name Type Description Default
account str | None

Since a person may have multiple Google accounts, and multiple people may run the same code, this parameter decides which token to load to authenticate with the Google API. The value can be any text (or None) that you want to associate with a particular Google account, provided that it contains valid characters for a filename. The value that you chose when you authenticated with your credentials should be used for all future instances of this class to access that particular Google account. You can associate a different value with a Google account at any time (by passing in a different account value), but you may be asked to authenticate with your credentials again, or, alternatively, you can rename the token files located in MSL_IO_DIR to match the new account value.

None
credentials PathLike | None

The path to the client secrets OAuth credential file. This parameter only needs to be specified the first time that you authenticate with a particular Google account or if you delete the token file that was created when you previously authenticated.

None
scopes list[str] | None

The list of scopes to enable for the Google API. See Gmail scopes for more details. If not specified then default scopes are chosen.

None
Source code in src/msl/io/google_api.py
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
def __init__(
    self,
    account: str | None = None,
    credentials: PathLike | None = None,
    scopes: list[str] | None = None,
) -> None:
    """Interact with Gmail.

    !!! info
        You must follow the instructions in the prerequisites section for setting up the
        [Gmail API](https://developers.google.com/gmail/api/quickstart/python#prerequisites){:target="_blank"}
        before you can use this class. It is also useful to be aware of the
        [refresh token expiration](https://developers.google.com/identity/protocols/oauth2#expiration){:target="_blank"}
        policy.

    Args:
        account: Since a person may have multiple Google accounts, and multiple people
            may run the same code, this parameter decides which token to load to
            authenticate with the Google API. The value can be any text (or `None`)
            that you want to associate with a particular Google account, provided that
            it contains valid characters for a filename. The value that you chose when
            you authenticated with your `credentials` should be used for all future
            instances of this class to access that particular Google account. You can
            associate a different value with a Google account at any time (by passing
            in a different `account` value), but you may be asked to authenticate with
            your `credentials` again, or, alternatively, you can rename the token files
            located in [MSL_IO_DIR][msl.io.constants.MSL_IO_DIR] to match the new
            `account` value.
        credentials: The path to the *client secrets* OAuth credential file. This
            parameter only needs to be specified the first time that you
            authenticate with a particular Google account or if you delete
            the token file that was created when you previously authenticated.
        scopes: The list of scopes to enable for the Google API. See
            [Gmail scopes](https://developers.google.com/identity/protocols/oauth2/scopes#gmail){:target="_blank"}
            for more details. If not specified then default scopes are chosen.
    """
    if not scopes:
        scopes = ["https://www.googleapis.com/auth/gmail.send", "https://www.googleapis.com/auth/gmail.metadata"]

    c = Path(os.fsdecode(credentials)) if credentials else None
    super().__init__(service="gmail", version="v1", account=account, credentials=c, scopes=scopes, read_only=False)

    self._my_email_address: str | None = None
    self._users: Any = self._service.users()

profile ¤

profile()

Gets the authenticated user's GMail profile.

Returns:

Type Description
Profile

The current users GMail profile.

Source code in src/msl/io/google_api.py
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
def profile(self) -> Profile:
    """Gets the authenticated user's GMail profile.

    Returns:
        The current users GMail profile.
    """
    profile = self._users.getProfile(userId="me").execute()
    return Profile(
        email_address=str(profile["emailAddress"]),
        messages_total=int(profile["messagesTotal"]),
        threads_total=int(profile["threadsTotal"]),
        history_id=str(profile["historyId"]),
    )

send ¤

send(recipients, sender='me', subject=None, body=None)

Send an email.

See also send_email.

Parameters:

Name Type Description Default
recipients str | MutableSequence[str]

The email address(es) of the recipient(s). The value 'me' can be used to indicate the authenticated user.

required
sender str

The email address of the sender. The value 'me' can be used to indicate the authenticated user.

'me'
subject str | None

The text to include in the subject field.

None
body str | None

The text to include in the body of the email. The text can be enclosed in <html></html> tags to use HTML elements to format the message.

None
Source code in src/msl/io/google_api.py
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
def send(
    self,
    recipients: str | MutableSequence[str],
    sender: str = "me",
    subject: str | None = None,
    body: str | None = None,
) -> None:
    """Send an email.

    !!! note "See also [send_email][msl.io.utils.send_email]."

    Args:
        recipients: The email address(es) of the recipient(s). The value `'me'` can be used
            to indicate the authenticated user.
        sender: The email address of the sender. The value `'me'` can be used to indicate
            the authenticated user.
        subject: The text to include in the subject field.
        body: The text to include in the body of the email. The text can be enclosed in
            `<html></html>` tags to use HTML elements to format the message.
    """
    from base64 import b64encode  # noqa: PLC0415
    from email.mime.multipart import MIMEMultipart  # noqa: PLC0415
    from email.mime.text import MIMEText  # noqa: PLC0415

    if isinstance(recipients, str):
        recipients = [recipients]

    for i in range(len(recipients)):
        if recipients[i] == "me":
            if self._my_email_address is None:
                self._my_email_address = str(self.profile()["email_address"])
            recipients[i] = self._my_email_address

    msg = MIMEMultipart()
    msg["From"] = sender
    msg["To"] = ", ".join(recipients)
    msg["Subject"] = subject or "(no subject)"

    text = body or ""
    subtype = "html" if text.startswith("<html>") else "plain"
    msg.attach(MIMEText(text, subtype))

    self._users.messages().send(userId=sender, body={"raw": b64encode(msg.as_bytes()).decode()}).execute()

GSheets ¤

GSheets(
    *,
    account=None,
    credentials=None,
    scopes=None,
    read_only=True,
)

Bases: GoogleAPI

Interact with Google Sheets.

Info

You must follow the instructions in the prerequisites section for setting up the Sheets API before you can use this class. It is also useful to be aware of the refresh token expiration policy.

Parameters:

Name Type Description Default
account str | None

Since a person may have multiple Google accounts, and multiple people may run the same code, this parameter decides which token to load to authenticate with the Google API. The value can be any text (or None) that you want to associate with a particular Google account, provided that it contains valid characters for a filename. The value that you chose when you authenticated with your credentials should be used for all future instances of this class to access that particular Google account. You can associate a different value with a Google account at any time (by passing in a different account value), but you may be asked to authenticate with your credentials again, or, alternatively, you can rename the token files located in MSL_IO_DIR to match the new account value.

None
credentials PathLike | None

The path to the client secrets OAuth credential file. This parameter only needs to be specified the first time that you authenticate with a particular Google account or if you delete the token file that was created when you previously authenticated.

None
scopes list[str] | None

The list of scopes to enable for the Google API. See Sheets scopes for more details. If not specified, default scopes are chosen based on the value of read_only.

None
read_only bool

Whether to interact with Google Sheets in read-only mode.

True
Source code in src/msl/io/google_api.py
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
def __init__(
    self,
    *,
    account: str | None = None,
    credentials: PathLike | None = None,
    scopes: list[str] | None = None,
    read_only: bool = True,
) -> None:
    """Interact with Google Sheets.

    !!! info
        You must follow the instructions in the prerequisites section for setting up the
        [Sheets API](https://developers.google.com/sheets/api/quickstart/python#prerequisites){:target="_blank"}
        before you can use this class. It is also useful to be aware of the
        [refresh token expiration](https://developers.google.com/identity/protocols/oauth2#expiration){:target="_blank"}
        policy.

    Args:
        account: Since a person may have multiple Google accounts, and multiple people
            may run the same code, this parameter decides which token to load
            to authenticate with the Google API. The value can be any text (or
            `None`) that you want to associate with a particular Google
            account, provided that it contains valid characters for a filename.
            The value that you chose when you authenticated with your `credentials`
            should be used for all future instances of this class to access that
            particular Google account. You can associate a different value with
            a Google account at any time (by passing in a different `account`
            value), but you may be asked to authenticate with your `credentials`
            again, or, alternatively, you can rename the token files located in
            [MSL_IO_DIR][msl.io.constants.MSL_IO_DIR] to match the new `account` value.
        credentials: The path to the *client secrets* OAuth credential file. This
            parameter only needs to be specified the first time that you
            authenticate with a particular Google account or if you delete
            the token file that was created when you previously authenticated.
        scopes: The list of scopes to enable for the Google API. See
            [Sheets scopes](https://developers.google.com/identity/protocols/oauth2/scopes#sheets){:target="_blank"}
            for more details. If not specified, default scopes are chosen based on the value of `read_only`.
        read_only: Whether to interact with Google Sheets in read-only mode.
    """
    if not scopes:
        if read_only:
            scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
        else:
            scopes = ["https://www.googleapis.com/auth/spreadsheets"]

    c = Path(os.fsdecode(credentials)) if credentials else None
    super().__init__(
        service="sheets", version="v4", account=account, credentials=c, scopes=scopes, read_only=read_only
    )

    self._spreadsheets: Any = self._service.spreadsheets()

add_sheets ¤

add_sheets(names, spreadsheet_id)

Add sheets to a spreadsheet.

Parameters:

Name Type Description Default
names str | Iterable[str]

The name(s) of the new sheet(s) to add.

required
spreadsheet_id str

The ID of the spreadsheet to add the sheet(s) to.

required

Returns:

Type Description
dict[int, str]

The keys are the IDs of the new sheets and the values are the names.

Source code in src/msl/io/google_api.py
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
def add_sheets(self, names: str | Iterable[str], spreadsheet_id: str) -> dict[int, str]:
    """Add sheets to a spreadsheet.

    Args:
        names: The name(s) of the new sheet(s) to add.
        spreadsheet_id: The ID of the spreadsheet to add the sheet(s) to.

    Returns:
        The keys are the IDs of the new sheets and the values are the names.
    """
    if isinstance(names, str):
        names = [names]

    response = self._spreadsheets.batchUpdate(
        spreadsheetId=spreadsheet_id,
        body={"requests": [{"addSheet": {"properties": {"title": name}}} for name in names]},
    ).execute()

    return {
        r["addSheet"]["properties"]["sheetId"]: r["addSheet"]["properties"]["title"] for r in response["replies"]
    }

append ¤

append(
    values,
    spreadsheet_id,
    cell=None,
    sheet=None,
    *,
    row_major=True,
    raw=False,
)

Append values to a sheet.

Parameters:

Name Type Description Default
values Any | list[Any] | tuple[Any, ...] | list[list[Any]] | tuple[tuple[Any, ...], ...]

The value(s) to append

required
spreadsheet_id str

The ID of a Google Sheets file.

required
cell str | None

The cell (top-left corner) to start appending the values to. If the cell already contains data then new rows are inserted and the values are written to the new rows. For example, 'D100'.

None
sheet str | None

The name of a sheet in the spreadsheet to append the values to. If not specified and only one sheet exists in the spreadsheet then automatically determines the sheet name; however, it is more efficient to specify the name of the sheet.

None
row_major bool

Whether to append the values in row-major or column-major order.

True
raw bool

Determines how the values should be interpreted. If True, the values will not be parsed and will be stored as-is. If False, the values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI.

False
Source code in src/msl/io/google_api.py
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
def append(  # noqa: PLR0913
    self,
    values: Any | list[Any] | tuple[Any, ...] | list[list[Any]] | tuple[tuple[Any, ...], ...],
    spreadsheet_id: str,
    cell: str | None = None,
    sheet: str | None = None,
    *,
    row_major: bool = True,
    raw: bool = False,
) -> None:
    """Append values to a sheet.

    Args:
        values: The value(s) to append
        spreadsheet_id: The ID of a Google Sheets file.
        cell: The cell (top-left corner) to start appending the values to. If the
            cell already contains data then new rows are inserted and the values
            are written to the new rows. For example, `'D100'`.
        sheet: The name of a sheet in the spreadsheet to append the values to.
            If not specified and only one sheet exists in the spreadsheet
            then automatically determines the sheet name; however, it is
            more efficient to specify the name of the sheet.
        row_major: Whether to append the values in row-major or column-major order.
        raw: Determines how the values should be interpreted. If `True`,
            the values will not be parsed and will be stored as-is. If
            `False`, the values will be parsed as if the user typed
            them into the UI. Numbers will stay as numbers, but strings may
            be converted to numbers, dates, etc. following the same rules
            that are applied when entering text into a cell via the Google
            Sheets UI.
    """
    self._spreadsheets.values().append(
        spreadsheetId=spreadsheet_id,
        range=self._get_range(sheet, cell, spreadsheet_id),
        valueInputOption="RAW" if raw else "USER_ENTERED",
        insertDataOption="INSERT_ROWS",
        body={
            "values": self._values(values),
            "majorDimension": "ROWS" if row_major else "COLUMNS",
        },
    ).execute()

cells ¤

cells(spreadsheet_id, ranges=None)

Return cells from a spreadsheet.

Parameters:

Name Type Description Default
spreadsheet_id str

The ID of a Google Sheets file.

required
ranges str | list[str] | None

The ranges to retrieve from the spreadsheet. If not specified then return all cells from all sheets. For example,

  • 'Sheet1' → Return all cells from the sheet named Sheet1
  • 'Sheet1!A1:H5' → Return cells A1:H5 from the sheet named Sheet1
  • ['Sheet1!A1:H5', 'Data', 'Devices!B4:B9'] → Return cells A1:H5 from the sheet named Sheet1, all cells from the sheet named Data and cells B4:B9 from the sheet named Devices
None

Returns:

Type Description
dict[str, list[list[GCell]]]

The cells from the spreadsheet. The keys are the names of the sheets.

Source code in src/msl/io/google_api.py
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
def cells(self, spreadsheet_id: str, ranges: str | list[str] | None = None) -> dict[str, list[list[GCell]]]:  # noqa: C901
    """Return cells from a spreadsheet.

    Args:
        spreadsheet_id: The ID of a Google Sheets file.
        ranges: The ranges to retrieve from the spreadsheet. If not specified then return all cells
            from all sheets. For example,

            * `'Sheet1'` &#8594; Return all cells from the sheet named `Sheet1`
            * `'Sheet1!A1:H5'` &#8594; Return cells `A1:H5` from the sheet named `Sheet1`
            * `['Sheet1!A1:H5', 'Data', 'Devices!B4:B9']` &#8594; Return cells `A1:H5`
                from the sheet named `Sheet1`, all cells from the sheet named `Data`
                and cells `B4:B9` from the sheet named Devices

    Returns:
        The cells from the spreadsheet. The keys are the names of the sheets.
    """
    response = self._spreadsheets.get(
        spreadsheetId=spreadsheet_id,
        includeGridData=True,
        ranges=ranges,
    ).execute()
    cells: dict[str, list[list[GCell]]] = {}
    for sheet in response["sheets"]:
        data: list[list[GCell]] = []
        for item in sheet["data"]:
            for row in item.get("rowData", []):
                row_data: list[GCell] = []
                for col in row.get("values", []):
                    effective_value = col.get("effectiveValue", None)
                    formatted = col.get("formattedValue", "")
                    if effective_value is None:
                        value = None
                        typ = GCellType.EMPTY
                    elif "numberValue" in effective_value:
                        value = effective_value["numberValue"]
                        t = col.get("effectiveFormat", {}).get("numberFormat", {}).get("type", "NUMBER")
                        try:
                            typ = GCellType(t)
                        except ValueError:
                            typ = GCellType.UNKNOWN
                    elif "stringValue" in effective_value:
                        value = effective_value["stringValue"]
                        typ = GCellType.STRING
                    elif "boolValue" in effective_value:
                        value = effective_value["boolValue"]
                        typ = GCellType.BOOLEAN
                    elif "errorValue" in effective_value:
                        msg = effective_value["errorValue"]["message"]
                        value = f"{col['formattedValue']} ({msg})"
                        typ = GCellType.ERROR
                    else:
                        value = formatted
                        typ = GCellType.UNKNOWN
                    row_data.append(GCell(value=value, type=typ, formatted=formatted))
                data.append(row_data)
            cells[sheet["properties"]["title"]] = data
    return cells

copy ¤

copy(
    name_or_id, spreadsheet_id, destination_spreadsheet_id
)

Copy a sheet from one spreadsheet to another spreadsheet.

Parameters:

Name Type Description Default
name_or_id str | int

The name or ID of the sheet to copy.

required
spreadsheet_id str

The ID of the spreadsheet that contains the sheet.

required
destination_spreadsheet_id str

The ID of a spreadsheet to copy the sheet to.

required

Returns:

Type Description
int

The ID of the sheet in the destination spreadsheet.

Source code in src/msl/io/google_api.py
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
def copy(self, name_or_id: str | int, spreadsheet_id: str, destination_spreadsheet_id: str) -> int:
    """Copy a sheet from one spreadsheet to another spreadsheet.

    Args:
        name_or_id: The name or ID of the sheet to copy.
        spreadsheet_id: The ID of the spreadsheet that contains the sheet.
        destination_spreadsheet_id: The ID of a spreadsheet to copy the sheet to.

    Returns:
        The ID of the sheet in the destination spreadsheet.
    """
    sheet_id = name_or_id if isinstance(name_or_id, int) else self.sheet_id(name_or_id, spreadsheet_id)

    response = (
        self._spreadsheets.sheets()
        .copyTo(
            spreadsheetId=spreadsheet_id,
            sheetId=sheet_id,
            body={
                "destination_spreadsheet_id": destination_spreadsheet_id,
            },
        )
        .execute()
    )
    return int(response["sheetId"])

create ¤

create(name, sheet_names=None)

Create a new spreadsheet.

The spreadsheet will be created in the My Drive root folder. To move it to a different folder use GDrive.create_folder and/or GDrive.move.

Parameters:

Name Type Description Default
name str

The name of the spreadsheet.

required
sheet_names Iterable[str] | None

The names of the sheets that will be in the new spreadsheet.

None

Returns:

Type Description
str

The ID of the spreadsheet that was created.

Source code in src/msl/io/google_api.py
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
def create(self, name: str, sheet_names: Iterable[str] | None = None) -> str:
    """Create a new spreadsheet.

    The spreadsheet will be created in the *My Drive* root folder.
    To move it to a different folder use [GDrive.create_folder][msl.io.google_api.GDrive.create_folder]
    and/or [GDrive.move][msl.io.google_api.GDrive.move].

    Args:
        name: The name of the spreadsheet.
        sheet_names: The names of the sheets that will be in the new spreadsheet.

    Returns:
        The ID of the spreadsheet that was created.
    """
    body: dict[str, dict[str, str] | list[dict[str, dict[str, str]]]] = {"properties": {"title": name}}
    if sheet_names:
        body["sheets"] = [{"properties": {"title": sn}} for sn in sheet_names]
    response = self._spreadsheets.create(body=body).execute()
    return str(response["spreadsheetId"])

delete_sheets ¤

delete_sheets(names_or_ids, spreadsheet_id)

Delete sheets from a spreadsheet.

Parameters:

Name Type Description Default
names_or_ids str | int | Iterable[str | int]

The name(s) or ID(s) of the sheet(s) to delete.

required
spreadsheet_id str

The ID of the spreadsheet to delete the sheet(s) from.

required
Source code in src/msl/io/google_api.py
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
def delete_sheets(self, names_or_ids: str | int | Iterable[str | int], spreadsheet_id: str) -> None:
    """Delete sheets from a spreadsheet.

    Args:
        names_or_ids: The name(s) or ID(s) of the sheet(s) to delete.
        spreadsheet_id: The ID of the spreadsheet to delete the sheet(s) from.
    """
    if isinstance(names_or_ids, (str, int)):
        names_or_ids = [names_or_ids]

    self._spreadsheets.batchUpdate(
        spreadsheetId=spreadsheet_id,
        body={
            "requests": [
                {"deleteSheet": {"sheetId": n if isinstance(n, int) else self.sheet_id(n, spreadsheet_id)}}
                for n in names_or_ids
            ]
        },
    ).execute()

rename_sheet ¤

rename_sheet(name_or_id, new_name, spreadsheet_id)

Rename a sheet.

Parameters:

Name Type Description Default
name_or_id str | int

The name or ID of the sheet to rename.

required
new_name str

The new name of the sheet.

required
spreadsheet_id str

The ID of the spreadsheet that contains the sheet.

required
Source code in src/msl/io/google_api.py
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
def rename_sheet(self, name_or_id: str | int, new_name: str, spreadsheet_id: str) -> None:
    """Rename a sheet.

    Args:
        name_or_id: The name or ID of the sheet to rename.
        new_name: The new name of the sheet.
        spreadsheet_id: The ID of the spreadsheet that contains the sheet.
    """
    sheet_id = name_or_id if isinstance(name_or_id, int) else self.sheet_id(name_or_id, spreadsheet_id)

    self._spreadsheets.batchUpdate(
        spreadsheetId=spreadsheet_id,
        body={
            "requests": [
                {
                    "updateSheetProperties": {
                        "properties": {
                            "sheetId": sheet_id,
                            "title": new_name,
                        },
                        "fields": "title",
                    }
                }
            ]
        },
    ).execute()

sheet_id ¤

sheet_id(name, spreadsheet_id)

Returns the ID of a sheet.

Parameters:

Name Type Description Default
name str

The name of the sheet.

required
spreadsheet_id str

The ID of the spreadsheet.

required

Returns:

Type Description
int

The ID of the sheet.

Source code in src/msl/io/google_api.py
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
def sheet_id(self, name: str, spreadsheet_id: str) -> int:
    """Returns the ID of a sheet.

    Args:
        name: The name of the sheet.
        spreadsheet_id: The ID of the spreadsheet.

    Returns:
        The ID of the sheet.
    """
    request = self._spreadsheets.get(spreadsheetId=spreadsheet_id)
    response = request.execute()
    for sheet in response["sheets"]:
        if sheet["properties"]["title"] == name:
            return int(sheet["properties"]["sheetId"])

    msg = f"A sheet named {name!r} does not exist"
    raise ValueError(msg)

sheet_names ¤

sheet_names(spreadsheet_id)

Get the names of all sheets in a spreadsheet.

Parameters:

Name Type Description Default
spreadsheet_id str

The ID of a Google Sheets file.

required

Returns:

Type Description
tuple[str, ...]

The names of all sheets.

Source code in src/msl/io/google_api.py
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
def sheet_names(self, spreadsheet_id: str) -> tuple[str, ...]:
    """Get the names of all sheets in a spreadsheet.

    Args:
        spreadsheet_id: The ID of a Google Sheets file.

    Returns:
        The names of all sheets.
    """
    request = self._spreadsheets.get(spreadsheetId=spreadsheet_id)
    response = request.execute()
    return tuple(r["properties"]["title"] for r in response["sheets"])

to_datetime staticmethod ¤

to_datetime(value)

Convert a serial number date into a datetime.

Parameters:

Name Type Description Default
value float

A date in the serial number format.

required

Returns:

Type Description
datetime

The date converted.

Source code in src/msl/io/google_api.py
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
@staticmethod
def to_datetime(value: float) -> datetime:
    """Convert a *serial number* date into a [datetime][datetime.datetime].

    Args:
        value: A date in the *serial number* format.

    Returns:
        The date converted.
    """
    days = int(value)
    seconds = (value - days) * 86400  # 60 * 60 * 24
    return GSheets.SERIAL_NUMBER_ORIGIN + timedelta(days=days, seconds=seconds)

values ¤

values(
    spreadsheet_id,
    sheet=None,
    cells=None,
    *,
    row_major=True,
    value_option=GValueOption.FORMATTED,
    datetime_option=GDateTimeOption.SERIAL_NUMBER,
)

Return a range of values from a spreadsheet.

Parameters:

Name Type Description Default
spreadsheet_id str

The ID of a Google Sheets file.

required
sheet str | None

The name of a sheet in the spreadsheet to read the values from. If not specified and only one sheet exists in the spreadsheet then automatically determines the sheet name; however, it is more efficient to specify the name of the sheet.

None
cells str | None

The A1 notation or R1C1 notation of the range to retrieve values from. If not specified then returns all values that are in sheet.

None
row_major bool

Whether to return the values in row-major or column-major order.

True
value_option str | GValueOption

How values should be represented in the output. If a str, it must be equal to one of the values in GValueOption.

FORMATTED
datetime_option str | GDateTimeOption

How dates, times, and durations should be represented in the output. If a str, it must be equal to one of the values in GDateTimeOption. This argument is ignored if value_option is GValueOption.FORMATTED.

SERIAL_NUMBER

Returns:

Type Description
list[list[Any]]

The values from the sheet.

Source code in src/msl/io/google_api.py
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
def values(  # noqa: PLR0913
    self,
    spreadsheet_id: str,
    sheet: str | None = None,
    cells: str | None = None,
    *,
    row_major: bool = True,
    value_option: str | GValueOption = GValueOption.FORMATTED,
    datetime_option: str | GDateTimeOption = GDateTimeOption.SERIAL_NUMBER,
) -> list[list[Any]]:
    """Return a range of values from a spreadsheet.

    Args:
        spreadsheet_id: The ID of a Google Sheets file.
        sheet: The name of a sheet in the spreadsheet to read the values from.
            If not specified and only one sheet exists in the spreadsheet
            then automatically determines the sheet name; however, it is
            more efficient to specify the name of the sheet.
        cells: The `A1` notation or `R1C1` notation of the range to retrieve values
            from. If not specified then returns all values that are in `sheet`.
        row_major: Whether to return the values in row-major or column-major order.
        value_option: How values should be represented in the output. If a [str][],
            it must be equal to one of the values in [GValueOption][msl.io.google_api.GValueOption].
        datetime_option: How dates, times, and durations should be represented in the
            output. If a [str][], it must be equal to one of the values in
            [GDateTimeOption][msl.io.google_api.GDateTimeOption]. This argument is ignored if `value_option` is
            [GValueOption.FORMATTED][msl.io.google_api.GValueOption.FORMATTED].

    Returns:
        The values from the sheet.
    """
    if isinstance(value_option, GValueOption):
        value_option = value_option.value

    if isinstance(datetime_option, GDateTimeOption):
        datetime_option = datetime_option.value

    response = (
        self._spreadsheets.values()
        .get(
            spreadsheetId=spreadsheet_id,
            range=self._get_range(sheet, cells, spreadsheet_id),
            majorDimension="ROWS" if row_major else "COLUMNS",
            valueRenderOption=value_option,
            dateTimeRenderOption=datetime_option,
        )
        .execute()
    )
    return response.get("values", [[]])  # type: ignore[no-any-return]

write ¤

write(
    values,
    spreadsheet_id,
    cell=None,
    sheet=None,
    *,
    row_major=True,
    raw=False,
)

Write values to a sheet.

If a cell that is being written to already contains a value, the value in that cell is overwritten with the new value.

Parameters:

Name Type Description Default
values Any | list[Any] | tuple[Any, ...] | list[list[Any]] | tuple[tuple[Any, ...], ...]

The value(s) to write.

required
spreadsheet_id str

The ID of a Google Sheets file.

required
cell str | None

The cell (top-left corner) to start writing the values to. For example, 'C9'.

None
sheet str | None

The name of a sheet in the spreadsheet to write the values to. If not specified and only one sheet exists in the spreadsheet then automatically determines the sheet name; however, it is more efficient to specify the name of the sheet.

None
row_major bool

Whether to write the values in row-major or column-major order.

True
raw bool

Determines how the values should be interpreted. If True, the values will not be parsed and will be stored as-is. If False, the values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI.

False
Source code in src/msl/io/google_api.py
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
def write(  # noqa: PLR0913
    self,
    values: Any | list[Any] | tuple[Any, ...] | list[list[Any]] | tuple[tuple[Any, ...], ...],
    spreadsheet_id: str,
    cell: str | None = None,
    sheet: str | None = None,
    *,
    row_major: bool = True,
    raw: bool = False,
) -> None:
    """Write values to a sheet.

    If a cell that is being written to already contains a value,
    the value in that cell is overwritten with the new value.

    Args:
        values: The value(s) to write.
        spreadsheet_id: The ID of a Google Sheets file.
        cell: The cell (top-left corner) to start writing the values to. For example, `'C9'`.
        sheet: The name of a sheet in the spreadsheet to write the values to.
            If not specified and only one sheet exists in the spreadsheet
            then automatically determines the sheet name; however, it is
            more efficient to specify the name of the sheet.
        row_major: Whether to write the values in row-major or column-major order.
        raw: Determines how the values should be interpreted. If `True`,
            the values will not be parsed and will be stored as-is. If
            `False`, the values will be parsed as if the user typed
            them into the UI. Numbers will stay as numbers, but strings may
            be converted to numbers, dates, etc. following the same rules
            that are applied when entering text into a cell via the Google
            Sheets UI.
    """
    self._spreadsheets.values().update(
        spreadsheetId=spreadsheet_id,
        range=self._get_range(sheet, cell, spreadsheet_id),
        valueInputOption="RAW" if raw else "USER_ENTERED",
        body={
            "values": self._values(values),
            "majorDimension": "ROWS" if row_major else "COLUMNS",
        },
    ).execute()

GValueOption ¤

Bases: Enum

Determines how values should be returned.

FORMATTED class-attribute instance-attribute ¤

FORMATTED = 'FORMATTED_VALUE'

Values will be calculated and formatted in the reply according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "$1.23".

FORMULA class-attribute instance-attribute ¤

FORMULA = 'FORMULA'

Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".

UNFORMATTED class-attribute instance-attribute ¤

UNFORMATTED = 'UNFORMATTED_VALUE'

Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23.

GoogleAPI ¤

GoogleAPI(
    *,
    service,
    version,
    account,
    credentials,
    scopes,
    read_only,
)

Base class for all Google APIs.

Source code in src/msl/io/google_api.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def __init__(  # noqa: PLR0913
    self,
    *,
    service: str,
    version: str,
    account: str | None,
    credentials: Path | None,
    scopes: list[str],
    read_only: bool,
) -> None:
    """Base class for all Google APIs."""
    name = f"{account}-" if account else ""
    readonly = "-readonly" if read_only else ""
    token = MSL_IO_DIR / f"{name}token-{service}{readonly}.json"
    oauth = _authenticate(token, credentials, scopes)
    self._service: Any = build(service, version, credentials=oauth)  # pyright: ignore[reportPossiblyUnboundVariable]

service property ¤

service

The Resource object with methods for interacting with the API service.

close ¤

close()

Close the connection to the API service.

Source code in src/msl/io/google_api.py
156
157
158
def close(self) -> None:
    """Close the connection to the API service."""
    self._service.close()

Profile dataclass ¤

Profile(
    email_address, messages_total, threads_total, history_id
)

An authenticated user's Gmail profile.

Attributes:

Name Type Description
email_address str

The authenticated user's email address

messages_total int

The total number of messages in the mailbox

threads_total int

The total number of threads in the mailbox

history_id str

The ID of the mailbox's current history record