diff --git a/README.md b/README.md index fb8127d..4a271c2 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,10 @@ Features: * Work with range of cells easily with DataRange and Gridrange * Data validation support. checkboxes, drop-downs etc. * Conditional formatting support -* Offline calls batching support -* get multiple ranges with get_values_batch +* get multiple ranges with get_values_batch and update wit update_values_batch ## Updates -* version [2.0.4](https://github.com/nithinmurali/pygsheets/releases/tag/2.0.4) released +* version [2.0.5](https://github.com/nithinmurali/pygsheets/releases/tag/2.0.5) released ## Installation @@ -38,11 +37,11 @@ pip install https://github.com/nithinmurali/pygsheets/archive/staging.zip ``` -If you are installing from github please see the docs [here](https://pygsheets.readthedocs.io/en/latest/). +If you are installing from github please see the docs [here](https://pygsheets.readthedocs.io/en/staging/). ## Basic Usage -Basic features are shown here, for complete set of features see the full documentation [here](http://pygsheets.readthedocs.io/en/stable/). +Basic features are shown here, for complete set of features see the full documentation [here](http://pygsheets.readthedocs.io/en/staging/). 1. Obtain OAuth2 credentials from Google Developers Console for __google spreadsheet api__ and __drive api__ and save the file as `client_secret.json` in same directory as project. [read more here.](https://pygsheets.readthedocs.io/en/latest/authorization.html) @@ -108,7 +107,7 @@ sh = gc.open("pygsheetTest") sht1 = gc.open_by_key('1mwA-NmvjDqd3A65c8hsxOpqdfdggPR0fgfg5nXRKScZAuM') # create a spreadsheet in a folder (by id) -sht2 = gc.create("new sheet", folder="adF345vfvcvby67ddfc") +sht2 = gc.create("new sheet", folder_name="my worksheets") # open enable TeamDrive support gc.drive.enable_team_drive("Dqd3A65c8hsxOpqdfdggPR0fgfg") @@ -175,6 +174,9 @@ cell_matrix = wks.get_all_values(returnas='matrix') # update a range of values with a cell list or matrix wks.update_values(crange='A1:E10', values=values_mat) +# update multiple ranges with bath update +wks.update_values_batch(['A1:A2', 'B1:B2'], [[[1],[2]], [[3],[4]]]) + # Insert 2 rows after 20th row and fill with values wks.insert_rows(row=20, number=2, values=values_list) diff --git a/pygsheets/__init__.py b/pygsheets/__init__.py index 3410d90..405f166 100644 --- a/pygsheets/__init__.py +++ b/pygsheets/__init__.py @@ -8,7 +8,7 @@ """ -__version__ = '2.0.4' +__version__ = '2.0.5' __author__ = 'Nithin Murali' from pygsheets.authorization import authorize diff --git a/pygsheets/address.py b/pygsheets/address.py index 047798b..c6dd000 100644 --- a/pygsheets/address.py +++ b/pygsheets/address.py @@ -429,6 +429,7 @@ def _calculate_label(self): def _calculate_addresses(self, label): """ update values from label """ self._start, self._end = Address(None, True), Address(None, True) + if len(label.split('!')) > 1: self.worksheet_title = label.split('!')[0] rem = label.split('!')[1] @@ -437,8 +438,15 @@ def _calculate_addresses(self, label): self._end = Address(rem.split(":")[1], allow_non_single=True) else: self._start = Address(rem, allow_non_single=True) + elif self._worksheet: + if ":" in label: + self._start = Address(label.split(":")[0], allow_non_single=True) + self._end = Address(label.split(":")[1], allow_non_single=True) + else: + self._start = Address(label, allow_non_single=True) else: pass + self._apply_index_constraints() def to_json(self): diff --git a/pygsheets/datarange.py b/pygsheets/datarange.py index 62f67ad..a417f13 100644 --- a/pygsheets/datarange.py +++ b/pygsheets/datarange.py @@ -437,6 +437,7 @@ def set_json(self, api_obj): self.description = api_obj.get('description', '') self.editors = api_obj.get('editors', {}) self.warningOnly = api_obj.get('warningOnly', False) + self.requestingUserCanEdit = api_obj.get('requestingUserCanEdit', None) def to_json(self): api_obj = { diff --git a/pygsheets/drive.py b/pygsheets/drive.py index 9bdd0f8..aa7a000 100644 --- a/pygsheets/drive.py +++ b/pygsheets/drive.py @@ -286,13 +286,14 @@ def export(self, sheet, file_format, path='', filename=''): import io file_name = str(sheet.id or tmp) + file_extension if filename is None else filename + file_extension - fh = io.FileIO(path + file_name, 'wb') + file_path = os.path.join(path, file_name) + fh = io.FileIO(file_path, 'wb') downloader = MediaIoBaseDownload(fh, request) done = False while done is False: status, done = downloader.next_chunk() # logging.info('Download progress: %d%%.', int(status.progress() * 100)) TODO fix this - logging.info('Download finished. File saved in %s.', path + file_name) + logging.info('Download finished. File saved in %s.', file_path) if tmp is not None: sheet.index = tmp + 1 diff --git a/pygsheets/sheet.py b/pygsheets/sheet.py index 29b36a8..8162a5e 100644 --- a/pygsheets/sheet.py +++ b/pygsheets/sheet.py @@ -372,8 +372,14 @@ def values_batch_update(self, spreadsheet_id, body, parse=True): valueInputOption=cformat) self._execute_requests(request) - # def values_batch_update_by_data_filter(self): - # pass + def values_batch_update_by_data_filter(self, spreadsheet_id, data, parse=True): + body = { + "data": data, + "valueInputOption": 'USER_ENTERED' if parse else 'RAW', + "includeValuesInResponse": False + } + request = self.service.spreadsheets().values().batchUpdateByDataFilter(spreadsheetId=spreadsheet_id, body=body) + self._execute_requests(request) # def values_clear(self): # pass @@ -473,7 +479,6 @@ def developer_metadata_update(self, spreadsheet_id, key, value, location, data_f } self.batch_update(spreadsheet_id, [request]) - # TODO: implement as base for batch update. # def values_update(self): # pass diff --git a/pygsheets/utils.py b/pygsheets/utils.py index e6459f8..8d68915 100644 --- a/pygsheets/utils.py +++ b/pygsheets/utils.py @@ -62,7 +62,13 @@ def numericise_all(input, empty_value=''): def is_number(n): - return str(n).replace('.', '', 1).isdigit() + if '_' in str(n): + return False + try: + float(n) + except ValueError: + return False + return True def format_addr(addr, output='flip'): diff --git a/pygsheets/worksheet.py b/pygsheets/worksheet.py index cf8fbad..c6ae22e 100755 --- a/pygsheets/worksheet.py +++ b/pygsheets/worksheet.py @@ -619,7 +619,7 @@ def update_value(self, addr, val, parse=None): @batchable def update_values(self, crange=None, values=None, cell_list=None, extend=False, majordim='ROWS', parse=None): - """Updates cell values in batch, it can take either a cell list or a range and values. cell list is only efficient + """Updates a range cell values, it can take either a cell list or a range and its values. cell list is only efficient for small lists. This will only update the cell values not other properties. :param cell_list: List of a :class:`Cell` objects to update with their values. If you pass a matrix to this,\ @@ -698,6 +698,38 @@ def update_values(self, crange=None, values=None, cell_list=None, extend=False, parse = parse if parse is not None else self.spreadsheet.default_parse self.client.sheet.values_batch_update(self.spreadsheet.id, body, parse) + @batchable + def update_values_batch(self, ranges, values, majordim='ROWS', parse=None): + """ + update multiple ranges of values in a single call. + + :param ranges: list of addresses of the range. can be GridRange, label, tuple, etc + :param values: list of values corresponding to ranges, should be list of matrices + :param majordim: major dimension of values provided. 'ROWS' or 'COLUMNS' + :param parse: if the values should be as if the user typed them into the UI else its stored as is. Default is + spreadsheet.default_parse + + Example: + >>> wks.update_values_batch(['A1:A2', 'B1:B2'], [[[1],[2]], [[3],[4]]]) + >>> wks.get_values_batch(['A1:A2', 'B1:B2']) + [[['1'], ['2']], [['3'], ['4']]] + + >>> wks.update_values_batch([((1,1), (2,1)), 'B1:B2'], [[[1,2]], [[3,4]]], 'COLUMNS') + >>> wks.get_values_batch(['A1:A2', 'B1:B2']) + [[['1'], ['2']], [['3'], ['4']]] + + """ + ranges = [GridRange.create(x, self).label for x in ranges] + if not isinstance(values, list): + raise InvalidArgumentValue('values is not a list') + if len(ranges) != len(values): + raise InvalidArgumentValue('number of ranges and values should match') + # TODO update to enable filters + data = [ + {'dataFilter': {'a1Range': x[0]}, 'values': x[1], 'majorDimension': majordim} for x in zip(ranges, values) + ] + self.client.sheet.values_batch_update_by_data_filter(self.spreadsheet.id, data, parse) + @batchable def update_cells_prop(self, **kwargs): warnings.warn(_warning_mesage.format('method', 'update_cells'), category=DeprecationWarning) @@ -1416,7 +1448,7 @@ def get_as_df(self, has_header=True, index_column=None, start=None, end=None, nu """ if not self._linked: return False - include_tailing_empty = True if has_header else kwargs.get('include_tailing_empty', False) + include_tailing_empty = kwargs.get('include_tailing_empty', False) include_tailing_empty_rows = kwargs.get('include_tailing_empty_rows', False) index_column = index_column or kwargs.get('index_colum', None) @@ -1431,13 +1463,18 @@ def get_as_df(self, has_header=True, index_column=None, start=None, end=None, nu else: values = self.get_all_values(returnas='matrix', include_tailing_empty=include_tailing_empty, value_render=value_render, include_tailing_empty_rows=include_tailing_empty_rows) + + max_row = max(len(row) for row in values) + values = [row + [empty_value] * (max_row - len(row)) for row in values] if numerize: values = [numericise_all(row, empty_value) for row in values] if has_header: keys = values[0] - values = [row[:len(keys)] for row in values[1:]] + values = values[1:] + if any(key == '' for key in keys): + warnings.warn('At least one column name in the data frame is an empty string. If this is a concern, please specify include_tailing_empty=False and/or ensure that each column containing data has a name.') df = pd.DataFrame(values, columns=keys) else: df = pd.DataFrame(values) @@ -1648,7 +1685,7 @@ def add_conditional_formatting(self, start, end, condition_type, format, conditi self.client.sheet.batch_update(self.spreadsheet.id, request) @batchable - def merge_cells(self, start, end, merge_type='MERGE_ALL', grange=None): + def merge_cells(self, start=None, end=None, merge_type='MERGE_ALL', grange=None): """ Merge cells in range diff --git a/setup.py b/setup.py index 81d5414..aecfc37 100644 --- a/setup.py +++ b/setup.py @@ -22,17 +22,7 @@ def read(filename): description = 'Google Spreadsheets Python API v4' - -long_description = """ -{index} - -License -------- -MIT - -Download -======== -""" +long_description = read('README.md') version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', read('pygsheets/__init__.py'), re.MULTILINE).group(1) @@ -47,6 +37,8 @@ def read(filename): name='pygsheets', packages=['pygsheets'], description=description, + long_description=long_description, + long_description_content_type='text/markdown', version=version, author='Nithin Murali', author_email='imnmfotmal@gmail.com', diff --git a/tests/online_test.py b/tests/online_test.py index b4c79d2..270432a 100644 --- a/tests/online_test.py +++ b/tests/online_test.py @@ -11,6 +11,7 @@ from pygsheets.exceptions import CannotRemoveOwnerError from pygsheets.custom_types import ExportType from pygsheets import Cell +import pygsheets.utils as utils from pygsheets.custom_types import HorizontalAlignment, VerticalAlignment try: @@ -406,7 +407,7 @@ def test_iter(self): def test_getitem(self): self.worksheet.update_row(1, [1, 2, 3, 4, 5]) row = self.worksheet[1] - assert len(row) == self.worksheet.cols + 1 + assert len(row) == self.worksheet.cols assert row[0] == str(1) # TODO first index is dummy def test_clear(self): @@ -716,6 +717,7 @@ def test_developer_metadata(self): final_meta = self.worksheet.get_developer_metadata() assert len(final_meta) == len(old_meta) + # @pytest.mark.skip() class TestDataRange(object): def setup_class(self): @@ -868,3 +870,18 @@ def test_start_ub_end(self): assert self.grange.start == pygsheets.Address('1', True) assert self.grange.end == pygsheets.Address('4', True) assert self.grange.label == self.worksheet.title + '!' + '1' + ':' + '4' + + +class TestUtils(object): + + def test_is_number(self): + assert utils.is_number("1") + assert utils.is_number("-1") + assert utils.is_number("1.234") + assert utils.is_number("-1.234324") + assert utils.is_number("+1.345") + assert not utils.is_number("yuy") + assert not utils.is_number("12yuy34") + assert not utils.is_number("12.3.4") + assert not utils.is_number("12?34") + assert not utils.is_number("1.345_34")