diff --git a/docs/advanced.rst b/docs/advanced.rst index d075d221c..414a0b6ee 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -47,7 +47,7 @@ Using ``Authlib`` instead of ``google-auth``. Similar to `google.auth.transport. claims = {'scope': ' '.join(scopes)} return AssertionSession( grant_type=AssertionSession.JWT_BEARER_GRANT_TYPE, - token_url=token_url, + token_endpoint=token_url, issuer=issuer, audience=token_url, claims=claims, diff --git a/docs/community.rst b/docs/community.rst new file mode 100644 index 000000000..fb06af910 --- /dev/null +++ b/docs/community.rst @@ -0,0 +1,30 @@ +Community Extensions +==================== + +.. _gspread-formating-label: + +gspread-formating +~~~~~~~~~~~~~~~~~ + +`gspread-formatting `_ offers extensive functionality to help you when you go beyond basic format +provided by ``gspread``. + + +.. _gspread-pandas-label: + +Using gspread with pandas +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can find the below libraries to use gsrpead with pandas: + + * `gspread-pandas `_ + * `gspread-dataframe `_ + +.. _gspread-orm-label: + +Object Relational Mappers (ORMs) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `gspread-models `_ package provides a straightforward and intuitive model-based +query interface, making it easy to interact with Google Sheets as if it were more like a database. + diff --git a/docs/index.rst b/docs/index.rst index 9485e31cf..809d41e64 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -70,6 +70,14 @@ Advanced advanced +Community extensions +-------------------- + +.. toctree:: + :maxdepth: 2 + + community + API Documentation --------------------------- diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 97879e84e..50b90971f 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -350,7 +350,73 @@ Check out the api docs for `DataValidationRule`_ and `CondtionType`_ for more de .. _CondtionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType -.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule +.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule + +Extract table +~~~~~~~~~~~~~ + +Gspread provides a function to extract a data table. +A data table is defined as a rectangular table that stops either on the **first empty** cell or +the **enge of the sheet**. + +You can extract table from any address by providing the top left corner of the desired table. + +Gspread provides 3 directions for searching the end of the table: + + * :attr:`~gspread.utils.TableDirection.right`: extract a single row searching on the right of the starting cell + * :attr:`~gspread.utils.TableDirection.down`: extract a single column searching on the bottom of the starting cell + * :attr:`~gspread.utils.TableDirection.table`: extract a rectangular table by first searching right from starting cell, + then searching down from starting cell. + + .. note:: + + Gspread will not look for empty cell inside the table. it only look at the top row and first column. + +Example extracting a table from the below sample sheet: + +.. list-table:: Find table + :header-rows: 1 + + * - ID + - Name + - Universe + - Super power + * - 1 + - Batman + - DC + - Very rich + * - 2 + - DeadPool + - Marvel + - self healing + * - 3 + - Superman + - DC + - super human + * - + - \- + - \- + - \- + * - 5 + - Lavigne958 + - + - maintains Gspread + * - 6 + - Alifee + - + - maintains Gspread + +Using the below code will result in rows 2 to 4: + +.. code:: python + + worksheet.expand("A2") + + [ + ["Batman", "DC", "Very rich"], + ["DeadPool", "Marvel", "self healing"], + ["Superman", "DC", "super human"], + ] @@ -390,7 +456,7 @@ Color the background of **A2:B2** cell range in black, change horizontal alignme The second argument to :meth:`~gspread.models.Worksheet.format` is a dictionary containing the fields to update. A full specification of format options is available at `CellFormat `_ in Sheet API Reference. .. Tip:: - `gspread-formatting `_ offers extensive functionality to help you when you go beyond basics. + for more complex formatting see :ref:`gspread-formating-label`. Using gspread with pandas @@ -412,10 +478,7 @@ Here's a basic example for writing a dataframe to a sheet. With :meth:`~gspread. worksheet.update([dataframe.columns.values.tolist()] + dataframe.values.tolist()) -For advanced pandas use cases check out these libraries: - - * `gspread-pandas `_ - * `gspread-dataframe `_ +For advanced pandas use cases check out community section :ref:`gspread-pandas-label` Using gspread with NumPy ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gspread/auth.py b/gspread/auth.py index 454f4fabc..e5547aa29 100644 --- a/gspread/auth.py +++ b/gspread/auth.py @@ -386,8 +386,8 @@ def api_key(token: str, http_client: HTTPClientType = HTTPClient) -> Client: """ if GOOGLE_AUTH_API_KEY_AVAILABLE is False: raise NotImplementedError( - "api_key is only available with package google.auth>=2.4.0." - 'Install it with "pip install google-auth>=2.4.0".' + "api_key is only available with package google.auth>=2.15.0. " + 'Install it with "pip install google-auth>=2.15.0".' ) creds = APIKeyCredentials(token) return Client(auth=creds, http_client=http_client) diff --git a/gspread/exceptions.py b/gspread/exceptions.py index 4701ebdf7..cd4c8ff5b 100644 --- a/gspread/exceptions.py +++ b/gspread/exceptions.py @@ -6,9 +6,10 @@ """ -from typing import Any, Dict, Mapping, Optional, Union +from typing import Any, Mapping from requests import Response +from requests.exceptions import JSONDecodeError class UnSupportedExportFormat(Exception): @@ -40,20 +41,24 @@ class APIError(GSpreadException): such as when we attempt to retrieve things that don't exist.""" def __init__(self, response: Response): - super().__init__(self._extract_error(response)) + try: + error = response.json()["error"] + except JSONDecodeError: + # in case we failed to parse the error from the API + # build an empty error object to notify the caller + # and keep the exception raise flow running + + error = { + "code": -1, + "message": response.text, + "status": "invalid JSON", + } + + super().__init__(error) self.response: Response = response - self.error: Mapping[str, Any] = response.json()["error"] + self.error: Mapping[str, Any] = error self.code: int = self.error["code"] - def _extract_error( - self, response: Response - ) -> Optional[Dict[str, Union[int, str]]]: - try: - errors = response.json() - return dict(errors["error"]) - except (AttributeError, KeyError, ValueError): - return None - def __str__(self) -> str: return "{}: [{}]: {}".format( self.__class__.__name__, self.code, self.error["message"] @@ -62,6 +67,9 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() + def __reduce__(self) -> tuple: + return self.__class__, (self.response,) + class SpreadsheetNotFound(GSpreadException): """Trying to open non-existent or inaccessible spreadsheet.""" diff --git a/gspread/http_client.py b/gspread/http_client.py index 7cc802dd2..cf392ca31 100644 --- a/gspread/http_client.py +++ b/gspread/http_client.py @@ -514,7 +514,7 @@ def import_csv(self, file_id: str, data: Union[str, bytes]) -> Any: class BackOffHTTPClient(HTTPClient): - """BackoffClient is a gspread client with exponential + """BackOffHTTPClient is a http client with exponential backoff retries. In case a request fails due to some API rate limits, @@ -524,12 +524,12 @@ class BackOffHTTPClient(HTTPClient): prevent the application from failing (by raising an APIError exception). .. Warning:: - This Client is not production ready yet. + This HTTPClient is not production ready yet. Use it at your own risk ! .. note:: To use with the `auth` module, make sure to pass this backoff - client factory using the ``client_factory`` parameter of the + http client using the ``http_client`` parameter of the method used. .. note:: diff --git a/gspread/utils.py b/gspread/utils.py index 11e848371..6daac8616 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -168,6 +168,12 @@ class ValidationConditionType(StrEnum): filter_expression = "FILTER_EXPRESSION" +class TableDirection(StrEnum): + table = "TABLE" + down = "DOWN" + right = "RIGHT" + + def convert_credentials(credentials: Credentials) -> Credentials: module = credentials.__module__ cls = credentials.__class__.__name__ @@ -306,7 +312,7 @@ def numericise_all( :param list values: Input row :param bool empty2zero: (optional) Whether or not to return empty cells as 0 (zero). Defaults to ``False``. - :param str default_blank: Which value to use for blank cells, + :param Any default_blank: Which value to use for blank cells, defaults to empty string. :param bool allow_underscores_in_numeric_literals: Whether or not to allow visual underscores in numeric literals @@ -979,6 +985,129 @@ def to_records( return [dict(zip(headers, row)) for row in values] +def _expand_right(values: List[List[str]], start: int, end: int, row: int) -> int: + """This is a private function, returning the column index of the last non empty cell + on the given row. + + Search starts from ``start`` index column. + Search ends on ``end`` index column. + Searches only in the row pointed by ``row``. + """ + try: + return values[row].index("", start, end) - 1 + except ValueError: + return end + + +def _expand_bottom(values: List[List[str]], start: int, end: int, col: int) -> int: + """This is a private function, returning the row index of the last non empty cell + on the given column. + + Search starts from ``start`` index row. + Search ends on ``end`` index row. + Searches only in the column pointed by ``col``. + """ + for rows in range(start, end): + # in case we try to look further than last row + if rows >= len(values): + return len(values) - 1 + + # check if cell is empty (or the row => empty cell) + if col >= len(values[rows]) or values[rows][col] == "": + return rows - 1 + + return end - 1 + + +def find_table( + values: List[List[str]], + start_range: str, + direction: TableDirection = TableDirection.table, +) -> List[List[str]]: + """Expands a list of values based on non-null adjacent cells. + + Expand can be done in 3 directions defined in :class:`~gspread.utils.TableDirection` + + * ``TableDirection.right``: expands right until the first empty cell + * ``TableDirection.down``: expands down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell and down until first empty cell + + In case of empty result an empty list is restuned. + + When the given ``start_range`` is outside the given matrix of values the exception + :class:`~gspread.exceptions.InvalidInputValue` is raised. + + Example:: + + values = [ + ['', '', '', '', '' ], + ['', 'B2', 'C2', '', 'E2'], + ['', 'B3', 'C3', '', 'E3'], + ['', '' , '' , '', 'E4'], + ] + >>> utils.find_table(TableDirection.table, 'B2') + [ + ['B2', 'C2'], + ['B3', 'C3'], + ] + + + .. note:: + + the ``TableDirection.table`` will look right from starting cell then look down from starting cell. + It will not check cells located inside the table. This could lead to + potential empty values located in the middle of the table. + + .. warning:: + + Given values must be padded with `''` empty values. + + :param list[list] values: values where to find the table. + :param gspread.utils.TableDirection direction: the expand direction. + :param str start_range: the starting cell range. + :rtype list(list): the resulting matrix + """ + row, col = a1_to_rowcol(start_range) + + # a1_to_rowcol returns coordinates starting form 1 + row -= 1 + col -= 1 + + if row >= len(values): + raise InvalidInputValue( + "given row for start_range is outside given values: start range row ({}) >= rows in values {}".format( + row, len(values) + ) + ) + + if col >= len(values[row]): + raise InvalidInputValue( + "given column for start_range is outside given values: start range column ({}) >= columns in values {}".format( + col, len(values[row]) + ) + ) + + if direction == TableDirection.down: + rightMost = col + bottomMost = _expand_bottom(values, row, len(values), col) + + if direction == TableDirection.right: + bottomMost = row + rightMost = _expand_right(values, col, len(values[row]), row) + + if direction == TableDirection.table: + rightMost = _expand_right(values, col, len(values[row]), row) + bottomMost = _expand_bottom(values, row, len(values), col) + + result = [] + + # build resulting array + for rows in values[row : bottomMost + 1]: + result.append(rows[col : rightMost + 1]) + + return result + + # SHOULD NOT BE NEEDED UNTIL NEXT MAJOR VERSION # DEPRECATION_WARNING_TEMPLATE = ( # "[Deprecated][in version {v_deprecated}]: {msg_deprecated}" diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 2bff00919..aae4ae462 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -41,6 +41,7 @@ PasteOrientation, PasteType, T, + TableDirection, ValidationConditionType, ValueInputOption, ValueRenderOption, @@ -53,6 +54,7 @@ convert_colors_to_hex_value, convert_hex_to_colors_dict, fill_gaps, + find_table, finditem, get_a1_from_absolute_range, is_full_a1_notation, @@ -496,7 +498,7 @@ def get_all_records( head: int = 1, expected_headers: Optional[List[str]] = None, value_render_option: Optional[ValueRenderOption] = None, - default_blank: str = "", + default_blank: Any = "", numericise_ignore: Iterable[Union[str, int]] = [], allow_underscores_in_numeric_literals: bool = False, empty2zero: bool = False, @@ -529,7 +531,7 @@ def get_all_records( be rendered in the output. See `ValueRenderOption`_ in the Sheets API. :type value_render_option: :class:`~gspread.utils.ValueRenderOption` - :param str default_blank: (optional) Determines which value to use for + :param Any default_blank: (optional) Determines which value to use for blank cells, defaults to empty string. :param list numericise_ignore: (optional) List of ints of indices of the columns (starting at 1) to ignore numericising, special use @@ -2606,6 +2608,37 @@ def unmerge_cells(self, name: str) -> JSONResponse: return self.client.batch_update(self.spreadsheet_id, body) + def batch_merge( + self, + merges: List[Dict[Literal["range", "mergeType"], Union[str, MergeType]]], + merge_type: MergeType = MergeType.merge_all, + ) -> Any: + """Merge multiple ranges at the same time. + + :param merges: list of dictionaries with the ranges(is A1-notation), and + an optional ``MergeType`` field. + See `MergeType`_ in the Sheets API reference. + :type merges: List[Dict[Literal["range", "mergeType"], Union[str, MergeType]]] + :params merge_type: (optional) default ``MergeType`` for all merges missing the merges. + defaults to ``MergeType.merge_all``. + :type merge_type: ``MergeType`` + + :returns: The body of the request response. + :rtype: dict + """ + + requests = [ + { + "mergeCells": { + "range": a1_range_to_grid_range(merge["range"], self.id), + "mergeType": merge.get("mergeType", merge_type), + } + } + for merge in merges + ] + + return self.client.batch_update(self.spreadsheet_id, {"requests": requests}) + def get_notes( self, default_empty_value: Optional[str] = "", @@ -3352,3 +3385,56 @@ def add_validation( } return self.client.batch_update(self.spreadsheet_id, body) + + def expand( + self, + top_left_range_name: str = "A1", + direction: TableDirection = TableDirection.table, + ) -> List[List[str]]: + """Expands a cell range based on non-null adjacent cells. + + Expand can be done in 3 directions defined in :class:`~gspread.utils.TableDirection` + + * ``TableDirection.right``: expands right until the first empty cell + * ``TableDirection.down``: expands down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell and down until the first empty cell + + In case of empty result an empty list is restuned. + + When the given ``start_range`` is outside the given matrix of values the exception + :class:`~gspread.exceptions.InvalidInputValue` is raised. + + Example:: + + values = [ + ['', '', '', '', '' ], + ['', 'B2', 'C2', '', 'E2'], + ['', 'B3', 'C3', '', 'E3'], + ['', '' , '' , '', 'E4'], + ] + >>> utils.find_table(TableDirection.table, 'B2') + [ + ['B2', 'C2'], + ['B3', 'C3'], + ] + + + .. note:: + + the ``TableDirection.table`` will look right from starting cell then look down from starting cell. + It will not check cells located inside the table. This could lead to + potential empty values located in the middle of the table. + + .. note:: + + when it is necessary to use non-default options for :meth:`~gspread.worksheet.Worksheet.get`, + please get the data first using desired options then use the function + :func:`gspread.utils.find_table` to extract the desired table. + + :param str top_left_range_name: the top left corner of the table to expand. + :param gspread.utils.TableDirection direction: the expand direction + :rtype list(list): the resulting matrix + """ + + values = self.get(pad_values=True) + return find_table(values, top_left_range_name, direction) diff --git a/lint-requirements.txt b/lint-requirements.txt index 6a9b87237..275ca53ce 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,8 +1,8 @@ -bandit==1.7.8 -black==24.4.2 +bandit==1.7.10 +black==24.8.0 codespell==2.2.5 -flake8==7.0.0 +flake8==7.1.1 isort==5.12.0 -mypy==1.10.0 +mypy==1.11.2 mypy-extensions==1.0.0 -typing_extensions==4.12.0 +typing_extensions==4.12.2 diff --git a/tests/cassettes/SpreadsheetTest.test_bad_json_api_error.json b/tests/cassettes/SpreadsheetTest.test_bad_json_api_error.json new file mode 100644 index 000000000..a689aaed8 --- /dev/null +++ b/tests/cassettes/SpreadsheetTest.test_bad_json_api_error.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "POST", + "uri": "https://www.googleapis.com/drive/v3/files?supportsAllDrives=True", + "body": "{\"name\": \"Test SpreadsheetTest test_bad_json_api_error\", \"mimeType\": \"application/vnd.google-apps.spreadsheet\"}", + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "111" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Date": [ + "Sun, 22 Sep 2024 21:14:56 GMT" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Transfer-Encoding": [ + "chunked" + ], + "Vary": [ + "Origin, X-Origin" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Server": [ + "ESF" + ], + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Pragma": [ + "no-cache" + ], + "content-length": [ + "198" + ] + }, + "body": { + "string": "{\n \"kind\": \"drive#file\",\n \"id\": \"1YCFTdhfXm6o_JeYoFgYquVItZBeeAju8sF7Xuz1Tr-Y\",\n \"name\": \"Test SpreadsheetTest test_bad_json_api_error\",\n \"mimeType\": \"application/vnd.google-apps.spreadsheet\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1YCFTdhfXm6o_JeYoFgYquVItZBeeAju8sF7Xuz1Tr-Y?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Transfer-Encoding": [ + "chunked" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Server": [ + "ESF" + ], + "Cache-Control": [ + "private" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Date": [ + "Sun, 22 Sep 2024 21:14:57 GMT" + ], + "content-length": [ + "3342" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1YCFTdhfXm6o_JeYoFgYquVItZBeeAju8sF7Xuz1Tr-Y\",\n \"properties\": {\n \"title\": \"Test SpreadsheetTest test_bad_json_api_error\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1000,\n \"columnCount\": 26\n }\n }\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1YCFTdhfXm6o_JeYoFgYquVItZBeeAju8sF7Xuz1Tr-Y/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "DELETE", + "uri": "https://www.googleapis.com/drive/v3/files/1YCFTdhfXm6o_JeYoFgYquVItZBeeAju8sF7Xuz1Tr-Y?supportsAllDrives=True", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 204, + "message": "No Content" + }, + "headers": { + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "Content-Type": [ + "text/html" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Content-Length": [ + "0" + ], + "Vary": [ + "Origin, X-Origin" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Server": [ + "ESF" + ], + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Pragma": [ + "no-cache" + ], + "Date": [ + "Sun, 22 Sep 2024 21:14:57 GMT" + ] + }, + "body": { + "string": "" + } + } + } + ] +} diff --git a/tests/cassettes/WorksheetTest.test_batch_merged_cells.json b/tests/cassettes/WorksheetTest.test_batch_merged_cells.json new file mode 100644 index 000000000..afb0c208f --- /dev/null +++ b/tests/cassettes/WorksheetTest.test_batch_merged_cells.json @@ -0,0 +1,983 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "POST", + "uri": "https://www.googleapis.com/drive/v3/files?supportsAllDrives=True", + "body": "{\"name\": \"Test WorksheetTest test_batch_merged_cells\", \"mimeType\": \"application/vnd.google-apps.spreadsheet\"}", + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "109" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "Server": [ + "ESF" + ], + "Pragma": [ + "no-cache" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:38 GMT" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "Vary": [ + "Origin, X-Origin" + ], + "content-length": [ + "196" + ] + }, + "body": { + "string": "{\n \"kind\": \"drive#file\",\n \"id\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"name\": \"Test WorksheetTest test_batch_merged_cells\",\n \"mimeType\": \"application/vnd.google-apps.spreadsheet\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:39 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3340" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_batch_merged_cells\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1000,\n \"columnCount\": 26\n }\n }\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:40 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3340" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_batch_merged_cells\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1000,\n \"columnCount\": 26\n }\n }\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/values/%27Sheet1%27:clear", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:41 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "107" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"clearedRange\": \"Sheet1!A1:Z1000\"\n}\n" + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY:batchUpdate", + "body": "{\"requests\": [{\"updateSheetProperties\": {\"properties\": {\"sheetId\": 0, \"gridProperties\": {\"rowCount\": 4, \"columnCount\": 4}}, \"fields\": \"gridProperties/rowCount,gridProperties/columnCount\"}}]}", + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "190" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:42 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "97" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"replies\": [\n {}\n ]\n}\n" + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/values/%27Sheet1%27%21A1%3AD4?valueInputOption=RAW", + "body": "{\"values\": [[\"1\", \"\", \"\", \"\"], [\"\", \"\", \"title\", \"\"], [\"\", \"\", \"2\", \"\"], [\"num\", \"val\", \"\", \"0\"]], \"majorDimension\": null}", + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "122" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:43 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "169" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"updatedRange\": \"Sheet1!A1:D4\",\n \"updatedRows\": 4,\n \"updatedColumns\": 4,\n \"updatedCells\": 16\n}\n" + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY:batchUpdate", + "body": "{\"requests\": [{\"mergeCells\": {\"range\": {\"startRowIndex\": 0, \"endRowIndex\": 2, \"startColumnIndex\": 0, \"endColumnIndex\": 2, \"sheetId\": 0}, \"mergeType\": \"MERGE_ALL\"}}, {\"mergeCells\": {\"range\": {\"startRowIndex\": 1, \"endRowIndex\": 2, \"startColumnIndex\": 2, \"endColumnIndex\": 4, \"sheetId\": 0}, \"mergeType\": \"MERGE_ALL\"}}, {\"mergeCells\": {\"range\": {\"startRowIndex\": 2, \"endRowIndex\": 4, \"startColumnIndex\": 2, \"endColumnIndex\": 3, \"sheetId\": 0}, \"mergeType\": \"MERGE_ALL\"}}]}", + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "467" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:43 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "113" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"replies\": [\n {},\n {},\n {}\n ]\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/values/%27Sheet1%27", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:44 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "248" + ] + }, + "body": { + "string": "{\n \"range\": \"Sheet1!A1:D4\",\n \"majorDimension\": \"ROWS\",\n \"values\": [\n [\n \"1\"\n ],\n [\n \"\",\n \"\",\n \"title\"\n ],\n [\n \"\",\n \"\",\n \"2\"\n ],\n [\n \"num\",\n \"val\",\n \"\",\n \"0\"\n ]\n ]\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/values/%27Sheet1%27", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:45 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "248" + ] + }, + "body": { + "string": "{\n \"range\": \"Sheet1!A1:D4\",\n \"majorDimension\": \"ROWS\",\n \"values\": [\n [\n \"1\"\n ],\n [\n \"\",\n \"\",\n \"title\"\n ],\n [\n \"\",\n \"\",\n \"2\"\n ],\n [\n \"num\",\n \"val\",\n \"\",\n \"0\"\n ]\n ]\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:45 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3788" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_batch_merged_cells\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 4,\n \"columnCount\": 4\n }\n },\n \"merges\": [\n {\n \"startRowIndex\": 0,\n \"endRowIndex\": 2,\n \"startColumnIndex\": 0,\n \"endColumnIndex\": 2\n },\n {\n \"startRowIndex\": 1,\n \"endRowIndex\": 2,\n \"startColumnIndex\": 2,\n \"endColumnIndex\": 4\n },\n {\n \"startRowIndex\": 2,\n \"endRowIndex\": 4,\n \"startColumnIndex\": 2,\n \"endColumnIndex\": 3\n }\n ]\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/values/%27Sheet1%27%21A1%3AD4", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:46 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "248" + ] + }, + "body": { + "string": "{\n \"range\": \"Sheet1!A1:D4\",\n \"majorDimension\": \"ROWS\",\n \"values\": [\n [\n \"1\"\n ],\n [\n \"\",\n \"\",\n \"title\"\n ],\n [\n \"\",\n \"\",\n \"2\"\n ],\n [\n \"num\",\n \"val\",\n \"\",\n \"0\"\n ]\n ]\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Transfer-Encoding": [ + "chunked" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:47 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "private" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3788" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_batch_merged_cells\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 4,\n \"columnCount\": 4\n }\n },\n \"merges\": [\n {\n \"startRowIndex\": 0,\n \"endRowIndex\": 2,\n \"startColumnIndex\": 0,\n \"endColumnIndex\": 2\n },\n {\n \"startRowIndex\": 1,\n \"endRowIndex\": 2,\n \"startColumnIndex\": 2,\n \"endColumnIndex\": 4\n },\n {\n \"startRowIndex\": 2,\n \"endRowIndex\": 4,\n \"startColumnIndex\": 2,\n \"endColumnIndex\": 3\n }\n ]\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "DELETE", + "uri": "https://www.googleapis.com/drive/v3/files/1z_YjzCNQiR55V8p6KJKYEE4teROoa1OGD0ziuTadlpY?supportsAllDrives=True", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.32.3" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 204, + "message": "No Content" + }, + "headers": { + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "text/html" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "Server": [ + "ESF" + ], + "Pragma": [ + "no-cache" + ], + "Date": [ + "Tue, 24 Sep 2024 16:46:48 GMT" + ], + "Content-Length": [ + "0" + ], + "X-XSS-Protection": [ + "0" + ], + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin, X-Origin" + ] + }, + "body": { + "string": "" + } + } + } + ] +} diff --git a/tests/conftest.py b/tests/conftest.py index 676f54829..82f17da48 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ +import io import itertools import os import unittest -from typing import Any, Dict, Generator, Optional +from typing import Any, Dict, Generator, Optional, Tuple import pytest from google.auth.credentials import Credentials @@ -107,6 +108,26 @@ def request(self, *args: Any, **kwargs: Any) -> Response: raise e +class InvalidJsonApiErrorClient(VCRHTTPClient): + """Special HTTP client that always raises an exception due to 500 error with + an invalid JSON body. + In this case for now it returns some HTML to simulate the use of the wrong HTTP endpoint. + """ + + ERROR_MSG = bytes("

