Skip to content

Commit

Permalink
Add merge_index to df_to_sheet (#92)
Browse files Browse the repository at this point in the history
Closes #91
  • Loading branch information
aiguofer authored Aug 31, 2023
1 parent 9cd68f5 commit 6580569
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 17 deletions.
78 changes: 67 additions & 11 deletions gspread_pandas/spread.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
from gspread_pandas.util import (
COL,
ROW,
axis_is_column,
axis_is_index,
chunks,
create_filter_request,
create_frozen_request,
create_merge_cells_request,
create_merge_headers_request,
create_merge_index_request,
create_unmerge_cells_request,
fillna,
find_col_indexes,
Expand Down Expand Up @@ -164,6 +167,9 @@ def refresh_spread_metadata(self):
"""Refresh spreadsheet metadata."""
self._spread_metadata = self.spread.fetch_sheet_metadata()

if self.sheet:
self.sheet._properties = self._sheet_metadata["properties"]

@property
def _sheet_metadata(self):
"""`(dict)` - Metadata for currently open worksheet"""
Expand Down Expand Up @@ -655,6 +661,7 @@ def df_to_sheet(
add_filter=False,
merge_headers=False,
flatten_headers_sep=None,
merge_index=False,
):
"""
Save a DataFrame into a worksheet.
Expand Down Expand Up @@ -696,25 +703,32 @@ def df_to_sheet(
if you want to flatten your multi-headers to a single row,
you can pass the string that you'd like to use to concatenate
the levels, for example, ': ' (default None)
merge_index : bool
whether to merge cells in the index that have the same value
(default False)
Returns
-------
None
"""
self._ensure_sheet(sheet)

include_index = index
header = df.columns
index_size = df.index.nlevels if index else 0
header_size = df.columns.nlevels
index = df.index
index_size = index.nlevels if include_index else 0
header_size = header.nlevels

if index:
if include_index:
df = df.reset_index()

df = fillna(df, fill_value)
df_list = df.values.tolist()

if headers:
header_rows = parse_df_col_names(df, index, index_size, flatten_headers_sep)
header_rows = parse_df_col_names(
df, include_index, index_size, flatten_headers_sep
)
df_list = header_rows + df_list

start = get_cell_as_tuple(start)
Expand Down Expand Up @@ -759,16 +773,58 @@ def df_to_sheet(
)

if merge_headers:
self.spread.batch_update(
{
"requests": create_merge_headers_request(
self.sheet.id, header, start, index_size
)
}
)
self._merge_index(start, header, index_size, "columns")

if include_index and merge_index:
self._merge_index(start, index, header_size, "index")

self.refresh_spread_metadata()

def _merge_index(self, start, index, other_axis_size, axis):
"""
Make a request to merge cells with the same values for the given index.
This really only applies to MultiIndex.
"""
if axis_is_index(axis):
create_requests = create_merge_index_request
elif axis_is_column(axis):
create_requests = create_merge_headers_request
else:
raise ValueError("Axis should be 'index' or 'columns'")

self._unmerge_index(start, index, other_axis_size, axis)

requests = create_requests(self.sheet.id, index, start, other_axis_size)

if requests:
self.spread.batch_update({"requests": requests})

def _unmerge_index(self, start, index, other_axis_size, axis):
"""
In order to ensure merged cells still match up for the given
MultiIndex, we need to first unmerge all the cells
"""
dims = self.get_sheet_dims()
if axis_is_index(axis):
ix_start = (
start[ROW] + other_axis_size,
start[COL],
)
ix_end = (
dims[ROW],
start[COL] + index.nlevels - 1,
)
elif axis_is_column(axis):
ix_start = (
start[ROW],
start[COL] + other_axis_size,
)
ix_end = (
start[ROW] + index.nlevels - 1,
dims[COL],
)
self.unmerge_cells(ix_start, ix_end)

def _fix_merge_values(self, vals):
"""
Assign the top-left value to all cells in a merged range.
Expand Down
38 changes: 34 additions & 4 deletions gspread_pandas/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,12 @@ def request(*args, **kwargs):


def create_merge_headers_request(sheet_id, headers, start, index_size):
"""Create v4 API request to merge labels for a given worksheet."""
"""Create v4 API request to merge header labels for a given worksheet."""
request = []
start = get_cell_as_tuple(start)

if isinstance(headers, pd.MultiIndex):
merge_cells = get_col_merge_ranges(headers)
merge_cells = get_merge_ranges(headers)
request.append(
[
create_merge_cells_request(
Expand All @@ -329,7 +329,29 @@ def create_merge_headers_request(sheet_id, headers, start, index_size):
return request


def get_col_merge_ranges(index):
def create_merge_index_request(sheet_id, index, start, header_size):
"""Create v4 API request to merge index labels for a given worksheet."""
request = []
start = get_cell_as_tuple(start)

if isinstance(index, pd.MultiIndex):
merge_cells = get_merge_ranges(index)
request.append(
[
create_merge_cells_request(
sheet_id,
(start[ROW] + row_rng[START] + header_size, start[COL] + col_ix),
(start[ROW] + row_rng[END] + header_size, start[COL] + col_ix),
)
for col_ix, col in enumerate(merge_cells)
for row_rng in col
]
)

return request


def get_merge_ranges(index):
"""
Get list of ranges to be merged for each level of columns.
Expand Down Expand Up @@ -357,7 +379,7 @@ def get_contiguous_ranges(lst, lst_start, lst_end):
Get list of tuples, each indicating the range of contiguous equal values in the lst
between lst_start and lst_end. Everything is 0 indexed.
For example, get_contiguous_ranges([0, 0, 0, 1, 1], 1, 4) = [(1, 2), (3, 4)]
For example, get_contiguous_ranges([0, 0, 0, 1, 1], 1, 4) = [(0, 2), (3, 4)]
[(the 2nd and 3rd items are both 0), (the 4th and 5th items are both 1)]
"""
prev_val = None
Expand Down Expand Up @@ -542,3 +564,11 @@ def find_col_indexes(cols, col_names, col_offset=1):
col_locs += [ix for ix in range(len(loc)) if loc[ix]]
# add 1 because we want the index based on spreadsheet, not python
return [ele + col_offset for ele in set(col_locs)]


def axis_is_index(axis):
return axis in ("index", 0)


def axis_is_column(axis):
return axis in ("columns", 1)
4 changes: 2 additions & 2 deletions tests/util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,14 +300,14 @@ def test_monkey_patch_request(betamax_authorizedsession):


def test_get_col_merge_ranges():
cols = pd.MultiIndex.from_arrays(
ix = pd.MultiIndex.from_arrays(
[
["col1", "col1", "col2", "col2"],
["subcol1", "subcol1", "subcol1", "subcol1"],
["subsubcol1", "subsubcol2", "subsubcol2", "subsubcol2"],
]
)
assert util.get_col_merge_ranges(cols) == [
assert util.get_merge_ranges(ix) == [
[(0, 1), (2, 3)],
[(0, 1), (2, 3)],
[(2, 3)],
Expand Down

0 comments on commit 6580569

Please sign in to comment.