Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ __pycache__
build
dist
/pgsheets.egg-info

/.eggs
# environments
.env
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
install:
- pip install -U setuptools>=17.1
- python setup.py install
script: python setup.py test
10 changes: 6 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
.. image:: https://travis-ci.org/henrystokeley/pgsheets.svg
:target: https://travis-ci.org/henrystokeley/pgsheets/tree/master

pgsheets : Manipulate Google Sheets Using Python
================================================

pgsheets is a Python3 library for interacting with Google Sheets.
pgsheets is a Python library for interacting with Google Sheets.
It makes use of `Pandas <http://pandas.pydata.org/>`__ DataFrames,
2-dimensional structures perfectly
suited for data analysis and representing a spreadsheet.

This library can be integrated easily with your existing data to present dashboards, update documents, or provide quick data analysis.

The library has been tested in Python 2.6 and 2.7, and Python 3.3 through 3.5.

Features
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -138,9 +143,6 @@ with `removeWorksheet()`:
Limitations
~~~~~~~~~~~~~~~~~~~~~~~~~~

The library has only been tested in Python3.4.
It will almost certainly not work in Python2.

Currently the following cannot be done with pgsheets:

- Create a spreadsheet
Expand Down
4 changes: 2 additions & 2 deletions pgsheets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class PGSheetsValueError(PGSheetsException):
def _check_status(r):
if r.status_code // 100 != 2:
raise PGSheetsHTTPException(
"Bad HTTP response {}:\n{}"
.format(r.status_code, r.content.decode())
"Bad HTTP response {code}:\n{content}"
.format(code=r.status_code, content=r.content.decode())
)
27 changes: 13 additions & 14 deletions pgsheets/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from xml.etree import ElementTree
from xml.etree.ElementTree import Element, SubElement
import urllib
from six.moves.urllib import parse
import re

import requests
import pandas as pd

from pgsheets.exceptions import _check_status, PGSheetsValueError


def _ns_w3(name):
return '{http://www.w3.org/2005/Atom}' + name

Expand All @@ -24,7 +23,7 @@ def _get_first(elements, prop, equal):
raise ValueError('missing element')


class Worksheet():
class Worksheet(object):
"""Represents a single Spreadsheet's worksheet.

Do not initialize manually, instead retrieve from a Spreadsheet object.
Expand All @@ -33,7 +32,7 @@ class Worksheet():
def __init__(self, token, element, **kwargs):
self._element = element
self._token = token
super().__init__(**kwargs)
super(Worksheet, self).__init__(**kwargs)

def _getFeed(self):
self_uri = _get_first(
Expand Down Expand Up @@ -222,7 +221,7 @@ def str_repr(data):
return ""
data = str(data)
if escape_formulae and data and data[0] == "=":
data = "'{}".format(data)
data = "'{data}".format(data=data)
return str(data)

if copy_columns:
Expand Down Expand Up @@ -260,7 +259,7 @@ def _addCells(self, cells):
)

def add_entry(feed, row, col, content):
code = 'R{}C{}'.format(row, col)
code = 'R{row}C{col}'.format(row=row, col=col)
entry = SubElement(feed, 'entry')
SubElement(entry, 'batch:id').text = code
SubElement(entry, 'batch:operation', {'type': 'update'})
Expand Down Expand Up @@ -295,11 +294,11 @@ def __repr__(self):
id_=self._getSheetKey())


class _BaseSpreadsheet():
class _BaseSpreadsheet(object):
def __init__(self, token, element, **kwargs):
self._token = token
self._element = element
super().__init__(**kwargs)
super(_BaseSpreadsheet, self).__init__(**kwargs)

def getKey(self):
return self._element.find(_ns_w3('id')).text.split('/')[-1]
Expand Down Expand Up @@ -338,7 +337,7 @@ def getWorksheet(self, title):
for w in worksheets:
if w._getTitle(w._element) == title:
return w
raise ValueError('unavailable sheet {}'.format(title))
raise ValueError('unavailable sheet {title}'.format(title=title))

def addWorksheet(self, title, rows=1, cols=1):
"""Adds a new worksheet to a spreadsheet.
Expand All @@ -357,8 +356,8 @@ def addWorksheet(self, title, rows=1, cols=1):
SubElement(entry, 'gs:colCount').text = str(cols)

key = self.getKey()
url = ('https://spreadsheets.google.com/feeds/worksheets/{}'
'/private/full'.format(urllib.parse.quote(key)))
url = ('https://spreadsheets.google.com/feeds/worksheets/{key}'
'/private/full'.format(key=parse.quote(key)))
r = requests.post(
url,
data=ElementTree.tostring(entry),
Expand Down Expand Up @@ -402,11 +401,11 @@ def __init__(self, token, key, **kwargs):
if m:
key = m.group(1)

key = urllib.parse.quote(key)
key = parse.quote(key)
url = ('https://spreadsheets.google.com/feeds/spreadsheets'
'/private/full/{}'.format(key))
'/private/full/{key}'.format(key=key))
r = requests.get(url, headers=token.getAuthorizationHeader())
_check_status(r)
element = ElementTree.fromstring(r.content.decode())

