Skip to content
This repository has been archived by the owner on Apr 23, 2021. It is now read-only.

Use oauth session for user calls; support for throttling calls #35

Open
wants to merge 3 commits into
base: develop
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
2 changes: 1 addition & 1 deletion betterreads/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def list_comments(self, comment_type, resource_id, page=1):
"%s/%s/comments" % (comment_type, resource_id), {"format": "xml"}
)
return [
GoodreadsComment(comment_dict)
GoodreadsComment(comment_dict, self)
for comment_dict in resp["comments"]["comment"]
]

Expand Down
5 changes: 3 additions & 2 deletions betterreads/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
class GoodreadsComment:
"""Goodreads comment class"""

def __init__(self, comment_dict):
def __init__(self, comment_dict, client):
self._comment_dict = comment_dict
self._client = client

@property
def gid(self):
Expand All @@ -22,7 +23,7 @@ def body(self):
@property
def user(self):
"""User that made the comment"""
return GoodreadsUser(self._comment_dict["user"], self)
return GoodreadsUser(self._comment_dict["user"], self._client)

@property
def created_at(self):
Expand Down
3 changes: 3 additions & 0 deletions betterreads/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import xmltodict
import json

from betterreads.request_util import throttle


class GoodreadsRequestException(Exception):
def __init__(self, error_msg, url):
Expand All @@ -21,6 +23,7 @@ def __init__(self, client, path, query_dict, req_format="xml"):
self.path = path
self.req_format = req_format

@throttle
def request(self):
resp = requests.get(self.host + self.path, params=self.params)
if resp.status_code != 200:
Expand Down
41 changes: 41 additions & 0 deletions betterreads/request_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
The API TOS limits to a request rate of 1 request per second per application. This decorator
can enforce that limit for single-threaded applications by setting the environmental
variable `GOODREADS_REQUEST_WAIT_MIN=1`. When not set, this decorator does nothing.
"""
import functools
import os
import time

GOODREADS_REQUEST_WAIT_MIN = float(os.environ.get("GOODREADS_REQUEST_WAIT_MIN", 0.0))


class ThrottleDecorator:

TIME_DELTA = 0.001

def __init__(self, request_wait_min=GOODREADS_REQUEST_WAIT_MIN):
self.request_wait_min = request_wait_min
# Initialize last_call so that the first call will always go through quickly.
self.last_call = time.time() - self.request_wait_min

def __call__(self, fn):
@functools.wraps(fn)
def throttled(*args, **kwargs):
if self.request_wait_min > 0:
# Wait until a call is allowed.
self._wait_until_okay_to_call()
resp = fn(*args, **kwargs)
self.last_call = time.time()
return resp
else:
return fn(*args, **kwargs)

return throttled

def _wait_until_okay_to_call(self):
while time.time() - self.last_call < self.request_wait_min:
time.sleep(self.TIME_DELTA)


throttle = ThrottleDecorator()
3 changes: 3 additions & 0 deletions betterreads/session.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from rauth.service import OAuth1Service, OAuth1Session
import xmltodict

from betterreads.request_util import throttle


class GoodreadsSession:
"""Handle OAuth sessions"""
Expand Down Expand Up @@ -51,6 +53,7 @@ def oauth_resume(self):
access_token_secret=self.access_token_secret,
)

@throttle
def get(self, path, params=None):
"""OAuth get request"""
if not params:
Expand Down
20 changes: 12 additions & 8 deletions betterreads/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
class GoodreadsUser:
def __init__(self, user_dict, client):
self._user_dict = user_dict
self._client = client # for later queries
if hasattr(client, "session"):
# Always use the oauth session when available.
self._request_getter = client.request_oauth
else:
self._request_getter = client.request

def __repr__(self):
if self.user_name:
Expand Down Expand Up @@ -51,7 +55,7 @@ def list_groups(self, page=1):
"""List groups for the user. If there are more than 30 groups, get them
page by page."""
try:
resp = self._client.request("group/list/%s.xml" % self.gid, {"page": page})
resp = self._request_getter("group/list/%s.xml" % self.gid, {"page": page})
groups = [
GoodreadsGroup(group_dict)
for group_dict in resp["groups"]["list"]["group"]
Expand All @@ -63,7 +67,7 @@ def list_groups(self, page=1):
def owned_books(self, page=1):
"""Return the list of books owned by the user"""
try:
resp = self._client.session.get(
resp = self._request_getter(
"owned_books/user", {"page": page, "format": "xml", "id": self.gid}
)
owned_books_resp = resp["owned_books"]["owned_book"]
Expand All @@ -77,15 +81,15 @@ def owned_books(self, page=1):

def reviews(self, page=1):
"""Get all books and reviews on user's shelves"""
resp = self._client.request(
"/review/list.xml", {"v": 2, "id": self.gid, "page": page}
resp = self._request_getter(
"review/list.xml", {"v": 2, "id": self.gid, "page": page}
)
return [GoodreadsReview(r) for r in resp["reviews"]["review"]]

def shelves(self, page=1):
"""Get the user's shelves. This method gets shelves only for users with
public profile"""
resp = self._client.request(
resp = self._request_getter(
"shelf/list.xml", {"user_id": self.gid, "page": page}
)
return [GoodreadsUserShelf(s) for s in resp["shelves"]["user_shelf"]]
Expand All @@ -95,8 +99,8 @@ def per_shelf_reviews(self, page=1, per_page=200, shelf_name="read"):
total = 1
all_reviews = []
while len(all_reviews) < total:
resp = self._client.request(
"/review/list.xml",
resp = self._request_getter(
"review/list.xml",
{
"v": 2,
"id": self.gid,
Expand Down