|
10 | 10 | from urllib.parse import quote |
11 | 11 |
|
12 | 12 | from .drive import File |
13 | | -from .utils import ApiComponent, TrackerSet, to_snake_case |
| 13 | +from .utils import ApiComponent, TrackerSet, to_snake_case, col_index_to_label |
14 | 14 |
|
15 | 15 | log = logging.getLogger(__name__) |
16 | 16 |
|
@@ -1948,6 +1948,74 @@ def add_named_range(self, name, reference, comment="", is_formula=False): |
1948 | 1948 | parent=self, **{self._cloud_data_key: response.json()} |
1949 | 1949 | ) |
1950 | 1950 |
|
| 1951 | + def update_cells(self, address, rows): |
| 1952 | + """ |
| 1953 | + Updates the cells at a given range in this worksheet. This is a convenience method since there is no |
| 1954 | + direct endpoint API for tableless row updates. |
| 1955 | + :param str|Range address: the address to resolve to a range which can be used for updating cells. |
| 1956 | + :param list[list[str]] rows: list of rows to push to this range. If updating a single cell, pass a list |
| 1957 | + containing a single row (list) containing a single cell worth of data. |
| 1958 | + """ |
| 1959 | + if isinstance(address, str): |
| 1960 | + address = self.get_range(address) |
| 1961 | + |
| 1962 | + if not isinstance(address, Range): |
| 1963 | + raise ValueError("address was not an accepted type: str or Range") |
| 1964 | + |
| 1965 | + if not isinstance(rows, list): |
| 1966 | + raise ValueError("rows was not an accepted type: list[list[str]]") |
| 1967 | + |
| 1968 | + # Let's not even try pushing to API if the range rectangle mismatches the input row and column count. |
| 1969 | + row_count = len(rows) |
| 1970 | + col_count = len(rows[0]) if row_count > 0 else 1 |
| 1971 | + |
| 1972 | + if address.row_count != row_count or address.column_count != col_count: |
| 1973 | + raise ValueError("rows and columns are not the same size as the range selected. This is required by the Microsoft Graph API.") |
| 1974 | + |
| 1975 | + address.values = rows |
| 1976 | + address.update() |
| 1977 | + |
| 1978 | + def append_rows(self, rows): |
| 1979 | + """ |
| 1980 | + Appends rows to the end of a worksheet. There is no direct Graph API to do this operation without a Table |
| 1981 | + instance. Instead, this method identifies the last row in the worksheet and requests a range after that row |
| 1982 | + and updates that range. |
| 1983 | +
|
| 1984 | + Beware! If you open your workbook from sharepoint and delete all of the rows in one go and attempt to append |
| 1985 | + new rows, you will get undefined behavior from the Microsoft Graph API. I don't know if I did not give enough |
| 1986 | + time for the backend to synchronize from the moment of deletion on my browser and the moment I triggered my |
| 1987 | + script, but this is something I have observed. Sometimes insertion fails and sometimes it inserts where the new |
| 1988 | + row would have been if data had not been deleted from the browser side. Maybe it is an API cache issue. However, |
| 1989 | + after the first row is inserted successfully, this undefined behavior goes away on repeat calls to my scripts. |
| 1990 | + Documenting this behavior for future consumers of this API. |
| 1991 | +
|
| 1992 | + :param list[list[str]] rows: list of rows to push to this range. If updating a single cell, pass a list |
| 1993 | + containing a single row (list) containing a single cell worth of data. |
| 1994 | + """ |
| 1995 | + row_count = len(rows) |
| 1996 | + col_count = len(rows[0]) if row_count > 0 else 0 |
| 1997 | + col_index = col_count - 1 |
| 1998 | + |
| 1999 | + # Find the last row index so we can grab a range after it. |
| 2000 | + current_range = self.get_used_range() |
| 2001 | + # Minor adjustment because Graph will return [['']] in an empty worksheet. |
| 2002 | + # Also, beware that Graph might report ghost values if testing using the front end site and that can be interesting |
| 2003 | + # during debugging. I ctrl + A and delete then click elsewhere before testing again. |
| 2004 | + # Might also take a moment for the backend to eventually catch up to the changes. |
| 2005 | + # Graph can be weirdly slow. It might be an institution thing. |
| 2006 | + if current_range.row_count == 1 and len(current_range.values[0]) == 1 and current_range.values[0][0] == '': |
| 2007 | + current_range.values = [] |
| 2008 | + current_range.row_count = 0 |
| 2009 | + |
| 2010 | + target_index = current_range.row_count |
| 2011 | + |
| 2012 | + # Generate the address needed to outline the bounding rectangle to use to fill in data. |
| 2013 | + col_name = col_index_to_label(col_index) |
| 2014 | + insert_range_address = 'A{}:{}{}'.format(target_index + 1, col_name, target_index + row_count) |
| 2015 | + |
| 2016 | + # Request to push the data to the given range. |
| 2017 | + self.update_cells(insert_range_address, rows) |
| 2018 | + |
1951 | 2019 | def get_named_range(self, name): |
1952 | 2020 | """Retrieves a Named range by it's name""" |
1953 | 2021 | url = self.build_url(self._endpoints.get("get_named_range").format(name=name)) |
|
0 commit comments