Skip to content

Spreadsheet Handler

googleSheetsLib.core.Spreadsheet

Interface to handle Google Spreadsheets operations via API.

This class serves as a wrapper to interface with Google Sheets. It can handle operations at the Spreadsheet level (metadata, batch updates) and acts as a factory for Sheet objects.

It handles authentication internally using the ClientWrapper class.

For more information on the API, visit: https://developers.google.com/workspace/sheets/api/quickstart/python

Attributes:

Name Type Description
spreadsheet_id str

The unique ID of the Spreadsheet (found in the URL).

name str

The Spreadsheet's title.

locale str

The locale of the spreadsheet (e.g., 'en_US', 'pt_BR').

timezone str

The timezone of the spreadsheet.

client ClientWrapper

Handler for API requests, authentication, and retry logic.

service SpreadsheetResource

The authenticated Google Sheets API resource.

batch_requests list[dict]

Pending requests for the batchUpdate endpoint.

batch_value_requests list[dict]

Pending requests for the values.batchUpdate endpoint.

last_refreshed datetime

Timestamp of the last metadata update.

metadata dict

Raw dictionary containing the full Spreadsheet metadata.

sheets_info dict

Metadata for individual tabs (id, name, grid size), indexed by tab name.

Parameters:

Name Type Description Default
spreadsheet_id str

The ID found in the Google Sheets URL.

required
token_fp str

File path to the auth token. Defaults to auth/token.json.

TOKEN_PATH
cred_fp str

File path to the credentials JSON. Defaults to auth/cred.json.

CRED_PATH
scopes list[str]

List of API scopes required. Defaults to SCOPES.

SCOPES
Notes

You can setup the environment variables GOOGLE_SERVICE_CREDS and GOOGLE_SERVICE_TOKEN instead of directing to the credentials path. Just insert the JSON in plain text in them and the ClientWrapper will take care of the rest.

Raises:

Type Description
ConnectionError

If the Google Client cannot be created or authenticated.

Example
# Initialize the handler
ss = Spreadsheet(spreadsheet_id="1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms")

