Skip to content

Commit

Permalink
Merge branch 'staging'
Browse files Browse the repository at this point in the history
Nithin Murali committed Feb 5, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 5c1c3c0 + 65376f6 commit a03069d
Showing 10 changed files with 98 additions and 29 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

2 changes: 1 addition & 1 deletion pygsheets/__init__.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
"""

__version__ = '2.0.4'
__version__ = '2.0.5'
__author__ = 'Nithin Murali'

from pygsheets.authorization import authorize
8 changes: 8 additions & 0 deletions pygsheets/address.py
Original file line number Diff line number Diff line change
@@ -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):
1 change: 1 addition & 0 deletions pygsheets/datarange.py
Original file line number Diff line number Diff line change
@@ -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 = {
5 changes: 3 additions & 2 deletions pygsheets/drive.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 8 additions & 3 deletions pygsheets/sheet.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion pygsheets/utils.py
Original file line number Diff line number Diff line change
@@ -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'):
45 changes: 41 additions & 4 deletions pygsheets/worksheet.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 3 additions & 11 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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='[email protected]',
19 changes: 18 additions & 1 deletion tests/online_test.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit a03069d

Please sign in to comment.