Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support public published spreadsheets #345

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gspread/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from urllib.parse import urlencode


from .client import Client, authorize
from .client import Client, authorize, public
from .models import Spreadsheet, Worksheet, Cell
from .exceptions import (GSpreadException, AuthenticationError,
SpreadsheetNotFound, NoValidUrlKeyFound,
Expand Down
54 changes: 51 additions & 3 deletions gspread/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DRIVE_FILES_API_V2_URL,
DRIVE_FILES_UPLOAD_API_V2_URL
)
from .utils import finditem, extract_id_from_url
from .utils import finditem, extract_id_from_url, is_public_url
from .exceptions import (SpreadsheetNotFound, UpdateCellError)


Expand All @@ -35,6 +35,9 @@ class Client(object):

:param auth: An OAuth2 credential object. Credential objects are those created by the
oauth2client library. https://github.com/google/oauth2client
To access public, published spreadsheets, set auth to None. Be careful,
it is insufficient for the spreadsheet to simply be public, it must
also be published (in the sense of File->Publish to the web->Entire Document->Publish)
:param http_session: (optional) A session object capable of making HTTP requests while persisting headers.
Defaults to :class:`~gspread.httpsession.HTTPSession`.

Expand Down Expand Up @@ -102,13 +105,17 @@ def open_by_key(self, key):
>>> c.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE')

"""
feed = self.get_spreadsheets_feed()
feed = self.get_spreadsheets_feed(hint=key)
for elem in feed.findall(_ns('entry')):
alter_link = finditem(lambda x: x.get('rel') == 'alternate',
elem.findall(_ns('link')))
spreadsheet_id = extract_id_from_url(alter_link.get('href'))
if spreadsheet_id == key:
return Spreadsheet(self, elem)

if is_public_url(alter_link.get('href')):
return Spreadsheet(self, elem)

else:
raise SpreadsheetNotFound

Expand Down Expand Up @@ -146,9 +153,32 @@ def openall(self, title=None):
continue
result.append(Spreadsheet(self, elem))

if self.auth is None:
raise SpreadsheetNotFound

return result

def get_spreadsheets_feed(self, visibility='private', projection='full'):
def get_spreadsheets_feed(self, visibility='private', projection='full', hint=None):
if not self.auth:
# If we have a spreadsheet key, we can try public route
if not hint:
# No joy, back out
return ElementTree.Element('feed')
url = construct_url('worksheets', spreadsheet_id=hint,
visibility='public', projection='full')
r = self.session.get(url)
# Construct a single-entry feed in the expected format
feed = ElementTree.Element('feed')
try:
entry = ElementTree.Element(_ns('entry'))
for elem in ElementTree.fromstring(r.content):
entry.append(elem)
feed.append(entry)
except ElementTree.ParseError:
# we get a html error page if sheet is public-but-not-published
return ElementTree.Element('feed')
return feed

url = construct_url('spreadsheets',
visibility=visibility, projection=projection)

Expand All @@ -157,6 +187,9 @@ def get_spreadsheets_feed(self, visibility='private', projection='full'):

def get_worksheets_feed(self, spreadsheet,
visibility='private', projection='full'):
if not self.auth:
# fall back to public
visibility = 'public'
url = construct_url('worksheets', spreadsheet,
visibility=visibility, projection=projection)

Expand All @@ -166,6 +199,9 @@ def get_worksheets_feed(self, spreadsheet,
def get_cells_feed(self, worksheet,
visibility='private', projection='full', params=None):

if not self.auth:
# fall back to public
visibility = 'public'
url = construct_url('cells', worksheet,
visibility=visibility, projection=projection)

Expand Down Expand Up @@ -401,3 +437,15 @@ def authorize(credentials):
client = Client(auth=credentials)
client.login()
return client


def public():
"""Prepare to access public, published spreadsheets.

No private spreadsheets will be accessible , for that you
need to authorize or login instead.

:returns: :class:`Client` instance.