# Getting an individual tab:
tab = ss['Tab Name']
tab2 = ss.get_sheet_by_id(2084)
Source code in src/googleSheetsLib/core.py
class Spreadsheet:
    """
    Interface to handle Google Spreadsheets operations via API.

    This class serves as a wrapper to interface with Google Sheets. It can handle
    operations at the Spreadsheet level (metadata, batch updates) and acts as a 
    factory for Sheet objects.

    It handles authentication internally using the `ClientWrapper` class.

    For more information on the API, visit:
    https://developers.google.com/workspace/sheets/api/quickstart/python


    Attributes:
        spreadsheet_id (str): The unique ID of the Spreadsheet (found in the URL).
        name (str): The Spreadsheet's title.
        locale (str): The locale of the spreadsheet (e.g., 'en_US', 'pt_BR').
        timezone (str): The timezone of the spreadsheet.
        client (ClientWrapper): Handler for API requests, authentication, and retry logic.
        service (SpreadsheetResource): The authenticated Google Sheets API resource.
        batch_requests (list[dict]): Pending requests for the `batchUpdate` endpoint.
        batch_value_requests (list[dict]): Pending requests for the `values.batchUpdate` endpoint.
        last_refreshed (dt.datetime): Timestamp of the last metadata update.
        metadata (dict): Raw dictionary containing the full Spreadsheet metadata.
        sheets_info (dict): Metadata for individual tabs (id, name, grid size), indexed by tab name.

    Args:
        spreadsheet_id (str): The ID found in the Google Sheets URL.
        token_fp (str, optional): File path to the auth token. Defaults to auth/token.json.
        cred_fp (str, optional): File path to the credentials JSON. Defaults to auth/cred.json.
        scopes (list[str], optional): List of API scopes required. Defaults to SCOPES.

    Notes:
        You can setup the environment variables GOOGLE_SERVICE_CREDS and GOOGLE_SERVICE_TOKEN
        instead of directing to the credentials path. Just insert the JSON in plain text
        in them and the ClientWrapper will take care of the rest.

    Raises:
        ConnectionError: If the Google Client cannot be created or authenticated.

    Example:
        ```python
        # Initialize the handler
        ss = Spreadsheet(spreadsheet_id="1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms")

        # Getting an individual tab:
        tab = ss['Tab Name']
        tab2 = ss.get_sheet_by_id(2084)
        ```
    """
    def __init__(self, 
                 spreadsheet_id:str,
                 token_fp:str = TOKEN_PATH,
                 cred_fp:str = CRED_PATH,
                 scopes:list[str] = SCOPES):

        self.client = ClientWrapper(token_path=token_fp,
                                    credentials_path=cred_fp,
                                    scopes=scopes)
        if not self.client.service:
            print('Not possible to create Google Client.')
            raise ConnectionError('Service not found. Check the credentials or the configs.')
        self.spreadsheet_id = spreadsheet_id
        self.service = self.client.service.spreadsheets() # type: ignore

        self.name = ''
        self.locale = ''
        self.timezone = ''
        self.sheets_info = dict()
        self.batch_requests = []
        self.batch_value_requests = []
        self.last_refreshed = dt.datetime(1999,1,1,0,0,0)

        # Construindo metadata:

        metadata = self._get_metadata()
        if metadata.data:
            self.build_metadata(metadata.data)
        else:
            print('Not possible to build metadata. Error in the response.')

    def _get_metadata(self) -> Response:
        """
        Internal method to help update metadata. It only makes the get request to the API
        and returns an Response object containing the metadata for future parsing.

        Returns:
            Response: Response object containing either the metadata in the .data field,
                or error information if the request failed.
        """
        request = self.service.get(spreadsheetId = self.spreadsheet_id) 
        response = self.client.execute(request)
        return response

    def build_metadata(self, metadata) -> bool:
        """
        Method to parse metadata dict and update the object's metadata attributes.

        It builds both the Spreadsheet's and the individual Tab's metadata.

        Args:
            metadata (dict): Dictionary containing the Spreadsheet's metadata, as structured
                by the .get() request on the Spreadsheet resource.

        Returns:
            bool: If the build succeded or failed. False if the metadata is empty or
                it generated a KeyError, True otherwise.

        """
        if not metadata:
            return False
        try:
            spreadsheet_metadata = metadata['properties']
            sheets_metadata = metadata['sheets']

            # Construíndo metadados da planilha como um todo:
            self.name = spreadsheet_metadata['title']
            self.locale = spreadsheet_metadata['locale']
            self.timezone = spreadsheet_metadata['timeZone']
            self.last_refreshed = dt.datetime.now()

            # Construindo metadados das abas:
            self.sheets_info.clear()

            for sheet in sheets_metadata:
                sheet = sheet['properties']
                name = sheet['title']
                sheet_id = sheet['sheetId']
                row_count = sheet['gridProperties']['rowCount']
                column_count = sheet['gridProperties']['columnCount']

                self.sheets_info[name] = {
                    'title': name,
                    'sheet_id' : sheet_id,
                    'row_count' : row_count,
                    'column_count' : column_count
                }

            return True

        except Exception as e:
            print(f'Error while building metadata: {e}.')
            return False

    def refresh_metadata(self) -> bool:
        """
        Method to refresh metadata. It only requests the metadata to the API and 
        sends it to the build_metadata method.

        Returns:
            bool: if the refresh failed or succeded.
        """
        logger.info('Refreshing metadata')
        metadata = self._get_metadata()
        if metadata.data:
            return self.build_metadata(metadata.data)
        else:
            return False

    def get_sheet(self, sheet_name:str) -> Sheet | None:
        """
        Retrieves a `Sheet` object by its name using cached metadata.

        This method acts as a factory, returning a `Sheet` object initialized 
        with the data currently stored in `self.sheets_info`.

        If the sheet exists in the Google Spreadsheet but not in the local metadata 
        (e.g., created recently), you must call `refresh_metadata()` first.

        This method supports the subscript syntax (e.g., `spreadsheet['Tab Name']`).

        Args:
            sheet_name (str): The exact name of the tab as it appears in Google Sheets.

        Returns:
            Sheet: A Sheet object initialized with the tab's ID and dimensions.
            None: If the sheet name is not found in the local metadata.

        Example:
            ```python
            # Direct method call
            inventory = ss.get_sheet('Inventory')

            # Using subscript syntax (sugar)
            sales = ss['Sales']

            # Handling non-existent sheets
            if not ss.get_sheet('Ghost Tab'):
                print("Tab not found or metadata outdated.")
            ```
        """
        logger.info(f'Building Sheet object. Searching for sheet {sheet_name} in local metadata...')
        if sheet_name in self.sheets_info:
            sheet_info = self.sheets_info[sheet_name]
            return Sheet(
                name = sheet_name,
                id = sheet_info['sheet_id'],
                service = self.service,
                client = self.client,
                row_count = sheet_info['row_count'],
                column_count = sheet_info['column_count'],
                parent_spreadsheet = self
            )
        else:
            logger.warning(f'Sheet {sheet_name} not found. Try refreshing the metadata or check your spelling.')
            return None

    def get_sheet_by_id(self, id:int) -> Sheet | None:
        """
        Retrieves a `Sheet` object by its id using cached metadata.

        It's useful if you rename a tab, but maintain the id in a varibable.

        To access a tab's id, get the Sheet's metadata in self.sheets_info and
        check the sheet_id value.

        This method acts as a factory, returning a `Sheet` object initialized 
        with the data currently stored in `self.sheets_info`.

        If the sheet exists in the Google Spreadsheet but not in the local metadata 
        (e.g., created recently), you must call `refresh_metadata()` first.

        This method supports the subscript syntax (e.g., `spreadsheet[id]`).

        Args:
            id (int): The tab's id. This is an internal id that has to be accessed
                by the Spreadsheet's metadata.

        Returns:
            Sheet: A Sheet object initialized with the tab's ID and dimensions.
            None: If the sheet name is not found in the local metadata.

        Example:
            ```python
            # Direct method call
            inventory = ss.get_sheet_by_id(49203)

            # Using subscript syntax (sugar)
            sales = ss[2384]

            # Using the sheets_info metadata:
            sheet_metadata = ss.sheets_info['Sales']
            sheet_id = sheet_metadata['sheet_id']
            sales = ss.get_sheet_by_id(sheet_id)

            # Handling non-existent sheets
            if not ss.get_sheet_by_id(3232):
                print("Tab not found or metadata outdated.")
            ```
        """
        name = ''
        for sheet_info in self.sheets_info.values():
            if sheet_info['sheet_id'] == id:
                name = sheet_info['title']
                break
        if name:
            return self.get_sheet(name)
        else:
            logger.warning(f'Sheet ID {id} not found.')
            return None

    def execute_batch(self) -> Response:
        """
        Executes all pending requests in the batch queue via the `batchUpdate` endpoint.

        This method compiles all operations stored in `self.batch_requests`, wraps them 
        into a single API call, and passes it to the client handler.

        If the execution is successful, the method should ideally clear the pending 
        requests queue.

        Returns:
            Response: A custom response object. 
                - If successful (`response.ok` is True), contains the API reply.
                - If failed, contains error details and the function name context.

        Example:
            ```python
            # Add some operations (e.g., delete rows, format cells)
            ss.batch_requests.append(delete_rows_request)
            ss.batch_requests.append(format_header_request)

            # Send everything in one go
            resp = ss.execute_batch()

            if resp.ok:
                print("Batch update successful!")
            else:
                print(f"Error: {resp.error}")
            ```
        """
        requests = self.batch_requests
        details = self._get_dets(locals())
        function_name = 'Spreadsheet.execute_batch'
        if not requests:
            return Response.fail('No request to execute.', function_name=function_name, details = details)
        try:
            body: BatchUpdateSpreadsheetRequest = {'requests' : requests} # type: ignore
            request = self.service.batchUpdate(
                spreadsheetId=self.spreadsheet_id,
                body = body
            )
        except Exception as e:
            return Response.fail(f'Error while building request: {e}', function_name=function_name, details=details)

        response = self.client.execute(request)

        if response.ok:
            response.details = details
            self.batch_requests = []

        elif response.error:
            response.error.details = details
            response.error.function_name = function_name

        return response


    def get_info(self) -> dict:
        """
        Returns a simple dictionary containing the Spreadsheet's info. 

        Useful for serialization.

        Returns:
            dict: Dictionary with spreadsheet_id, name, last refreshed and tabs info.
        """
        return {
            'spreadsheet_id' : self.spreadsheet_id,
            'name' : self.name,
            'timezone' : self.timezone,
            'locale' : self.locale,
            'last_refreshed' : str(self.last_refreshed),
            'sheets' : self.sheets_info
        }

    def _get_dets(self, locals:dict):
        """
        Internal method to create details about a method execution, mostly
        to attach to Response type objects.

        Receives a dictionary of parameters, generally representing the local namespace,
        and returns an enriched and cleaned dictionary with the Spreadsheet's info.

        Args:
            locals (dict): dict with runtime parameters used to build the details dictionary.

        Returns:
            deets (dict): data received enriched with aditional Spreadsheet information. 
        """
        deets = locals.copy()
        if 'self' in deets:
            del deets['self']
        deets['spreadsheet_info'] = self.get_info()
        return deets

    def __getitem__(self, sheet: int | str):
        """
        Dunder method to implement subscript syntax for both get_sheet and get_sheet_by_id.

        If it receives a string, calls get_sheet.
        If it receives an integer, calls get_sheet_by_id.

        Args:
            sheet (str | int): The sheet's name or id to be created.
        Returns:
            Sheet: Sheet type object if the Sheet object creation was succesfull.
            None: If it couldn't find the id or name in the metadata.
        Raises:
            ValueError: if the received parameter is neither str or int.
        """
        if isinstance(sheet, int):
            return self.get_sheet_by_id(sheet)
        elif isinstance(sheet, str):
            return self.get_sheet(sheet)
        else:
            raise ValueError(f'Unexpected parameter type: {type(sheet)}.')

