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

feat: add method to rate an album #379

Merged
merged 3 commits into from
Oct 9, 2021
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ ENV*
node_modules
package.json
yarn.lock

# For secrets
.env
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ def client():
# This is to get human readable response output in VCR cassettes
headers={"Accept-Encoding": "identity"},
)


@pytest.fixture()
def client_token(client):
client.access_token = "dummy"
return client
39 changes: 39 additions & 0 deletions deezer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import requests

from deezer.exceptions import DeezerErrorResponse, DeezerHTTPError
from deezer.resources import (
Album,
Artist,
Expand Down Expand Up @@ -61,6 +62,7 @@ class Client:
"track": Track,
"user": User,
}
base_url = "https://api.deezer.com"

def __init__(
self, app_id=None, app_secret=None, access_token=None, headers=None, **kwargs
Expand Down Expand Up @@ -122,6 +124,33 @@ def object_url(self, object_t, object_id=None, relation=None):
request_path = "/".join(request_items)
return self.url(request_path)

def request(self, method: str, path: str, **params):
"""
Make a request to the API and parse the response.

:param method: HTTP verb to use: GET, POST< DELETE, ...
:param path: The path to make the API call to (e.g. 'artist/1234')
:param params: Query parameters to add the the request
:return:
"""
if self.access_token is not None:
params["access_token"] = str(self.access_token)
response = self.session.request(
method,
f"{self.base_url}/{path}",
params=params,
)
try:
response.raise_for_status()
except requests.HTTPError as exc:
raise DeezerHTTPError.from_http_error(exc) from exc
json_data = response.json()
if not isinstance(json_data, dict):
return json_data
if "error" in json_data:
raise DeezerErrorResponse(json_data)
return self._process_json(json_data)

def get_object(
self, object_t, object_id=None, relation=None, parent=None, **params
):
Expand Down Expand Up @@ -150,6 +179,16 @@ def get_album(self, object_id, relation=None, **kwargs):
"""
return self.get_object("album", object_id, relation=relation, **kwargs)

def rate_album(self, album_id: str, note: int) -> bool:
"""
Rate the album of the given ID with the given note.

The note should be and integer between 1 and 5.

:returns: boolean
"""
return self.request("POST", f"album/{album_id}", note=note)

def get_artist(self, object_id, relation=None, **kwargs):
"""
Get the artist with the provided ID.
Expand Down
53 changes: 53 additions & 0 deletions deezer/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Dict

import requests


class DeezerAPIException(Exception):
"""Base exception for API errors."""


class DeezerRetryableException(DeezerAPIException):
"""A request failing with this might work if retried."""


class DeezerHTTPError(DeezerAPIException):
"""Specialisation wrapping HTTPError from the requests library."""

def __init__(self, http_exception: requests.HTTPError, *args: object) -> None:
if http_exception.response is not None and http_exception.response.text:
url = http_exception.response.request.url
status_code = http_exception.response.status_code
text = http_exception.response.text
super().__init__(status_code, url, text, *args)
else:
super().__init__(http_exception, *args)

@classmethod
def from_http_error(cls, exc: requests.HTTPError) -> "DeezerHTTPError":
if exc.response.status_code in {502, 503, 504}:
return DeezerRetryableHTTPError(exc)
if exc.response.status_code == 403:
return DeezerForbiddenError(exc)
if exc.response.status_code == 404:
return DeezerNotFoundError(exc)
return DeezerHTTPError(exc)


class DeezerRetryableHTTPError(DeezerRetryableException, DeezerHTTPError):
"""A HTTP error due to a potentially temporary issue."""


class DeezerForbiddenError(DeezerHTTPError):
"""A HTTP error cause by permission denied error."""


class DeezerNotFoundError(DeezerHTTPError):
"""For 404 HTTP errors."""


class DeezerErrorResponse(DeezerAPIException):
"""A functional error when the API doesn't accept the request."""

def __init__(self, json_data: Dict[str, str]) -> None:
self.json_data = json_data
9 changes: 9 additions & 0 deletions deezer/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ def iter_tracks(self, **kwargs):
"""
return self.iter_relation("tracks", **kwargs)

def rate(self, note: int) -> bool:
"""
Rate the album with the given note.

:param note: rating to give.
:return: boolean, whether the album was rated
"""
return self.client.rate_album(album_id=self.id, note=note)


class Artist(Resource):
"""
Expand Down
54 changes: 54 additions & 0 deletions tests/cassettes/TestAlbum.test_rate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- identity
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- python-requests/2.26.0
method: POST
uri: https://api.deezer.com/album/302127?note=3&access_token=dummy
response:
body:
string: 'true'
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- X-Requested-With, Content-Type, Authorization, Origin, Accept, Accept-Encoding
Access-Control-Allow-Methods:
- POST, GET, OPTIONS, DELETE, PUT
Access-Control-Expose-Headers:
- Location
Access-Control-Max-Age:
- '86400'
Cache-Control:
- no-store, no-cache, must-revalidate
Connection:
- keep-alive
Content-Length:
- '4'
Content-Type:
- application/json; charset=utf-8
P3P:
- policyref="/w3c/p3p.xml" CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"
Pragma:
- no-cache
Server:
- Apache
X-Content-Type-Options:
- nosniff
X-Host:
- blm-web-59
x-org:
- FR
status:
code: 200
message: OK
version: 1
54 changes: 54 additions & 0 deletions tests/cassettes/TestClient.test_rate_album.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- identity
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- python-requests/2.26.0
method: POST
uri: https://api.deezer.com/album/302127?note=4&access_token=dummy
response:
body:
string: 'true'
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- X-Requested-With, Content-Type, Authorization, Origin, Accept, Accept-Encoding
Access-Control-Allow-Methods:
- POST, GET, OPTIONS, DELETE, PUT
Access-Control-Expose-Headers:
- Location
Access-Control-Max-Age:
- '86400'
Cache-Control:
- no-store, no-cache, must-revalidate
Connection:
- keep-alive
Content-Length:
- '4'
Content-Type:
- application/json; charset=utf-8
P3P:
- policyref="/w3c/p3p.xml" CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"
Pragma:
- no-cache
Server:
- Apache
X-Content-Type-Options:
- nosniff
X-Host:
- blm-web-53
x-org:
- FR
status:
code: 200
message: OK
version: 1
53 changes: 53 additions & 0 deletions tests/cassettes/TestClient.test_rate_album_error.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- identity
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- python-requests/2.26.0
method: POST
uri: https://api.deezer.com/album/302127?note=4
response:
body:
string: '{"error":{"type":"OAuthException","message":"You don''t have the right
permission to execute this method. You need the following permissions : basic_access"}}'
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- X-Requested-With, Content-Type, Authorization, Origin, Accept, Accept-Encoding
Access-Control-Allow-Methods:
- POST, GET, OPTIONS, DELETE, PUT
Access-Control-Expose-Headers:
- Location
Access-Control-Max-Age:
- '86400'
Connection:
- keep-alive
Content-Length:
- '157'
Content-Type:
- application/json; charset=utf-8
P3P:
- policyref="/w3c/p3p.xml" CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"
Server:
- Apache
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Host:
- blm-web-148
x-org:
- FR
status:
code: 200
message: OK
version: 1
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import requests

import deezer
from deezer.exceptions import DeezerErrorResponse

pytestmark = pytest.mark.vcr

Expand Down Expand Up @@ -300,3 +301,11 @@ def test_with_language_header(self, header_value, expected_name):
def test_process_json_types(self, client, json, expected_type):
result = client._process_json(json)
assert type(result) == expected_type

def test_rate_album(self, client_token):
result = client_token.rate_album(302127, 4)
assert result is True

def test_rate_album_error(self, client):
with pytest.raises(DeezerErrorResponse):
client.rate_album(302127, 4)
5 changes: 5 additions & 0 deletions tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ def test_as_dict(self, client):
album = client.get_album(302127)
assert album.as_dict()["id"] == 302127

def test_rate(self, client_token):
album = deezer.resources.Album(client_token, {"id": 302127})
result = album.rate(3)
assert result is True


class TestArtist:
def test_artist_attributes(self, client):
Expand Down