Skip to content
This repository has been archived by the owner on Jun 2, 2020. It is now read-only.

Commit

Permalink
Merge pull request #43 from brunal/py23
Browse files Browse the repository at this point in the history
Make discogs_client python 2 & python 3 compatible
  • Loading branch information
rodneykeeling committed Mar 20, 2015
2 parents 2623536 + 43c4a3f commit 956f82d
Show file tree
Hide file tree
Showing 25 changed files with 169 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
install:
- pip install -r requirements.txt --use-mirrors
script:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

clean:
find . -name '*.pyc' -delete
find . -name __pycache__ -delete
4 changes: 3 additions & 1 deletion discogs_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
__version_info__ = (2,0,2)
from __future__ import absolute_import, division, print_function, unicode_literals

__version_info__ = 2, 0, 2
__version__ = '2.0.2'

from discogs_client.client import Client
Expand Down
27 changes: 19 additions & 8 deletions discogs_client/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import requests
from __future__ import absolute_import, division, print_function, unicode_literals

import warnings
import json
import oauth2
import urllib
try:
# python2
from urllib import urlencode
except ImportError:
# python3
from urllib.parse import urlencode

from discogs_client import models
from discogs_client.exceptions import ConfigurationError, HTTPError
from discogs_client.utils import update_qs
from discogs_client.fetchers import RequestsFetcher, OAuth2Fetcher


class Client(object):
_base_url = 'https://api.discogs.com'
_request_token_url = 'https://api.discogs.com/oauth/request_token'
Expand Down Expand Up @@ -44,9 +51,10 @@ def get_authorize_url(self, callback_url=None):

params = {}
params['User-Agent'] = self.user_agent
params['Content-Type'] = 'application/x-www-form-urlencoded'
if callback_url:
params['oauth_callback'] = callback_url
postdata = urllib.urlencode(params)
postdata = urlencode(params)

content, status_code = self._fetcher.fetch(self, 'POST', self._request_token_url, data=postdata, headers=params)
if status_code != 200:
Expand All @@ -55,14 +63,17 @@ def get_authorize_url(self, callback_url=None):
token, secret = self._fetcher.store_token_from_qs(content)

params = {'oauth_token': token}
query_string = urllib.urlencode(params)
query_string = urlencode(params)

return (token, secret, '?'.join((self._authorize_url, query_string)))

def get_access_token(self, verifier):
"""
Uses the verifier to exchange a request token for an access token.
"""
if isinstance(verifier, bytes):
verifier = verifier.decode('utf8')

self._fetcher.set_verifier(verifier)

params = {}
Expand All @@ -82,7 +93,7 @@ def _check_user_agent(self):

def _request(self, method, url, data=None):
if self.verbose:
print ' '.join((method, url))
print(' '.join((method, url)))

self._check_user_agent()

Expand All @@ -92,14 +103,14 @@ def _request(self, method, url, data=None):
}

if data:
headers['Content-Type'] = 'application/json'
headers['Content-Type'] = 'application/x-www-form-urlencoded'

content, status_code = self._fetcher.fetch(self, method, url, data=data, headers=headers)

if status_code == 204:
return None

body = json.loads(content)
body = json.loads(content.decode('utf8'))

if 200 <= status_code < 300:
return body
Expand Down
3 changes: 3 additions & 0 deletions discogs_client/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals


class DiscogsAPIError(Exception):
"""Root Exception class for Discogs API errors."""
pass
Expand Down
63 changes: 37 additions & 26 deletions discogs_client/fetchers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import requests
import oauth2
from requests.api import request
from oauthlib import oauth1
import json
import urlparse
import os
try:
# python2
from urlparse import parse_qsl
except ImportError:
# python3
from urllib.parse import parse_qsl


class Fetcher(object):
"""
Expand All @@ -12,8 +21,14 @@ class Fetcher(object):
(It's a slightly leaky abstraction designed to make testing easier.)
"""
def fetch(self, client, method, url, data=None, headers=None, json=True):
# Should return (content, status_code)
raise NotImplemented
"""Fetch the given request
Returns
-------
content : str (python2) or bytes (python3)
status_code : int
"""
raise NotImplementedError()


class LoggingDelegator(object):
Expand Down Expand Up @@ -41,42 +56,38 @@ def fetch(self, client, method, url, data=None, headers=None, json=True):
class OAuth2Fetcher(Fetcher):
"""Fetches via HTTP + OAuth 1.0a from the Discogs API."""
def __init__(self, consumer_key, consumer_secret, token=None, secret=None):
consumer = oauth2.Consumer(consumer_key, consumer_secret)
token_obj = None

if token and secret:
token_obj = oauth2.Token(token, secret)

self.oauth_client = oauth2.Client(consumer, token_obj)
self.client = oauth1.Client(consumer_key, client_secret=consumer_secret)
self.store_token(token, secret)

def store_token_from_qs(self, query_string):
token_dict = dict(urlparse.parse_qsl(query_string))
token = token_dict['oauth_token']
secret = token_dict['oauth_token_secret']
token_dict = dict(parse_qsl(query_string))
token = token_dict[b'oauth_token'].decode('utf8')
secret = token_dict[b'oauth_token_secret'].decode('utf8')
self.store_token(token, secret)
return token, secret

def forget_token(self):
self.oauth_client.token = None
self.store_token(None, None)

def store_token(self, token, secret):
self.oauth_client.token = oauth2.Token(token, secret)
self.client.resource_owner_key = token
self.client.resource_owner_secret = secret