build_metadata(metadata)

Method to parse metadata dict and update the object's metadata attributes.

It builds both the Spreadsheet's and the individual Tab's metadata.

Parameters:

Name Type Description Default
metadata dict

Dictionary containing the Spreadsheet's metadata, as structured by the .get() request on the Spreadsheet resource.

required

Returns:

Name Type Description
bool bool

If the build succeded or failed. False if the metadata is empty or it generated a KeyError, True otherwise.

Source code in src/googleSheetsLib/core.py
def build_metadata(self, metadata) -> bool:
    """
    Method to parse metadata dict and update the object's metadata attributes.

    It builds both the Spreadsheet's and the individual Tab's metadata.

    Args:
        metadata (dict): Dictionary containing the Spreadsheet's metadata, as structured
            by the .get() request on the Spreadsheet resource.

    Returns:
        bool: If the build succeded or failed. False if the metadata is empty or
            it generated a KeyError, True otherwise.

    """
    if not metadata:
        return False
    try:
        spreadsheet_metadata = metadata['properties']
        sheets_metadata = metadata['sheets']

        # Construíndo metadados da planilha como um todo:
        self.name = spreadsheet_metadata['title']
        self.locale = spreadsheet_metadata['locale']
        self.timezone = spreadsheet_metadata['timeZone']
        self.last_refreshed = dt.datetime.now()

        # Construindo metadados das abas:
        self.sheets_info.clear()

        for sheet in sheets_metadata:
            sheet = sheet['properties']
            name = sheet['title']
            sheet_id = sheet['sheetId']
            row_count = sheet['gridProperties']['rowCount']
            column_count = sheet['gridProperties']['columnCount']

            self.sheets_info[name] = {
                'title': name,
                'sheet_id' : sheet_id,
                'row_count' : row_count,
                'column_count' : column_count
            }

        return True

    except Exception as e:
        print(f'Error while building metadata: {e}.')
        return False

