Skip to content

Commit

Permalink
Merge pull request #26 from nithinmurali/staging
Browse files Browse the repository at this point in the history
Non local authorization and bug fixes
  • Loading branch information
nithinmurali committed Dec 8, 2016
2 parents 1583be8 + a31a3b3 commit d4efd0c
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 55 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pip install https://github.com/nithinmurali/pygsheets/archive/master.zip

Basic features are shown here, for complete set of features see the full documentation [here](http://pygsheets.readthedocs.io/en/latest/).

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](docs/auth.rst)
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.](docs/auth.rst)

2. Start using pygsheets:

Expand Down
11 changes: 7 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
- [x] Catch timout error and show an apropriote error rather than whole stack trace
- [x] loop in worksheet like csv, for row in wks:
- [x] compalitable with pandas
- [x] improve the authorizing - use only refresh tokens once autheticated (look at pgsheets)
- [x] add non local authorizaton
- [x] add python 3 support
- [ ] add google app engin support
- [ ] compalitable with numpy
- [ ] add sorting (https://developers.google.com/sheets/reference/rest/v4/spreadsheets/request#sortrangerequest)
- [ ] improve the authorizing - use only refresh tokens once autheticated (look at pgsheets)
- [ ] add search sheets/cells by regex
- [ ] make linking optional and work with the offline copy, when linking is renabled maybe sync the changes to cloud with batch update
- [ ] combine adj batch requests into 1
- [ ] save the batch requests, offline , and load later and push it?
- [ ] while fetching records try to cluster and find diffrent tablular datas (https://developers.google.com/sheets/guides/values)
- [ ] format(format, font, cell color) support
- [ ] comment and notes ability
- [ ] add non local authorizaton
- [ ] add python 3 support
- [ ] combine adj batch requests into 1
- [ ] write more examples
- [ ] write offline tests using mocks
- [ ] write tests for different authorizations (non local, using tokens etc)
38 changes: 37 additions & 1 deletion docs/source/authorizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,41 @@ Now click on the download button to download the 'client_secretxxx.json' file
8. Find the client_id from the file, your application will be able to acess any sheet which is shared with this email. To use this file initiliaze the pygsheets client as shown
::

gc = pygsheets.authorize(oauth_file='client_secretxxx.json')
gc = pygsheets.authorize(outh_file='client_secretxxx.json')

::

First time this will ask you to authorize pygsheets to acess your google sheets and drive, for this it will open a tab
in the brower, where you have to provide your google credentials and authorize it. This will create a json file with the
tokens based on the `outh_creds_store` param. So that you dont have to authorize it everytime you run the application.
In case if you already have a file with tokens then you can just pass it as the outh_file instead of the client secret file.

Incase you are running the script in a headless brower where it can't open a broweser, you can enbale non-local authorization.
Hence instead of opening a brower in the same meachine, it will provide a link which you can run on your local computer
and authorize the application.

::

gc = pygsheets.authorize(outh_file='client_secretxxx.json', outh_nonlocal=True)


Custom Credentials Objects
--------------------------
If you have another method of authenicating you can easily create a custom credentials object.

::

class Credentials (object):
def __init__ (self, access_token=None):
self.access_token = access_token

def refresh (self, http):
# get new access_token
# this only gets called if access_token is None

Then you could pass this for authorization as

::

gc = pygsheets.authorize(credentials=mycreds)

68 changes: 54 additions & 14 deletions pygsheets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ def __init__(self, auth):
http = auth.authorize(httplib2.Http(cache="/tmp/.pygsheets_cache", timeout=10))
discoveryurl = ('https://sheets.googleapis.com/$discovery/rest?'
'version=v4')
self.service = discovery.build('sheets', 'v4', http=http,
discoveryServiceUrl=discoveryurl)
self.service = discovery.build('sheets', 'v4', http=http)
self.driveService = discovery.build('drive', 'v3', http=http)
self._spreadsheeets = []
self.batch_requests = dict()
Expand Down Expand Up @@ -383,28 +382,31 @@ def callback(request_id, response, exception):
for req in self.batch_requests[spreadsheet_id]:
batch_req.add(req)
i = i+1
if i == 100:
if i % 100 == 0:
i = 0
batch_req.execute()
batch_req = self.service.new_batch_http_request(callback=callback)
batch_req.execute()
self.batch_requests[spreadsheet_id] = []


def get_outh_credentials(client_secret_file, credential_dir=None):
def get_outh_credentials(client_secret_file, credential_dir=None, outh_nonlocal=False):
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
:param client_secret_file: path to outh2 client secret file
:param application_name: name of application
:param credential_dir: path to directory where tokens should be stored
'global' if you want to store in system-wide location
None if you want to store in current script directory
:param outh_nonlocal: if the authorization should be done in another computer,
this will provide a url which when run will ask for credentials
:return
Credentials, the obtained credential.
"""
lflags = flags
if credential_dir == 'global':
home_dir = os.path.expanduser('~')
credential_dir = os.path.join(home_dir, '.credentials')
Expand All @@ -415,31 +417,59 @@ def get_outh_credentials(client_secret_file, credential_dir=None):
else:
pass

credential_path = os.path.join(credential_dir,
'sheets.googleapis.com-python.json')
# verify credentials directory
if not os.path.isdir(credential_dir):
raise IOError(credential_dir + " Dosent exist")
credential_path = os.path.join(credential_dir, 'sheets.googleapis.com-python.json')

# check if refresh token file is passed
with warnings.catch_warnings():
warnings.simplefilter("ignore")
try:
store = oauth2client.file.Storage(client_secret_file)
credentials = store.get()
except KeyError:
credentials = None

store = oauth2client.file.Storage(credential_path)
credentials = store.get()
# else try to get credentials from storage
if not credentials or credentials.invalid:
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
store = oauth2client.file.Storage(credential_path)
credentials = store.get()
except KeyError:
credentials = None

# else get the credentials from flow
if not credentials or credentials.invalid:
# verify client secret file
if not os.path.isfile(client_secret_file):
raise IOError(client_secret_file + " Dosent exist")
# execute flow
flow = client.flow_from_clientsecrets(client_secret_file, SCOPES)
flow.user_agent = 'pygsheets'
if flags:
credentials = tools.run_flow(flow, store, flags)
if lflags:
lflags.noauth_local_webserver = outh_nonlocal
credentials = tools.run_flow(flow, store, lflags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
print('Storing credentials to ' + credential_path)
return credentials


def authorize(outh_file='client_secret.json', outh_creds_store=None, service_file=None, credentials=None):
def authorize(outh_file='client_secret.json', outh_creds_store=None, outh_nonlocal=False, service_file=None,
credentials=None):
"""Login to Google API using OAuth2 credentials.
This function instantiates :class:`Client` and performs auhtication.
:param outh_file: path to outh2 credentials file
:param outh_file: path to outh2 credentials file, or tokens file
:param outh_creds_store: path to directory where tokens should be stored
'global' if you want to store in system-wide location
None if you want to store in current script directory
:param outh_nonlocal: if the authorization should be done in another computer,
this will provide a url which when run will ask for credentials
:param service_file: path to service credentials file
:param credentials: outh2 credentials object
Expand All @@ -454,8 +484,18 @@ def authorize(outh_file='client_secret.json', outh_creds_store=None, service_fil
print('service_email : '+str(data['client_email']))
credentials = ServiceAccountCredentials.from_json_keyfile_name(service_file, SCOPES)
elif outh_file:
credentials = get_outh_credentials(client_secret_file=outh_file, credential_dir=outh_creds_store)
credentials = get_outh_credentials(client_secret_file=outh_file, credential_dir=outh_creds_store,
outh_nonlocal=outh_nonlocal)
else:
raise AuthenticationError
rclient = Client(auth=credentials)
return rclient


# @TODO
def public():
"""
return a :class:`Client` which can acess only publically shared sheets
"""
pass
59 changes: 28 additions & 31 deletions pygsheets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,60 +642,57 @@ def update_cell(self, addr, val, parse=True):
body['values'] = [[val]]
self.client.sh_update_range(self.spreadsheet.id, body, self.spreadsheet.batch_mode, parse)

def update_cells(self, cell_list=None, range=None, values=None, majordim='ROWS'):
def update_cells(self, crange=None, values=None, cell_list=None, majordim='ROWS'):
"""Updates cells in batch, it can take either a cell list or a range and values
:param cell_list: List of a :class:`Cell` objects to update with their values
:param range: range in format A1:A2 or just 'A1' or even (1,2) end cell will be infered from values
:param crange: range in format A1:A2 or just 'A1' or even (1,2) end cell will be infered from values
:param values: list of values if range given, if value is None its unchanged
:param majordim: major dimension of given data
"""
if cell_list:
if not self.spreadsheet.batch_mode:
self.spreadsheet.batch_start()
crange = 'A1:' + str(self.get_addr((self.rows, self.cols)))
# @TODO fit the minimum rectangle than whole array
values = [[None for x in range(self.cols)] for y in range(self.rows)]
for cell in cell_list:
self.update_cell(cell.label, cell.value)
self.spreadsheet.batch_stop() # @TODO fix this
elif range and values:
body = dict()
estimate_size = False
if type(range) == str:
if range.find(':') == -1:
estimate_size = True
elif type(range) == tuple:
values[cell.col-1][cell.row-1] = cell.value
body = dict()
estimate_size = False
if type(crange) == str:
if crange.find(':') == -1:
estimate_size = True
else:
raise InvalidArgumentValue
elif type(crange) == tuple:
estimate_size = True
else:
raise InvalidArgumentValue

if estimate_size:
start_r_tuple = Worksheet.get_addr(range, output='tuple')
if majordim == 'ROWS':
end_r_tuple = (start_r_tuple[0]+len(values), start_r_tuple[1]+len(values[0]))
else:
end_r_tuple = (start_r_tuple[0] + len(values[0]), start_r_tuple[1] + len(values))
body['range'] = self._get_range(range, Worksheet.get_addr(end_r_tuple))
if estimate_size:
start_r_tuple = Worksheet.get_addr(crange, output='tuple')
if majordim == 'ROWS':
end_r_tuple = (start_r_tuple[0]+len(values), start_r_tuple[1]+len(values[0]))
else:
body['range'] = self._get_range(*range.split(':'))
body['majorDimension'] = majordim
body['values'] = values
self.client.sh_update_range(self.spreadsheet.id, body, self.spreadsheet.batch_mode)
end_r_tuple = (start_r_tuple[0] + len(values[0]), start_r_tuple[1] + len(values))
body['range'] = self._get_range(crange, Worksheet.get_addr(end_r_tuple))
else:
raise InvalidArgumentValue(cell_list) # @TODO test
body['range'] = self._get_range(*crange.split(':'))
body['majorDimension'] = majordim
body['values'] = values
self.client.sh_update_range(self.spreadsheet.id, body, self.spreadsheet.batch_mode)

def update_col(self, index, values):
"""update an existing colum with values
"""
colrange = Worksheet.get_addr((1, index), 'label') + ":" + Worksheet.get_addr((len(values), index), "label")
self.update_cells(range=colrange, values=[values], majordim='COLUMNS')
self.update_cells(crange=colrange, values=[values], majordim='COLUMNS')

def update_row(self, index, values):
"""update an existing row with values
"""
colrange = self.get_addr((index, 1), 'label') + ':' + self.get_addr((index, len(values)), 'label')
self.update_cells(range=colrange, values=[values], majordim='ROWS')
self.update_cells(crange=colrange, values=[values], majordim='ROWS')

def resize(self, rows=None, cols=None):
"""Resizes the worksheet.
Expand Down Expand Up @@ -766,7 +763,7 @@ def fill_cells(self, start, end, value=''):
start = self.get_addr(start, "tuple")
end = self.get_addr(end, "tuple")
values = [[value]*(end[1]-start[1]+1)]*(end[0]-start[0]+1)
self.update_cells(range=start, values=values)
self.update_cells(crange=start, values=values)

def insert_cols(self, col, number=1, values=None):
"""
Expand Down Expand Up @@ -883,7 +880,7 @@ def set_dataframe(self, df, start, copy_index=False, copy_head=True, fit=False,
self.cols = start[1] - 1 + len(values)
if escape_formulae:
warnings.warn("Functionality not implimented")
self.update_cells(range=start, values=values)
self.update_cells(crange=start, values=values)

def get_as_df(self, head=1, numerize=True, empty_value=''):
"""
Expand Down
9 changes: 6 additions & 3 deletions test/dummy-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@
import httplib2
import os

import IPython
from apiclient import discovery
import oauth2client
from oauth2client import client
from oauth2client import tools

try:
import argparse
flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
# print ("flagssss"+str(flags))
# flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
flags = tools.argparser.parse_args([])
print ("flagssss "+str(flags))
flags = tools.argparser.parse_args()
except ImportError:
flags = None

# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/sheets.googleapis.com-python-quickstart.json
SCOPES = 'https://www.googleapis.com/auth/spreadsheets.readonly'
CLIENT_SECRET_FILE = 'client_secret.json'
CLIENT_SECRET_FILE = 'data/creds.json'
APPLICATION_NAME = 'Google Sheets API Python Quickstart'


Expand Down Expand Up @@ -52,6 +54,7 @@ def get_credentials():
flow.user_agent = APPLICATION_NAME
if flags:
print("flag :" + str(flags))
IPython.embed()
credentials = tools.run_flow(flow, store, flags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
Expand Down
2 changes: 1 addition & 1 deletion test/online_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_values(self):
assert vals[0][0].value == 'test val'

def test_update_cells(self):
self.worksheet.update_cells(range='A1:B2', values=[[1, 2], [3, 4]])
self.worksheet.update_cells(crange='A1:B2', values=[[1, 2], [3, 4]])
assert self.worksheet.cell((1, 1)).value == str(1)

cells = [pygsheets.Cell('A1', 10), pygsheets.Cell('A2', 12)]
Expand Down

0 comments on commit d4efd0c

Please sign in to comment.