From 09e46eaf7ba60de9f408431ddfa070ff4e7adfe9 Mon Sep 17 00:00:00 2001 From: Noah Czelusta Date: Thu, 16 Mar 2023 09:01:00 +0900 Subject: [PATCH 1/7] Added column_index_to_letter function with tests --- gspread/utils.py | 27 +++++++++++++++++++++++++++ tests/utils_test.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/gspread/utils.py b/gspread/utils.py index d6e74fd43..2c2f402ef 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -482,6 +482,33 @@ def column_letter_to_index(column): return index +def column_index_to_letter(column: int) -> str: + """Converts a column's numerical index to its letter + + This fucntion is case insensitive + + :param str letter: An int corrensponding to the column's numerical index. + Indexed from 1. + :returns: A column label in A1 notation, e.g. 'B'. + Letter case is ignored. + :rtype: int + + Raises :exc:`gspread.exceptions.InvalidInputValue` in case of invalid input. + + Example: + + >>> a1_to_rowcol(1) + 'A' + """ + + if not isinstance(column, int) or column < 1: + raise InvalidInputValue( + f'invalid value: {column}, must be an int >= 1' + ) + + return rowcol_to_a1(1, column).strip('1') + + def cast_to_a1_notation(method): """Decorator function casts wrapped arguments to A1 notation in range method calls. diff --git a/tests/utils_test.py b/tests/utils_test.py index 0f1508caf..473aa77d8 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -218,3 +218,33 @@ def test_column_letter_to_index(self): label, expected ), ) + + def test_column_index_to_letter(self): + # All the input values to test one after an other + # [0] input value + # [1] expected return value + # [2] expected exception to raise + inputs = [ + ("", None, gspread.exceptions.InvalidInputValue), + (1, 'A', None), + (26, 'Z', None), + (27, 'AA', None), + (703, 'AAA', None), + (256094574536617744129141650397448476, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', None), + ] + + for label, expected, exception in inputs: + if exception is not None: + # assert the exception is raised + with self.assertRaises(exception): + utils.column_letter_to_index(label) + else: + # assert the return values is correct + result = utils.column_index_to_letter(label) + self.assertEqual( + result, + expected, + "could not convert column index '{}' to the right letter '{}".format( + label, expected + ), + ) \ No newline at end of file From 486e4e608d8a00ed3a70c58c95d18df95fbac9fc Mon Sep 17 00:00:00 2001 From: Noah Czelusta Date: Sat, 18 Mar 2023 09:44:08 +0900 Subject: [PATCH 2/7] Added extract_title_from_range to utils.py --- gspread/utils.py | 22 ++++++++++++++++++++++ tests/utils_test.py | 28 +++++++++++++++++++++------- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/gspread/utils.py b/gspread/utils.py index 2c2f402ef..0943261d7 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -27,6 +27,8 @@ URL_KEY_V1_RE = re.compile(r"key=([^&#]+)") URL_KEY_V2_RE = re.compile(r"/spreadsheets/d/([a-zA-Z0-9-_]+)") +TITLE_RANGE_RE = re.compile(r"'(.*?)'!.*") + Dimension = namedtuple("Dimension", ["rows", "cols"])("ROWS", "COLUMNS") ValueRenderOption = namedtuple( "ValueRenderOption", ["formatted", "unformatted", "formula"] @@ -546,6 +548,26 @@ def extract_id_from_url(url): raise NoValidUrlKeyFound +def extract_title_from_range(range_string: str) -> str: + """ Will extract the sheet title from a range. + + :param str letter: A range string + :returns: the title of the worksheet from the range string + :rtype: str + + Raises :exc: `gspread.exceptions.InvalidInputValue` + + Example: + + >>> extract_title_from_range("'Volunteer Portal'!A1:Z1005" -> "Volunteer Portal") + 'Volunteer Portal' + """ + match = TITLE_RANGE_RE.search(range_string) + if match: + return match.group(1) + + raise InvalidInputValue + def wid_to_gid(wid): """Calculate gid of a worksheet from its wid.""" widval = wid[1:] if len(wid) > 3 else wid diff --git a/tests/utils_test.py b/tests/utils_test.py index 473aa77d8..57b27d017 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -241,10 +241,24 @@ def test_column_index_to_letter(self): else: # assert the return values is correct result = utils.column_index_to_letter(label) - self.assertEqual( - result, - expected, - "could not convert column index '{}' to the right letter '{}".format( - label, expected - ), - ) \ No newline at end of file + self.assertEqual(result, expected) + + def test_extract_title_from_range(self): + # All the input values to test one after an other + # [0] input value + # [1] expected return value + # [2] expected exception to raise + inputs = [ + ("asdf", None, gspread.exceptions.InvalidInputValue), + ("'Volunteer Portal'!A1:Z1005", "Volunteer Portal", None), + ] + + for label, expected, exception in inputs: + if exception is not None: + # assert the exception is raised + with self.assertRaises(exception): + utils.extract_title_from_range(label) + else: + # assert the return values is correct + result = utils.extract_title_from_range(label) + self.assertEqual(result, expected) \ No newline at end of file From 3428a1752c0397db6fa617d19b889db3958370c3 Mon Sep 17 00:00:00 2001 From: Noah Czelusta Date: Sat, 18 Mar 2023 09:56:46 +0900 Subject: [PATCH 3/7] get_all_worksheet_values implemented --- gspread/spreadsheet.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index 5d5a2bb37..71c47a52a 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -20,8 +20,14 @@ SPREADSHEET_VALUES_CLEAR_URL, SPREADSHEET_VALUES_URL, ) -from .utils import ExportFormat, finditem, quote -from .worksheet import Worksheet +from .utils import ( + ExportFormat, + finditem, quote, + column_index_to_letter, + extract_title_from_range, + fill_gaps, +) +from .worksheet import Worksheet, ValueRange class Spreadsheet: @@ -739,3 +745,30 @@ def list_protected_ranges(self, sheetid): raise WorksheetNotFound("worksheet id {} not found".format(sheetid)) return sheet.get("protectedRanges", []) + + def get_all_worksheet_values(self, skip_worksheet_titles: list[str] = None): + """ Grabs all the data from all the worksheets in one API call. Skips any worksheets that were named in the + skip_worksheet_title parm. + :returns Dict of worksheet data with worksheet title as key + """ + + if skip_worksheet_titles is None: + skip_worksheet_titles = [] + + ranges = [] + + for worksheet in self.google_sheet.worksheets(): + if worksheet.title not in skip_worksheet_titles: + ranges.append(f'{worksheet.title}!A1:{column_index_to_letter(worksheet.col_count)}') + + values = self.google_sheet.values_batch_get( + ranges=ranges + ) + + return_data = {} + + for values in values['valueRanges']: + value_range = ValueRange.from_json(values) + return_data[extract_title_from_range(value_range.range)] = fill_gaps(value_range) + + return return_data From 09200aac993db88fa270ba472b0b72cae7cd3d73 Mon Sep 17 00:00:00 2001 From: Noah Czelusta Date: Sat, 18 Mar 2023 10:00:42 +0900 Subject: [PATCH 4/7] Format and lint --- gspread/spreadsheet.py | 23 +++++++++++++---------- gspread/utils.py | 17 ++++++++--------- tests/utils_test.py | 12 ++++++------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index 71c47a52a..9d927d1ac 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -22,12 +22,13 @@ ) from .utils import ( ExportFormat, - finditem, quote, column_index_to_letter, extract_title_from_range, fill_gaps, + finditem, + quote, ) -from .worksheet import Worksheet, ValueRange +from .worksheet import ValueRange, Worksheet class Spreadsheet: @@ -747,8 +748,8 @@ def list_protected_ranges(self, sheetid): return sheet.get("protectedRanges", []) def get_all_worksheet_values(self, skip_worksheet_titles: list[str] = None): - """ Grabs all the data from all the worksheets in one API call. Skips any worksheets that were named in the - skip_worksheet_title parm. + """Grabs all the data from all the worksheets in one API call. Skips any worksheets that were named in the + skip_worksheet_title param. :returns Dict of worksheet data with worksheet title as key """ @@ -759,16 +760,18 @@ def get_all_worksheet_values(self, skip_worksheet_titles: list[str] = None): for worksheet in self.google_sheet.worksheets(): if worksheet.title not in skip_worksheet_titles: - ranges.append(f'{worksheet.title}!A1:{column_index_to_letter(worksheet.col_count)}') + ranges.append( + f"{worksheet.title}!A1:{column_index_to_letter(worksheet.col_count)}" + ) - values = self.google_sheet.values_batch_get( - ranges=ranges - ) + values = self.google_sheet.values_batch_get(ranges=ranges) return_data = {} - for values in values['valueRanges']: + for values in values["valueRanges"]: value_range = ValueRange.from_json(values) - return_data[extract_title_from_range(value_range.range)] = fill_gaps(value_range) + return_data[extract_title_from_range(value_range.range)] = fill_gaps( + value_range + ) return return_data diff --git a/gspread/utils.py b/gspread/utils.py index 0943261d7..1e065323e 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -487,9 +487,9 @@ def column_letter_to_index(column): def column_index_to_letter(column: int) -> str: """Converts a column's numerical index to its letter - This fucntion is case insensitive + This function is case insensitive - :param str letter: An int corrensponding to the column's numerical index. + :param str letter: An int corresponding to the column's numerical index. Indexed from 1. :returns: A column label in A1 notation, e.g. 'B'. Letter case is ignored. @@ -504,11 +504,9 @@ def column_index_to_letter(column: int) -> str: """ if not isinstance(column, int) or column < 1: - raise InvalidInputValue( - f'invalid value: {column}, must be an int >= 1' - ) + raise InvalidInputValue(f"invalid value: {column}, must be an int >= 1") - return rowcol_to_a1(1, column).strip('1') + return rowcol_to_a1(1, column).strip("1") def cast_to_a1_notation(method): @@ -549,8 +547,8 @@ def extract_id_from_url(url): def extract_title_from_range(range_string: str) -> str: - """ Will extract the sheet title from a range. - + """Will extract the sheet title from a range. + :param str letter: A range string :returns: the title of the worksheet from the range string :rtype: str @@ -565,9 +563,10 @@ def extract_title_from_range(range_string: str) -> str: match = TITLE_RANGE_RE.search(range_string) if match: return match.group(1) - + raise InvalidInputValue + def wid_to_gid(wid): """Calculate gid of a worksheet from its wid.""" widval = wid[1:] if len(wid) > 3 else wid diff --git a/tests/utils_test.py b/tests/utils_test.py index 57b27d017..7de88147e 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -226,11 +226,11 @@ def test_column_index_to_letter(self): # [2] expected exception to raise inputs = [ ("", None, gspread.exceptions.InvalidInputValue), - (1, 'A', None), - (26, 'Z', None), - (27, 'AA', None), - (703, 'AAA', None), - (256094574536617744129141650397448476, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', None), + (1, "A", None), + (26, "Z", None), + (27, "AA", None), + (703, "AAA", None), + (256094574536617744129141650397448476, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", None), ] for label, expected, exception in inputs: @@ -261,4 +261,4 @@ def test_extract_title_from_range(self): else: # assert the return values is correct result = utils.extract_title_from_range(label) - self.assertEqual(result, expected) \ No newline at end of file + self.assertEqual(result, expected) From 5e0da119d01d11280b8d1ff9dd8b5aa80d68a5f7 Mon Sep 17 00:00:00 2001 From: swimninja247 Date: Wed, 12 Apr 2023 23:36:01 -0400 Subject: [PATCH 5/7] Made requested changes, linted + formatted --- gspread/spreadsheet.py | 19 +++++-------------- gspread/utils.py | 25 ------------------------- tests/utils_test.py | 24 ------------------------ 3 files changed, 5 insertions(+), 63 deletions(-) diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index 9d927d1ac..1197f675c 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -20,14 +20,7 @@ SPREADSHEET_VALUES_CLEAR_URL, SPREADSHEET_VALUES_URL, ) -from .utils import ( - ExportFormat, - column_index_to_letter, - extract_title_from_range, - fill_gaps, - finditem, - quote, -) +from .utils import ExportFormat, extract_title_from_range, fill_gaps, finditem, quote from .worksheet import ValueRange, Worksheet @@ -747,7 +740,7 @@ def list_protected_ranges(self, sheetid): return sheet.get("protectedRanges", []) - def get_all_worksheet_values(self, skip_worksheet_titles: list[str] = None): + def get_all_worksheet_values(self, skip_worksheet_titles: list = None): """Grabs all the data from all the worksheets in one API call. Skips any worksheets that were named in the skip_worksheet_title param. :returns Dict of worksheet data with worksheet title as key @@ -758,13 +751,11 @@ def get_all_worksheet_values(self, skip_worksheet_titles: list[str] = None): ranges = [] - for worksheet in self.google_sheet.worksheets(): + for worksheet in self.worksheets().worksheets(): if worksheet.title not in skip_worksheet_titles: - ranges.append( - f"{worksheet.title}!A1:{column_index_to_letter(worksheet.col_count)}" - ) + ranges.append(worksheet.title) - values = self.google_sheet.values_batch_get(ranges=ranges) + values = self.worksheet.values_batch_get(ranges=ranges) return_data = {} diff --git a/gspread/utils.py b/gspread/utils.py index 1e065323e..ac95d1853 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -484,31 +484,6 @@ def column_letter_to_index(column): return index -def column_index_to_letter(column: int) -> str: - """Converts a column's numerical index to its letter - - This function is case insensitive - - :param str letter: An int corresponding to the column's numerical index. - Indexed from 1. - :returns: A column label in A1 notation, e.g. 'B'. - Letter case is ignored. - :rtype: int - - Raises :exc:`gspread.exceptions.InvalidInputValue` in case of invalid input. - - Example: - - >>> a1_to_rowcol(1) - 'A' - """ - - if not isinstance(column, int) or column < 1: - raise InvalidInputValue(f"invalid value: {column}, must be an int >= 1") - - return rowcol_to_a1(1, column).strip("1") - - def cast_to_a1_notation(method): """Decorator function casts wrapped arguments to A1 notation in range method calls. diff --git a/tests/utils_test.py b/tests/utils_test.py index 7de88147e..7213aef46 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -219,30 +219,6 @@ def test_column_letter_to_index(self): ), ) - def test_column_index_to_letter(self): - # All the input values to test one after an other - # [0] input value - # [1] expected return value - # [2] expected exception to raise - inputs = [ - ("", None, gspread.exceptions.InvalidInputValue), - (1, "A", None), - (26, "Z", None), - (27, "AA", None), - (703, "AAA", None), - (256094574536617744129141650397448476, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", None), - ] - - for label, expected, exception in inputs: - if exception is not None: - # assert the exception is raised - with self.assertRaises(exception): - utils.column_letter_to_index(label) - else: - # assert the return values is correct - result = utils.column_index_to_letter(label) - self.assertEqual(result, expected) - def test_extract_title_from_range(self): # All the input values to test one after an other # [0] input value From 4e7561e5bdd30ee79868d7b5afc8ad907d38acb6 Mon Sep 17 00:00:00 2001 From: swimninja247 Date: Wed, 12 Apr 2023 23:43:40 -0400 Subject: [PATCH 6/7] Fixed docstrings formatting --- gspread/spreadsheet.py | 5 ++++- gspread/utils.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index 1197f675c..cc1d94540 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -743,7 +743,10 @@ def list_protected_ranges(self, sheetid): def get_all_worksheet_values(self, skip_worksheet_titles: list = None): """Grabs all the data from all the worksheets in one API call. Skips any worksheets that were named in the skip_worksheet_title param. - :returns Dict of worksheet data with worksheet title as key + + :param list skip_worksheet_titles: A list of worksheet titles to skip. + :returns: a dict of worksheet data with worksheet title as key + :rtype: dict """ if skip_worksheet_titles is None: diff --git a/gspread/utils.py b/gspread/utils.py index ac95d1853..319fec03c 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -528,7 +528,8 @@ def extract_title_from_range(range_string: str) -> str: :returns: the title of the worksheet from the range string :rtype: str - Raises :exc: `gspread.exceptions.InvalidInputValue` + :raises: + :class:`~gspread.exceptions.InvalidInputValue`: if can't extract a title Example: From 40492861020533277d09624bf7d220281346e38f Mon Sep 17 00:00:00 2001 From: swimninja247 Date: Wed, 12 Apr 2023 23:46:35 -0400 Subject: [PATCH 7/7] Fixed a typo --- gspread/spreadsheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index cc1d94540..69ae98512 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -758,7 +758,7 @@ def get_all_worksheet_values(self, skip_worksheet_titles: list = None): if worksheet.title not in skip_worksheet_titles: ranges.append(worksheet.title) - values = self.worksheet.values_batch_get(ranges=ranges) + values = self.values_batch_get(ranges=ranges) return_data = {}