refresh_metadata()

Method to refresh metadata. It only requests the metadata to the API and sends it to the build_metadata method.

Returns:

Name Type Description
bool bool

if the refresh failed or succeded.

Source code in src/googleSheetsLib/core.py
def refresh_metadata(self) -> bool:
    """
    Method to refresh metadata. It only requests the metadata to the API and 
    sends it to the build_metadata method.

    Returns:
        bool: if the refresh failed or succeded.
    """
    logger.info('Refreshing metadata')
    metadata = self._get_metadata()
    if metadata.data:
        return self.build_metadata(metadata.data)
    else:
        return False

get_sheet(sheet_name)

Retrieves a Sheet object by its name using cached metadata.

This method acts as a factory, returning a Sheet object initialized with the data currently stored in self.sheets_info.

If the sheet exists in the Google Spreadsheet but not in the local metadata (e.g., created recently), you must call refresh_metadata() first.

This method supports the subscript syntax (e.g., spreadsheet['Tab Name']).

Parameters:

Name Type Description Default
sheet_name str

The exact name of the tab as it appears in Google Sheets.

required

Returns:

Name Type Description
Sheet Sheet | None

A Sheet object initialized with the tab's ID and dimensions.

None Sheet | None

If the sheet name is not found in the local metadata.

Example
# Direct method call
inventory = ss.get_sheet('Inventory')