Failed

", "utf-8") + + def request(self, *args: Any, **kwargs: Any) -> Response: + resp = Response() + # fake an HTML response instead of a valid JSON response. + # urllib3 expect 'raw' to be bytes. + resp.raw = io.BytesIO(self.ERROR_MSG) + resp.status_code = 500 + resp.encoding = "text/html" + + # now raise the APIError exception as the regular HTTP client would + raise gspread.exceptions.APIError(resp) + + @pytest.fixture(scope="module") def client() -> Client: if CREDS_FILENAME is not None: @@ -119,3 +140,16 @@ def client() -> Client: raise AssertionError return gc + + +def invalid_json_client() -> Tuple[Client, bytes]: + """Returns an HTTP client that always returns an invalid JSON payload + and the expected error message from the raised exception. + """ + return ( + Client( + auth=DummyCredentials(DUMMY_ACCESS_TOKEN), + http_client=InvalidJsonApiErrorClient, + ), + InvalidJsonApiErrorClient.ERROR_MSG, + ) diff --git a/tests/spreadsheet_test.py b/tests/spreadsheet_test.py index 79d78ac82..ca1b5ed79 100644 --- a/tests/spreadsheet_test.py +++ b/tests/spreadsheet_test.py @@ -5,7 +5,7 @@ import gspread -from .conftest import GspreadTest +from .conftest import GspreadTest, invalid_json_client class SpreadsheetTest(GspreadTest): @@ -20,6 +20,16 @@ def init(self, client, request): client.del_spreadsheet(SpreadsheetTest.spreadsheet.id) + @pytest.mark.vcr() + def test_bad_json_api_error(self): + # no need to pass auth tokens we use a custom HTTP Client that always fail + bad_client, error_msg = invalid_json_client() + + with pytest.raises(gspread.exceptions.APIError) as e: + bad_client.get_file_drive_metadata("abcdef0123456789") + + self.assertTrue(e.match(error_msg.decode())) + @pytest.mark.vcr() def test_properties(self): self.assertTrue(re.match(r"^[a-zA-Z0-9-_]+$", self.spreadsheet.id)) diff --git a/tests/utils_test.py b/tests/utils_test.py index 187e74eb0..f64b759ae 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -503,3 +503,173 @@ def test_to_records(self): # given key are unordered # but they must match a value from the given input values self.assertIn(record[key], values[i]) + + def test_find_table_simple(self): + """Test find table with basic case""" + values = [ + ["A1", "B1", "C1", "D1"], + ["", "B2", "C2", "", "E2"], + ["", "B3", "C3", "D3", "E3"], + ["A4", "", "C4", "D4", "E4"], + ] + + table = utils.find_table( + values, + "B2", + utils.TableDirection.table, + ) + + table_max_row_max_column = utils.find_table( + values, + "D3", + utils.TableDirection.table, + ) + right = utils.find_table( + values, + "B2", + utils.TableDirection.right, + ) + down = utils.find_table( + values, + "B2", + utils.TableDirection.down, + ) + single = utils.find_table(values, "D1", utils.TableDirection.table) + no_values = utils.find_table(values, "A2", utils.TableDirection.table) + + table_values = [ + ["B2", "C2"], + ["B3", "C3"], + ] + + for rowindex, row in enumerate(table_values): + self.assertListEqual(row, table[rowindex]) + + table_max_row_max_column_values = [ + ["D3", "E3"], + ["D4", "E4"], + ] + + for rowindex, row in enumerate(table_max_row_max_column): + self.assertListEqual(row, table_max_row_max_column_values[rowindex]) + + right_values = [ + ["B2", "C2"], + ] + for rowindex, row in enumerate(right_values): + self.assertListEqual(row, right[rowindex]) + + bottom_values = [ + ["B2"], + ["B3"], + ] + for rowindex, row in enumerate(bottom_values): + self.assertListEqual(row, down[rowindex]) + + self.assertEqual(len(single), 1) + self.assertEqual(len(single[0]), 1) + self.assertEqual(single[0][0], "D1") + self.assertEqual(no_values, []) + + def test_find_table_inner_gap(self): + """Test find table with gap in header""" + values = [ + ["A1", "B1", "C1", ""], + ["A2", "", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1", "B1", "C1"], + ["A2", "", "C2"], + ["A3", "B3", "C3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) + + def test_find_table_first_row_gap(self): + """Test find table with first cell empty""" + values = [ + ["A1", "", "C1", ""], + ["A2", "B2", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1"], + ["A2"], + ["A3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) + + def test_find_table_first_column_gap(self): + """Test find table with a gap in first column""" + values = [ + ["A1", "B1", "C1", ""], + ["", "B2", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1", "B1", "C1"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) + + def test_find_table_last_column_gap(self): + """Test find table with a gap in last column""" + values = [ + ["A1", "B1", "C1", ""], + ["A2", "B2", "", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1", "B1", "C1"], + ["A2", "B2", ""], + ["A3", "B3", "C3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) + + def test_find_table_empty_top_left_corner(self): + """Test find table with an empty top left cell and empty adjacent cells""" + + values = [ + ["", "", "C1", ""], + ["", "B2", "C2", ""], + ["", "B3", "C3", ""], + ] + + table = utils.find_table(values, "A1", utils.TableDirection.table) + + self.assertListEqual(table, [], "resulting table should be empty") diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index ef362ea91..f8523b5bd 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1,4 +1,5 @@ import itertools +import pickle # nosec import random import re from inspect import signature @@ -211,6 +212,43 @@ def test_get_values_and_combine_merged_cells(self): values_with_merged = self.sheet.get_values("A1:D4", combine_merged_cells=True) self.assertEqual(values_with_merged, expected_merge) + @pytest.mark.vcr() + def test_batch_merged_cells(self): + self.sheet.resize(4, 4) + sheet_data = [ + ["1", "", "", ""], + ["", "", "title", ""], + ["", "", "2", ""], + ["num", "val", "", "0"], + ] + + self.sheet.update(sheet_data, "A1:D4") + + self.sheet.batch_merge( + [ + {"range": "A1:B2"}, + {"range": "C2:D2"}, + {"range": "C3:C4"}, + ] + ) + + expected_merge = [ + ["1", "1", "", ""], + ["1", "1", "title", "title"], + ["", "", "2", ""], + ["num", "val", "2", "0"], + ] + + values = self.sheet.get_values() + values_with_merged = self.sheet.get_values(combine_merged_cells=True) + + self.assertEqual(values, sheet_data) + self.assertEqual(values_with_merged, expected_merge) + + # test with cell address + values_with_merged = self.sheet.get_values("A1:D4", combine_merged_cells=True) + self.assertEqual(values_with_merged, expected_merge) + @pytest.mark.vcr() def test_get_values_with_args_or_kwargs(self): # test that get_values accepts args and kwargs @@ -1962,4 +2000,10 @@ def test_add_validation(self): {"spreadsheetId": self.spreadsheet.id, "replies": [{}]}, ) - self.assertRaises(APIError, sheet.update, values="X", range_name="A1") + with self.assertRaises(APIError) as ex: + sheet.update(values="X", range_name="A1") + + # Ensure that the exception is able to be pickled and unpickled + # Further ensure we are able to access the exception's properties after pickling + reloaded_exception = pickle.loads(pickle.dumps(ex.exception)) # nosec + self.assertEqual(reloaded_exception.args[0]["status"], "INVALID_ARGUMENT")