def set_verifier(self, verifier):
self.oauth_client.token.set_verifier(verifier)
self.client.verifier = verifier

def fetch(self, client, method, url, data=None, headers=None, json_format=True):
if data:
body = json.dumps(data) if json_format else data
resp, content = self.oauth_client.request(url, method, body, headers=headers)
else:
resp, content = self.oauth_client.request(url, method, headers=headers)
return content, int(resp['status'])
body = json.dumps(data) if json_format and data else data
uri, headers, body = self.client.sign(url, http_method=method,
body=data, headers=headers)

resp = request(method, uri, headers=headers, data=body)
return resp.content, resp.status_code


class FilesystemFetcher(Fetcher):
"""Fetches from a directory of files."""
default_response = json.dumps({'message': 'Resource not found.'}), 404
default_response = json.dumps({'message': 'Resource not found.'}).encode('utf8'), 404

def __init__(self, base_path):
self.base_path = base_path
Expand All @@ -92,15 +103,15 @@ def fetch(self, client, method, url, data=None, headers=None, json=True):
path = os.path.join(self.base_path, base_name)
try:
with open(path, 'r') as f:
content = f.read()
content = f.read().encode('utf8') # return bytes not unicode
return content, 200
except:
return self.default_response


class MemoryFetcher(Fetcher):
"""Fetches from a dict of URL -> (content, status_code)."""
default_response = json.dumps({'message': 'Resource not found.'}), 404
default_response = json.dumps({'message': 'Resource not found.'}).encode('utf8'), 404

def __init__(self, responses):
self.responses = responses
Expand Down
22 changes: 13 additions & 9 deletions discogs_client/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals

from six import with_metaclass

from discogs_client.exceptions import HTTPError
from discogs_client.utils import parse_timestamp, update_qs, omit_none

Expand Down Expand Up @@ -171,14 +175,14 @@ class ObjectCollection(Field):

class APIObjectMeta(type):
def __new__(cls, name, bases, dict_):
for k, v in dict_.iteritems():
for k, v in dict_.items():
if isinstance(v, Field):
dict_[k] = v.to_descriptor(k)
return super(APIObjectMeta, cls).__new__(cls, name, bases, dict_)


class APIObject(object):
__metaclass__ = APIObjectMeta
class APIObject(with_metaclass(APIObjectMeta, object)):
pass


class PrimaryAPIObject(APIObject):
Expand Down Expand Up @@ -307,7 +311,7 @@ def _url_for_page(self, page):
return update_qs(self.url, base_qs)

def sort(self, key, order='asc'):
if not order in ('asc', 'desc'):
if order not in ('asc', 'desc'):
raise ValueError("Order must be one of 'asc', 'desc'")
self._sort_key = key
self._sort_order = order
Expand All @@ -332,7 +336,7 @@ def count(self):
return self._num_items

def page(self, index):
if not index in self._pages:
if index not in self._pages:
data = self.client._get(self._url_for_page(index))
self._pages[index] = [
self._transform(item) for item in data[self._list_key]
Expand All @@ -343,12 +347,12 @@ def _transform(self, item):
return item

def __getitem__(self, index):
page_index = index / self.per_page + 1
page_index = index // self.per_page + 1
offset = index % self.per_page

try:
page = self.page(page_index)
except HTTPError, e:
except HTTPError as e:
if e.status_code == 404:
raise IndexError(e.msg)
else:
Expand All @@ -360,7 +364,7 @@ def __len__(self):
return self.count

def __iter__(self):
for i in xrange(1, self.pages + 1):
for i in range(1, self.pages + 1):
page = self.page(i)
for item in page:
yield item
Expand All @@ -381,7 +385,7 @@ class Wantlist(PaginatedList):
def add(self, release, notes=None, notes_public=None, rating=None):
release_id = release.id if isinstance(release, Release) else release
data = {
'release_id': release_id,
'release_id': str(release_id),
'notes': notes,
'notes_public': notes_public,
'rating': rating,
Expand Down
8 changes: 5 additions & 3 deletions discogs_client/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from discogs_client.fetchers import LoggingDelegator, FilesystemFetcher, \
MemoryFetcher


class DiscogsClientTestCase(unittest.TestCase):
def setUp(self):

Expand All @@ -18,9 +19,9 @@ def setUp(self):

# Memory client
responses = {
'/artists/1': ('{"id": 1, "name": "Badger"}', 200),
'/500': ('{"message": "mushroom"}', 500),
'/204': ('', 204),
'/artists/1': (b'{"id": 1, "name": "Badger"}', 200),
'/500': (b'{"message": "mushroom"}', 500),
'/204': (b'', 204),
}
self.m = Client('ua')
self.m._base_url = ''
Expand All @@ -40,6 +41,7 @@ def assertPosted(self, assert_url, assert_data):
self.assertEqual(url, assert_url)
self.assertEqual(data, json.dumps(assert_data))


def suite():
from discogs_client.tests import test_core, test_models, test_fetchers
suite = unittest.TestSuite(test_core.suite())
Expand Down
16 changes: 16 additions & 0 deletions discogs_client/tests/make_symlinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python
import os
import sys
from itertools import permutations

for name in sys.argv[1:]:
print("doing {}".format(name))
root, next = name.split('?')
data, ext = next.split('.')
elems = data.split('&')
for permut in permutations(elems):
link_name = "{}?{}.{}".format(root, '&'.join(permut), ext)
if link_name == name:
continue
os.symlink(name, link_name)
print("wrote {}".format(link_name))
Loading

0 comments on commit 956f82d

Please sign in to comment.