# Using subscript syntax (sugar)
sales = ss['Sales']

# Handling non-existent sheets
if not ss.get_sheet('Ghost Tab'):
    print("Tab not found or metadata outdated.")
Source code in src/googleSheetsLib/core.py
def get_sheet(self, sheet_name:str) -> Sheet | None:
    """
    Retrieves a `Sheet` object by its name using cached metadata.

    This method acts as a factory, returning a `Sheet` object initialized 
    with the data currently stored in `self.sheets_info`.

    If the sheet exists in the Google Spreadsheet but not in the local metadata 
    (e.g., created recently), you must call `refresh_metadata()` first.

    This method supports the subscript syntax (e.g., `spreadsheet['Tab Name']`).

    Args:
        sheet_name (str): The exact name of the tab as it appears in Google Sheets.

    Returns:
        Sheet: A Sheet object initialized with the tab's ID and dimensions.
        None: If the sheet name is not found in the local metadata.

    Example:
        ```python
        # Direct method call
        inventory = ss.get_sheet('Inventory')

        # Using subscript syntax (sugar)
        sales = ss['Sales']

        # Handling non-existent sheets
        if not ss.get_sheet('Ghost Tab'):
            print("Tab not found or metadata outdated.")
        ```
    """
    logger.info(f'Building Sheet object. Searching for sheet {sheet_name} in local metadata...')
    if sheet_name in self.sheets_info:
        sheet_info = self.sheets_info[sheet_name]
        return Sheet(
            name = sheet_name,
            id = sheet_info['sheet_id'],
            service = self.service,
            client = self.client,
            row_count = sheet_info['row_count'],
            column_count = sheet_info['column_count'],
            parent_spreadsheet = self
        )
    else:
        logger.warning(f'Sheet {sheet_name} not found. Try refreshing the metadata or check your spelling.')
        return None

get_sheet_by_id(id)

Retrieves a Sheet object by its id using cached metadata.

It's useful if you rename a tab, but maintain the id in a varibable.

To access a tab's id, get the Sheet's metadata in self.sheets_info and check the sheet_id value.

This method acts as a factory, returning a Sheet object initialized with the data currently stored in self.sheets_info.

If the sheet exists in the Google Spreadsheet but not in the local metadata (e.g., created recently), you must call refresh_metadata() first.

This method supports the subscript syntax (e.g., spreadsheet[id]).

Parameters:

Name Type Description Default
id int

The tab's id. This is an internal id that has to be accessed by the Spreadsheet's metadata.

required

Returns:

Name Type Description
Sheet Sheet | None

A Sheet object initialized with the tab's ID and dimensions.

None Sheet | None

If the sheet name is not found in the local metadata.

Example
# Direct method call
inventory = ss.get_sheet_by_id(49203)

# Using subscript syntax (sugar)
sales = ss[2384]

# Using the sheets_info metadata:
sheet_metadata = ss.sheets_info['Sales']
sheet_id = sheet_metadata['sheet_id']
sales = ss.get_sheet_by_id(sheet_id)

# Handling non-existent sheets
if not ss.get_sheet_by_id(3232):
    print("Tab not found or metadata outdated.")
