Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added set_records and set_record to update sheet via records.[Issue #677] #1494

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cef9fb3
Added a column headers property and getters [Issue #677]
muddi900 Jul 17, 2024
43a8ac2
Created basic set_row & set_rows methods.
muddi900 Jul 17, 2024
d0162b1
using insert_rows instead of update
muddi900 Jul 17, 2024
708b1c1
Using `append_rows` to handle input.
muddi900 Jul 20, 2024
5e13729
Added options to handle missinge headers and extra headers
muddi900 Jul 21, 2024
1829c07
added a test for set_records.
muddi900 Jul 21, 2024
e2c1151
removed uncessary code.
muddi900 Jul 21, 2024
7437dca
fixed the unique headers test.
muddi900 Jul 21, 2024
6ae9b12
added `ValueRenderOption` to mitigate type errors.
muddi900 Jul 21, 2024
f1d8d5a
started the docstring for `set_records`.
muddi900 Jul 21, 2024
eb06148
made test values more generic.
muddi900 Jul 23, 2024
aac2a0f
Renamed the functions to `append_` to be more accurate.
muddi900 Jul 24, 2024
97b11c8
Added `insert_records`/`insert_record` and related tests.
muddi900 Jul 24, 2024
907980b
fix tests to better incorporate `insert_row` behavior.
muddi900 Jul 25, 2024
25351ed
Completed docstring for `append_records`
muddi900 Jul 28, 2024
5b3f90d
Added all the doc strings.
muddi900 Jul 28, 2024
2411790
removed extraneous statement
muddi900 Oct 26, 2024
d5f85d5
removed getters and cache.
muddi900 Oct 26, 2024
d36a7d2
better arg passthrough
muddi900 Oct 26, 2024
dd76f2b
Updated the docs
muddi900 Oct 26, 2024
1bb6f4e
Added doc string for `get_column_headers`
muddi900 Nov 26, 2024
68a79fe
Added a default `value_input_option`
muddi900 Nov 26, 2024
439e89a
More explicit test values
muddi900 Nov 26, 2024
5764eb0
Default `ValueInputOption` for `insert_records`
muddi900 Nov 26, 2024
b4f68a8
Fixed the column header method
muddi900 Nov 26, 2024
48679a3
Explicit test values for `insert_records`
muddi900 Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 192 additions & 1 deletion gspread/worksheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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__,
Expand Down Expand Up @@ -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."""

Expand Down
104 changes: 104 additions & 0 deletions tests/worksheet_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading