Skip to content
This repository has been archived by the owner on Sep 5, 2019. It is now read-only.

Fix bug with JSONDecodeError; add retries; fix iteration when collection is changing #21

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ print "https://" + USERVOICE_SUBDOMAIN + ".uservoice.com/?sso=" + sso_token
Making API calls
----------------

You need to create an instance of uservoice.Client. Get API_KEY and API_SECRET for an API client which you can create from
You need to create an instance of uservoice.Client. Get API_KEY and API_SECRET for an API client which you can create from
Admin Console. Go to Settings -> Channels -> API.

```python
Expand Down Expand Up @@ -140,3 +140,13 @@ token = u.access_tokens.get(system='uservoice')
# 2. Use the token and secret to log in
access_token = client.login_with_access_token(token.token, token.secret)
```


Specs
===============
```
cp test/config.yml.templ test/config.yml
python test/test_sso.py
python test/test_collection.py
python test/test_client.py
``
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
author_email='[email protected]',
packages=['uservoice'],
install_requires=[
'future',
'pycrypto',
'pytz',
'PyYAML',
'requests',
'future',
'simplejson',
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of reintroducing the simplejson dependency -- which was intentionally removed -- could we instead fix the bug to work with the stdlib json module?

Copy link
Author

Choose a reason for hiding this comment

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

that would definitely be a better approach but i'm not sure i will have time to make the change personally.

'tenacity',
'requests-oauthlib'],
test_suite='test',
)
2 changes: 1 addition & 1 deletion test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
from . import test_sso

if __name__ == '__main__':
unittest.main()
unittest.main()
5 changes: 5 additions & 0 deletions test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
import uservoice


class TestClient(unittest.TestCase):
def setUp(self):
super(TestClient, self).setUp()
Expand Down Expand Up @@ -94,3 +95,7 @@ def test_should_get_reasonable_collection_using_collection_class(self):
def test_should_get_page_of_tickets(self):
tickets = self.client.get("/api/v1/tickets?per_page=2")
self.assertEqual(len(tickets), 2)


if __name__ == '__main__':
unittest.main()
3 changes: 3 additions & 0 deletions test/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,6 @@ def test_accessing_element_in_first_page_with_really_small_limited_size_with_pag
collection = uservoice.Collection(self.pagedClient, '/api/v1/suggestions', limit=2)
self.assertEqual(collection[1]['id'], 2)


if __name__ == '__main__':
unittest.main()
4 changes: 4 additions & 0 deletions test/test_sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ def test_generating_with_validity(self):
}, 10)
print("{protocol}://{subdomain}.{domain}?sso={sso}".format(protocol=self.config.get('protocol', 'https'),domain=self.config.get('uservoice_domain', 'uservoice.com'),subdomain=self.config['subdomain_name'],sso=sso_token))
self.assertTrue(len(sso_token) > 10)


if __name__ == '__main__':
unittest.main()
7 changes: 5 additions & 2 deletions uservoice/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from builtins import object
import operator
import array
import urllib.request, urllib.parse, urllib.error
import urllib.request, urllib.error, urllib.parse
import json
import simplejson as json
import uservoice
from requests_oauthlib import OAuth1
from urllib.parse import parse_qs
Expand Down Expand Up @@ -40,6 +39,7 @@ def __init__(self, subdomain_name, api_key, api_secret=None, oauth_token='', oau
self.subdomain_name = subdomain_name
self.uservoice_domain = uservoice_domain
self.protocol = protocol
self.login_email = None

def get_request_token(self, callback=None):
url = self.api_url + '/oauth/request_token'
Expand Down Expand Up @@ -126,10 +126,13 @@ def get_collection(self, path, **opts):
return uservoice.Collection(self, path, **opts)

def login_as(self, email):
self.login_email = email

resp = self.post('/api/v1/users/login_as', {
'request_token': self.get_request_token().token,
'user': { 'email': email }
})

if 'token' in resp:
token = resp['token']['oauth_token']
secret = resp['token']['oauth_token_secret']
Expand Down
52 changes: 45 additions & 7 deletions uservoice/collection.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,74 @@
from __future__ import division

import logging
from builtins import str
from builtins import range
from builtins import object
from past.utils import old_div
from requests.exceptions import RequestException
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
from uservoice.client import APIError, Unauthorized

PER_PAGE = 100

def retry_logger(fxn, attempt_number, elapsed_seconds):
logging.info("Retrying {} (try #{}, {}s passed)".format(fxn.__name__, attempt_number, elapsed_seconds))

class Collection(object):
RETRY_KWARGS = {
'after': retry_logger,
'reraise': True,
'retry': retry_if_exception_type((APIError, RequestException)),
'stop': stop_after_attempt(10),
'wait': wait_exponential(multiplier=5, max=300)
}

def __init__(self, client, query, limit=2**60):
self.client = client
self.query = query
self.limit = limit
self.per_page = min(self.limit, PER_PAGE)
self.pages = {}
self.response_data = None
self.index = -1

def __len__(self):
if not self.response_data:
try:
self[0]
except IndexError:
pass

return min(self.response_data['total_records'], self.limit)

@retry(**RETRY_KWARGS)
def __getitem__(self, i):
if i == 0 or (i > 0 and i < len(self)):
return self.load_page(int(old_div(i,float(PER_PAGE))) + 1)[i % PER_PAGE]
else:
raise IndexError
try:
if i == 0 or (i > 0 and i < len(self)):
return self.load_page(int(old_div(i,float(PER_PAGE))) + 1)[i % PER_PAGE]
else:
raise IndexError
except Unauthorized as e:
logging.warn("Unauthorized error; attempting to log back in...")
if self.client.login_email is None:
self.client.login_as_owner()
else:
self.client.login_as(self.client.login_email)

raise e


def __iter__(self):
for index in range(len(self)):
yield self[index]
return self

def __next__(self):
self.index += 1

if self.index >= len(self):
raise StopIteration

return self[self.index]


def load_page(self, i):
if not i in self.pages:
Expand All @@ -47,4 +86,3 @@ def load_page(self, i):
else:
raise uservoice.NotFound.new('The resource you requested is not a collection')
return self.pages[i]