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

Async client for Tornado, search #1

Merged
merged 5 commits into from
Jul 31, 2014
Merged
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ So far you can only retrieve the data for the public objects, for which no login
>>> client = deezer.Client()
>>> client.get_album(12).title
u'Monkey Business'


You also can use AsyncClient with tornado.

>>> from tornado.gen import coroutine
>>> from tornado.ioloop import IOLoop
>>> from deezer import AsyncClient
>>>
>>>
>>> @coroutine
... def main():
... client = AsyncClient()
... album = yield client.get_album(12)
... print(album.title)
...
>>> IOLoop.instance().run_sync(main)
Monkey Business

See the whole API on the [Sphinx](http://sphinx-doc.org/) generated [documentation](http://deezer-python.readthedocs.org/).

Authentication
Expand Down
12 changes: 10 additions & 2 deletions deezer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@
"""

__version__ = '0.1'
__all__ = ['Client', 'Resource', 'Album', 'Artist', 'Genre',
'Playlist', 'Track', 'User', 'Comment', 'Radio']
__all__ = ['AsyncClient', 'Client', 'Resource', 'Album',
'Artist', 'Genre', 'Playlist', 'Track', 'User',
'Comment', 'Radio']

USER_AGENT = 'Deezer Python API Wrapper v%s' % __version__

try:
from deezer.async import AsyncClient
except ImportError:
def AsyncClient(*args, **kwargs):
msg = "You need to install Tornado to be able use the async client."
raise RuntimeError(msg)

from deezer.client import Client
from deezer.resources import Album, Resource, Artist, Playlist
from deezer.resources import Genre, Track, User, Comment, Radio
47 changes: 47 additions & 0 deletions deezer/async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Implements an async tornado client class to query the
`Deezer API <http://developers.deezer.com/api>`_
"""
import json
import logging
from tornado.gen import coroutine, Return
from tornado.httpclient import AsyncHTTPClient
from deezer.client import Client


class AsyncClient(Client):
"""
An async client to retrieve some basic infos about Deezer resourses.

Create a client instance with the provided options. Options should
be passed in to the constructor as kwargs.

>>> import deezer
>>> client = deezer.AsyncClient(app_id='foo', app_secret='bar')

This client provides several method to retrieve the content of most
sort of Deezer objects, based on their json structure.
"""
def __init__(self, *args, **kwargs):
super(AsyncClient, self).__init__(*args, **kwargs)
max_clients = kwargs.get('max_clients', 2)
self._async_client = AsyncHTTPClient(max_clients=max_clients)

@coroutine
def get_object(self, object_t, object_id=None, relation=None, **kwargs):
"""
Actually query the Deezer API to retrieve the object

:returns: json dictionnary or raw string if other
format requested
"""
url = self.object_url(object_t, object_id, relation, **kwargs)
logging.debug(url)
response = yield self._async_client.fetch(url)
resp_str = response.body.decode('utf-8')
if self.output is 'json':
jsn = json.loads(resp_str)
result = self._process_json(jsn)
else:
result = resp_str
raise Return(result)
217 changes: 134 additions & 83 deletions deezer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
`Deezer API <http://developers.deezer.com/api>`_
"""

import json
try:
from urllib import urlencode
from urllib2 import urlopen
except ImportError:
#python 3
from urllib.parse import urlencode
from urllib.request import urlopen
import json
from deezer.resources import Album, Artist, Comment, Genre
from deezer.resources import Playlist, Radio, Track, User
from deezer.resources import Resource


class Client(object):
"""A client to retrieve some basic infos about Deezer resourses.
Expand All @@ -29,18 +33,19 @@ class Client(object):
host = "api.deezer.com"
output = "json"

objects_types = (
"album",
"artist",
"comment",
"editorial",
# "folder", # need identification
"genre",
"playlist",
"radio",
"track",
"user",
)
objects_types = {
'album': Album,
'artist': Artist,
'comment': Comment,
'editorial': None,
# 'folder': None, # need identification
'genre': Genre,
'playlist': Playlist,
'radio': Radio,
'search': None,
'track': Track,
'user': User,
}

def __init__(self, **kwargs):
super(Client, self).__init__()
Expand All @@ -54,126 +59,172 @@ def __init__(self, **kwargs):
self.app_secret = kwargs.get('app_secret')
self.access_token = kwargs.get('access_token')

def _process_item(self, item):
"""
Recursively convert dictionary
to :class:`~deezer.resources.Resource` object

:returns: instance of :class:`~deezer.resources.Resource`
"""
for key, value in item.items():
if isinstance(value, dict) and 'type' in value:
item[key] = self._process_item(value)
elif isinstance(value, dict) and 'data' in value:
item[key] = [self._process_item(i) for i in value['data']]
object_t = self.objects_types.get(item['type'], Resource)
return object_t(self, item)

def _process_json(self, jsn):
"""
Convert json to a :class:`~deezer.resources.Resource` object,
or list of :class:`~deezer.resources.Resource` objects.
"""
if 'data' in jsn:
return [self._process_item(item) for item in jsn['data']]
else:
return self._process_item(jsn)

def make_str(self, value):
"""
Convert value to str in python2 and python3 compatible way

:returns: str instance
"""
try:
value = str(value)
except UnicodeEncodeError:
#python2
value = value.encode('utf-8')
return value

@property
def scheme(self):
"""Get the http prefix for the address depending on the
use_ssl attribute
"""
return self.use_ssl and 'https://' or 'http://'
Get the http prefix for the address depending on the use_ssl attribute
"""
return self.use_ssl and 'https' or 'http'

def url(self, request=''):
"""Build the url with the appended request if provided.

:raises ValueError: if the request does not start by '/'"""
if request and not request.startswith('/'):
raise ValueError
return "%s%s%s" % (self.scheme, self.host, request)
"""Build the url with the appended request if provided."""
if request.startswith('/'):
request = request[1:]
return "%s://%s/%s" % (self.scheme, self.host, request)

def object_url(self, object_t, object_id=None, relation=None, options=None):
def object_url(self, object_t, object_id=None, relation=None, **kwargs):
"""
Helper method to build the url to query to access the object
passed as parameter

:raises TypeError: if the object type is invalid
"""
options = [options] if options else []
if self.output is not "json":
options.append("output=%s" % self.output)
if object_t not in self.objects_types:
raise TypeError("%s is not a valid type" % object_t)
request = "/" + object_t
if object_id:
request += "/%s" % object_id
if relation:
request += "/%s" % relation
request_items = (object_t, object_id, relation)
request_items = (item for item in request_items if item is not None)
request_items = (str(item) for item in request_items)
request = '/'.join(request_items)
base_url = self.url(request)
return base_url + ("?%s" % "&".join(options) if options else "")
if self.output is not 'json':
kwargs['output'] = self.output
if kwargs:
for key, value in kwargs.items():
if not isinstance(value, str):
kwargs[key] = self.make_str(value)
result = '%s?%s' % (base_url, urlencode(kwargs))
else:
result = base_url
return result

def get_object(self, object_t, object_id=None, relation=None):
def get_object(self, object_t, object_id=None, relation=None, **kwargs):
"""
Actually query the Deezer API to retrieve the object

:returns: json dictionnary or raw string if other
:returns: json dictionary or raw string if other
format requested
"""
response = urlopen(self.object_url(object_t, object_id, relation))
url = self.object_url(object_t, object_id, relation, **kwargs)
response = urlopen(url)
resp_str = response.read().decode('utf-8')
if self.output is "json":
resp_str = response.read()
try:
return json.loads(resp_str)
except TypeError:
#Python 3
encoding = response.headers.get_content_charset()
decoded_str = resp_str.decode(encoding)
return json.loads(decoded_str)
jsn = json.loads(resp_str)
return self._process_json(jsn)
else:
return response.read()
return resp_str

def get_album(self, object_id):
"""Get the album with the provided id
"""
Get the album with the provided id

:returns: an :class:`~deezer.resources.Album` object"""
jsn = self.get_object("album", object_id)
return Album(self, jsn)
:returns: an :class:`~deezer.resources.Album` object
"""
return self.get_object("album", object_id)

def get_artist(self, object_id):
"""Get the artist with the provided id
"""
Get the artist with the provided id

:returns: an :class:`~deezer.resources.Artist` object"""
jsn = self.get_object("artist", object_id)
return Artist(self, jsn)
:returns: an :class:`~deezer.resources.Artist` object
"""
return self.get_object("artist", object_id)

def get_comment(self, object_id):
"""Get the comment with the provided id
"""
Get the comment with the provided id

:returns: a :class:`~deezer.resources.Comment` object"""
jsn = self.get_object("comment", object_id)
return Comment(self, jsn)
:returns: a :class:`~deezer.resources.Comment` object
"""
return self.get_object("comment", object_id)

def get_genre(self, object_id):
"""Get the genre with the provided id
"""
Get the genre with the provided id

:returns: a :class:`~deezer.resources.Genre` object"""
jsn = self.get_object("genre", object_id)
return Genre(self, jsn)
:returns: a :class:`~deezer.resources.Genre` object
"""
return self.get_object("genre", object_id)

def get_genres(self):
"""
Returns a list of :class:`~deezer.resources.Genre` objects.
:returns: a list of :class:`~deezer.resources.Genre` objects.
"""
jsn = self.get_object("genre")
ret = []
for genre in jsn["data"]:
ret.append(Genre(self, genre))
return ret

return self.get_object("genre")

def get_playlist(self, object_id):
"""Get the playlist with the provided id
"""
Get the playlist with the provided id

:returns: a :class:`~deezer.resources.Playlist` object"""
jsn = self.get_object("playlist", object_id)
return Playlist(self, jsn)
:returns: a :class:`~deezer.resources.Playlist` object
"""
return self.get_object("playlist", object_id)

def get_radio(self, object_id=None):
"""Get the radio with the provided id.
"""
Get the radio with the provided id.

:returns: a :class:`~deezer.resources.Radio` object"""
jsn = self.get_object("radio", object_id)
return Radio(self, jsn)
:returns: a :class:`~deezer.resources.Radio` object
"""
return self.get_object("radio", object_id)

def get_track(self, object_id):
"""Get the track with the provided id
"""
Get the track with the provided id

:returns: a :class:`~deezer.resources.Track` object"""
jsn = self.get_object("track", object_id)
return Track(self, jsn)
:returns: a :class:`~deezer.resources.Track` object
"""
return self.get_object("track", object_id)

def get_user(self, object_id):
"""Get the user with the provided id
"""
Get the user with the provided id

:returns: a :class:`~deezer.resources.User` object"""
jsn = self.get_object("user", object_id)
return User(self, jsn)
:returns: a :class:`~deezer.resources.User` object
"""
return self.get_object("user", object_id)

def search(self, query, relation='track', **kwargs):
"""
Search track, album, artist or user

:returns: a list of :class:`~deezer.resources.Resource` objects.
"""
return self.get_object('search', relation, q=query, *kwargs)
Loading