diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 31ba6a67..c6fff0e1 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -31,7 +31,7 @@ from .cell import Cell from .exceptions import GSpreadException from .http_client import HTTPClient, ParamsType -from .urls import WORKSHEET_DRIVE_URL +from .urls import WORKSHEET_DRIVE_URL, SPREADSHEET_VALUES_APPEND_URL from .utils import ( DateTimeOption, Dimension, @@ -200,6 +200,16 @@ def __init__( # do not use if possible. self._spreadsheet = spreadsheet + def get_column_headers(self, header_row: Optional[int] = None) -> List[str]: + """Get the column headers a list of strings. + + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + + """ + + return self.row_values(header_row or 1) + def __repr__(self) -> str: return "<{} {} id:{}>".format( self.__class__.__name__, @@ -608,6 +618,187 @@ def get_all_records( return to_records(keys, values) + def append_records( + self, + rows: List[Dict[str, Any]], + ignore_extra_headers: bool = False, + default_blank: Any = "", + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, + ) -> None: + """Appends records as rows to your data range. + + :param rows: A list of dictionaries, where each dictionary represents a row to be appended. + The keys of the dictionaries should correspond to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. + :type default_blank: Any + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` + + :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. + """ + + cols = self.get_column_headers(header_row) + insert_rows = [] + for row in rows: + if not set(row).issubset(set(cols)) and not ignore_extra_headers: + raise GSpreadException("Extra headers found in the data set") + + insert_row = [] + for col in cols: + insert_row.append(row.get(col, default_blank)) + insert_rows.append(insert_row) + + self.append_rows( + insert_rows, + value_input_option=value_input_option or ValueInputOption.raw, + ) + + def append_record( + self, + row: Dict[str, Any], + ignore_extra_headers: bool = False, + default_blank: Any = "", + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, + ) -> None: + """Appends a dict as a row to your data range. + + :param row: A dict. The keys of the dict should + correspond to the column headers of the spreadsheet. + :type row: Dict[str, Any] + :param ignore_extra_headers: If True, extra headers found in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. + :type default_blank: Any + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` + + :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. + """ + + self.append_records( + [row], + ignore_extra_headers=ignore_extra_headers, + default_blank=default_blank, + header_row=header_row, + value_input_option=value_input_option, + ) + + def insert_records( + self, + rows: List[Dict[str, Any]], + ignore_extra_headers: bool = False, + default_blank: Any = "", + insert_row: int = 2, + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, + ) -> None: + """Insert records as rows to your data range at the stated row. + + :param rows: A list of dictionaries, where + each dictionary represents a row to be inserted. + The keys of the dictionaries should correspond + to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found + in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing + columns in the data. Defaults to an empty string. + :type default_blank: Any + :param insert_row: Row number(1-indexed) where the + data would be inserted. Defaults to 2. + :type insert_row: int + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` + + :raises GSpreadException: If extra headers are found + in the data set and `ignore_extra_headers` is False. + """ + + cols = self.get_column_headers() + insert_rows = [] + for row in rows: + if not set(row).issubset(set(cols)) and not ignore_extra_headers: + raise GSpreadException("Extra headers found in the data set") + + ins_row = [] + for col in cols: + ins_row.append(row.get(col, default_blank)) + insert_rows.append(ins_row) + + self.insert_rows( + insert_rows, + row=insert_row, + value_input_option=value_input_option or ValueInputOption.raw, + header_row=header_row, + ) + + def insert_record( + self, + row: Dict[str, Any], + ignore_extra_headers: bool = False, + default_blank: Any = "", + insert_row: int = 2, + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, + ) -> None: + """Insert a dict as rows to your data range at the stated row. + + :param row: A dict, where + each dictionary represents a row to be inserted. + The keys of the dictionaries should correspond + to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found + in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing + columns in the data. Defaults to an empty string. + :type default_blank: Any + :param insert_row: Row number(1-indexed) where the + data would be inserted. Defaults to 2. + :type insert_row: int + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` + + :raises GSpreadException: If extra headers are found + in the data set and `ignore_extra_headers` is False. + """ + + self.insert_records( + [row], + ignore_extra_headers=ignore_extra_headers, + insert_row=insert_row, + default_blank=default_blank, + header_row=header_row, + value_input_option=value_input_option, + ) + def get_all_cells(self) -> List[Cell]: """Returns a list of all `Cell` of the current sheet.""" diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index f8523b5b..6e0ddc0c 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1751,6 +1751,110 @@ def test_batch_clear(self): self.assertListEqual(w.get_values("A1:B1"), [[]]) self.assertListEqual(w.get_values("C2:E2"), [[]]) + @pytest.mark.vcr() + def test_append_records(self): + + w = self.spreadsheet.sheet1 + values_before = [ + ["header1", "header2"], + ["value1", "value2"], + ] + w.update(values_before, "A1:B2") + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values_before, + ) + update_values = [ + {"header1": "new value1", "header2": "new value2"}, + {"header1": "new value3"}, + ] + w.append_records(update_values) + values_after = [ + ["header1", "header2"], + ["value1", "value2"], + ["new value1", "new value2"], + ["new value3", ""], + ] + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values_after, + ) + with pytest.raises(GSpreadException): + w.append_record({"header1": "error value1", "location": "error value2"}) + + w.append_record( + { + "header1": "single entry1", + "status": "active", + "header2": "single entry2", + }, + ignore_extra_headers=True, + ) + values_after_single_entry = [ + ["header1", "header2"], + ["value1", "value2"], + ["new value1", "new value2"], + ["new value3", ""], + ["single entry1", "single entry2"], + ] + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values_after_single_entry, + ) + + @pytest.mark.vcr() + def test_insert_records(self): + w = self.spreadsheet.sheet1 + values_before = [ + ["header1", "header2"], + ["value1", "value2"], + ] + w.update(values_before, "A1:B2") + values_after = [ + ["header1", "header2"], + ["value3", "value4"], + ["", "value5"], + ["value1", "value2"], + ] + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values_before, + ) + + w.insert_records( + [ + {"header1": "value3", "header2": "value4"}, + {"header2": "value5"}, + ] + ) + + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values_after, + ) + + with pytest.raises(GSpreadException): + w.insert_record({"header1": "error value1", "location": "error value2"}) + + w.insert_record( + {"header4": "ignore value", "header1": "value6", "header2": "value7"}, + insert_row=4, + ignore_extra_headers=True, + ) + + values_after_single_entry = [ + ["header1", "header2"], + ["value3", "value4"], + ["", "value5"], + ["value6", "value7"], + ["value1", "value2"], + ] + + self.assertEqual( + w.get_all_values(), + values_after_single_entry, + ) + @pytest.mark.vcr() def test_group_columns(self): w = self.sheet