"""
return Client(auth=None)
3 changes: 3 additions & 0 deletions gspread/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def __init__(self, client, feed_entry):
self._sheet_list = []
self._feed_entry = feed_entry
self._id = feed_entry.find(_ns('id')).text.split('/')[-1]
if self._id == 'full':
# public sheet case, id is elsewhere
self._id = feed_entry.find(_ns('id')).text.split('/')[-3]
self._title = feed_entry.find(_ns('title')).text
self._updated = feed_entry.find(_ns('updated')).text

Expand Down
6 changes: 6 additions & 0 deletions gspread/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

URL_KEY_V1_RE = re.compile(r'key=([^&#]+)')
URL_KEY_V2_RE = re.compile(r'/spreadsheets/d/([a-zA-Z0-9-_]+)')
URL_KEY_V2_PUBLIC_RE = re.compile(r'spreadsheets/d/([a-zA-Z0-9-_]+)/pubhtml')


def finditem(func, seq):
Expand Down Expand Up @@ -190,6 +191,11 @@ def wid_to_gid(wid):
return str(int(str(widval), 36) ^ xorval)


def is_public_url(url):
m = URL_KEY_V2_PUBLIC_RE.search(url)
return bool(m)


if __name__ == '__main__':
import doctest
doctest.testmod()
86 changes: 86 additions & 0 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ def setUp(self):
self.assertTrue(isinstance(self.gc, gspread.Client))


class PublicGspreadTest(unittest.TestCase):

@classmethod
def setUpClass(cls):
try:
cls.config = read_config(CONFIG_FILENAME)
cls.gc = gspread.public()
except IOError as e:
msg = "Can't find %s for reading test configuration. "
raise Exception(msg % e.filename)

def setUp(self):
self.assertTrue(isinstance(self.gc, gspread.Client))


class ClientTest(GspreadTest):

"""Test for gspread.client."""
Expand Down Expand Up @@ -156,6 +171,31 @@ def test_create(self):
self.assertTrue(isinstance(new_spreadsheet, gspread.Spreadsheet))


class PublicClientTest(PublicGspreadTest):

"""Test for gspread.client without authorization."""

def test_open(self):
title = self.config.get('PublicSpreadsheet', 'title')
self.assertRaises(gspread.SpreadsheetNotFound,
self.gc.open,
title)

def test_open_by_key(self):
key = self.config.get('PublicSpreadsheet', 'key')
spreadsheet = self.gc.open_by_key(key)
self.assertTrue(isinstance(spreadsheet, gspread.Spreadsheet))

def test_open_by_url(self):
url = self.config.get('PublicSpreadsheet', 'url')
spreadsheet = self.gc.open_by_url(url)
self.assertTrue(isinstance(spreadsheet, gspread.Spreadsheet))

def test_openall(self):
self.assertRaises(gspread.SpreadsheetNotFound,
self.gc.openall)


class SpreadsheetTest(GspreadTest):

"""Test for gspread.Spreadsheet."""
Expand Down Expand Up @@ -189,6 +229,37 @@ def test_worksheet_iteration(self):
[sheet for sheet in self.spreadsheet])


class PublicSpreadsheetTest(PublicGspreadTest):

"""Test for gspread.Spreadsheet without authorization."""

def setUp(self):
super(PublicSpreadsheetTest, self).setUp()
key = self.config.get('PublicSpreadsheet', 'key')
self.spreadsheet = self.gc.open_by_key(key)

def test_properties(self):
self.assertEqual(self.config.get('PublicSpreadsheet', 'title'),
self.spreadsheet.title)

def test_sheet1(self):
sheet1 = self.spreadsheet.sheet1
self.assertTrue(isinstance(sheet1, gspread.Worksheet))

def test_get_worksheet(self):
sheet1 = self.spreadsheet.get_worksheet(0)
self.assertTrue(isinstance(sheet1, gspread.Worksheet))

def test_worksheet(self):
sheet_title = self.config.get('Spreadsheet', 'sheet1_title')
sheet = self.spreadsheet.worksheet(sheet_title)
self.assertTrue(isinstance(sheet, gspread.Worksheet))

def test_worksheet_iteration(self):
self.assertEqual(self.spreadsheet.worksheets(),
[sheet for sheet in self.spreadsheet])


class WorksheetTest(GspreadTest):

"""Test for gspread.Worksheet."""
Expand Down Expand Up @@ -531,6 +602,21 @@ def test_export(self):
self.assertEqual(exported_values, value_list)


class PublicWorksheetTest(PublicGspreadTest):

"""Test for gspread.Worksheet without authorization."""

def setUp(self):
super(PublicWorksheetTest, self).setUp()
key = self.config.get('PublicSpreadsheet', 'key')
self.spreadsheet = self.gc.open_by_key(key)
self.sheet = self.spreadsheet.sheet1

def test_get_all_values(self):
read_data = self.sheet.get_all_values()
self.assertTrue(len(read_data) > 0)


class WorksheetDeleteTest(GspreadTest):

def setUp(self):
Expand Down
6 changes: 6 additions & 0 deletions tests/tests.config.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ url: full url of this spreadsheet
sheet1_title: title of the first worksheet
new_spreadsheet_title: could be anything you like

[PublicSpreadsheet]
title: gspread public test 2 (public and published)
key: 16i5mfs194m44guNIBUylgZrvNslQlkp7F9JNbQar7E4
url: https://docs.google.com/spreadsheets/d/16i5mfs194m44guNIBUylgZrvNslQlkp7F9JNbQar7E4/edit?usp=sharing
sheet1_title: Sheet1

[Worksheet]
id: string following "#gid="
title: title of this worksheet
Expand Down