Source code in src/googleSheetsLib/core.py
def get_sheet_by_id(self, id:int) -> Sheet | None:
    """
    Retrieves a `Sheet` object by its id using cached metadata.

    It's useful if you rename a tab, but maintain the id in a varibable.

    To access a tab's id, get the Sheet's metadata in self.sheets_info and
    check the sheet_id value.

    This method acts as a factory, returning a `Sheet` object initialized 
    with the data currently stored in `self.sheets_info`.

    If the sheet exists in the Google Spreadsheet but not in the local metadata 
    (e.g., created recently), you must call `refresh_metadata()` first.

    This method supports the subscript syntax (e.g., `spreadsheet[id]`).

    Args:
        id (int): The tab's id. This is an internal id that has to be accessed
            by the Spreadsheet's metadata.

    Returns:
        Sheet: A Sheet object initialized with the tab's ID and dimensions.
        None: If the sheet name is not found in the local metadata.

    Example:
        ```python
        # Direct method call
        inventory = ss.get_sheet_by_id(49203)

        # Using subscript syntax (sugar)
        sales = ss[2384]

        # Using the sheets_info metadata:
        sheet_metadata = ss.sheets_info['Sales']
        sheet_id = sheet_metadata['sheet_id']
        sales = ss.get_sheet_by_id(sheet_id)

        # Handling non-existent sheets
        if not ss.get_sheet_by_id(3232):
            print("Tab not found or metadata outdated.")
        ```
    """
    name = ''
    for sheet_info in self.sheets_info.values():
        if sheet_info['sheet_id'] == id:
            name = sheet_info['title']
            break
    if name:
        return self.get_sheet(name)
    else:
        logger.warning(f'Sheet ID {id} not found.')
        return None

execute_batch()

Executes all pending requests in the batch queue via the batchUpdate endpoint.

This method compiles all operations stored in self.batch_requests, wraps them into a single API call, and passes it to the client handler.

If the execution is successful, the method should ideally clear the pending requests queue.

Returns:

Name Type Description
Response Response

A custom response object. - If successful (response.ok is True), contains the API reply. - If failed, contains error details and the function name context.

Example
# Add some operations (e.g., delete rows, format cells)
ss.batch_requests.append(delete_rows_request)
ss.batch_requests.append(format_header_request)

# Send everything in one go
resp = ss.execute_batch()

if resp.ok:
    print("Batch update successful!")
else:
    print(f"Error: {resp.error}")
Source code in src/googleSheetsLib/core.py
def execute_batch(self) -> Response:
    """
    Executes all pending requests in the batch queue via the `batchUpdate` endpoint.

    This method compiles all operations stored in `self.batch_requests`, wraps them 
    into a single API call, and passes it to the client handler.

    If the execution is successful, the method should ideally clear the pending 
    requests queue.

    Returns:
        Response: A custom response object. 
            - If successful (`response.ok` is True), contains the API reply.
            - If failed, contains error details and the function name context.

    Example:
        ```python
        # Add some operations (e.g., delete rows, format cells)
        ss.batch_requests.append(delete_rows_request)
        ss.batch_requests.append(format_header_request)

        # Send everything in one go
        resp = ss.execute_batch()

        if resp.ok:
            print("Batch update successful!")
        else:
            print(f"Error: {resp.error}")
        ```
    """
    requests = self.batch_requests
    details = self._get_dets(locals())
    function_name = 'Spreadsheet.execute_batch'
    if not requests:
        return Response.fail('No request to execute.', function_name=function_name, details = details)
    try:
        body: BatchUpdateSpreadsheetRequest = {'requests' : requests} # type: ignore
        request = self.service.batchUpdate(
            spreadsheetId=self.spreadsheet_id,
            body = body
        )
    except Exception as e:
        return Response.fail(f'Error while building request: {e}', function_name=function_name, details=details)

    response = self.client.execute(request)

    if response.ok:
        response.details = details
        self.batch_requests = []

    elif response.error:
        response.error.details = details
        response.error.function_name = function_name

    return response

get_info()

Returns a simple dictionary containing the Spreadsheet's info.

Useful for serialization.

Returns:

Name Type Description
dict dict

Dictionary with spreadsheet_id, name, last refreshed and tabs info.

Source code in src/googleSheetsLib/core.py
def get_info(self) -> dict:
    """
    Returns a simple dictionary containing the Spreadsheet's info. 

    Useful for serialization.

    Returns:
        dict: Dictionary with spreadsheet_id, name, last refreshed and tabs info.
    """
    return {
        'spreadsheet_id' : self.spreadsheet_id,
        'name' : self.name,
        'timezone' : self.timezone,
        'locale' : self.locale,
        'last_refreshed' : str(self.last_refreshed),
        'sheets' : self.sheets_info
    }