super().__init__(token=token, element=element, **kwargs)
super(Spreadsheet, self).__init__(token=token, element=element, **kwargs)
12 changes: 6 additions & 6 deletions pgsheets/token.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import urllib.parse
from six.moves.urllib import parse
import json
import datetime

Expand All @@ -7,15 +7,15 @@
from pgsheets.exceptions import _check_status


class Client():
class Client(object):
"""Represent an application's Google's client data, along with methods for
getting a refresh token.

A refresh token is required to intialize a Token object.
"""

def __init__(self, client_id, client_secret, **kwargs):
super().__init__(**kwargs)
super(Client, self).__init__(**kwargs)
self._client_id = client_id
self._client_secret = client_secret
self._redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
Expand All @@ -27,7 +27,7 @@ def getOauthUrl(self):
Provide the returned code to the getRefreshToken() method to get a
token that can be used repeatedly in the future.
"""
scope = urllib.parse.quote('https://spreadsheets.google.com/feeds')
scope = parse.quote('https://spreadsheets.google.com/feeds')

return (
"https://accounts.google.com/o/oauth2/auth?"
Expand Down Expand Up @@ -68,7 +68,7 @@ def getRefreshToken(self, user_code):
return data['refresh_token']


class Token():
class Token(object):
_REFRSH_TOKEN_SLACK = 100

def __init__(self, client, refresh_token, **kwargs):
Expand All @@ -77,7 +77,7 @@ def __init__(self, client, refresh_token, **kwargs):
The refresh_token should be stored and provided on all
initializations of any particular client and Google user.
"""
super().__init__(**kwargs)
super(Token, self).__init__(**kwargs)
self._client = client
self._refresh_token = refresh_token
self._expires = None
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pandas >= 0.14.0
requests >= 2.0.0
six
16 changes: 16 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
import os
import sys

from setuptools import setup, find_packages

Expand All @@ -9,6 +11,15 @@
requirements = f.read()
requirements = [
r for r in requirements.splitlines() if r != '']
# exclude pandas requirement for CI testing
if os.environ.get('TRAVIS') or os.environ.get('CI'):
requirements = [ r for r in requirements if not 'pandas' in r ]
# test_requires depending on Python version
tests_require = []
if sys.version_info[0] == 2:
tests_require = ['mock']
if sys.version_info[1] < 7:
tests_require.append('unittest2')
# get readme
with open('README.rst') as f:
readme = f.read()
Expand All @@ -30,11 +41,16 @@
license="MIT",
url="https://github.com/henrystokeley/pgsheets",
install_requires=requirements,
tests_require=tests_require,
test_suite='test',
classifiers=[
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Scientific/Engineering',
'Topic :: Office/Business :: Financial :: Spreadsheet',
],
Expand Down
4 changes: 4 additions & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sys, types
sys.modules['pandas'] = types.ModuleType('pandas', 'Fake pandas module')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All tests pass and everything looks good, but can you explain why you're mocking out the pandas module? Even if it isn't required right now, shouldn't we still test against pandas?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robin900

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I do not wish to attempt to get pandas to build on the travis-ci host. 1) Testing a collaborative open source package against multiple Python versions requires something like travis-ci, and 2) using travis-ci means we have to build each test environment from scratch for each build. There are ways to retain assets from build to build on a TravisCI instance, but it requires a paid subscription.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note that pandas is a requirement in setup.py UNLESS the environment indicates it's travis-ci or another CI. So installing and running the tests locally will still check for pandas requirement, and import the genuine pandas module.

If the test code ever grows tests that actually call pandas code, our choices seem to be:

  1. Stop using travis-ci, which will make support for multiple Python versions hard to maintain.
  2. Upgrade to a paid travis-ci account, or wait until Travis offers the dependency caching feature to open-source projects.
  3. Mock the portions of the pandas module/contents that are covered by the tests, and use those mocks only in CI mode.

I would vote for #3.

del sys
del types
3 changes: 2 additions & 1 deletion test/api_content.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Responses from Google API for use in testing"""
from __future__ import unicode_literals
import datetime

def get_spreadsheet_element(key="test_key", title="title"):
Expand Down Expand Up @@ -43,7 +44,7 @@ def get_spreadsheet_element(key="test_key", title="title"):
update_month=d.month,
update_day=d.day,
title=title))
return data.encode()
return data.encode('utf8')

def get_worksheet_entry(key, sheet_title, encode=True):
open_tag = ("<entry>" if not encode else
Expand Down
10 changes: 10 additions & 0 deletions test/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import sys
if sys.version_info[0] == 3:
from unittest import TestCase
from unittest.mock import patch
elif sys.version_info[0] == 2:
from mock import patch
if sys.version_info[1] < 7:
from unittest2 import TestCase
else:
from unittest import TestCase
Loading