deezer-python 6.1.1__tar.gz → 7.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {deezer_python-6.1.1 → deezer_python-7.0.0}/PKG-INFO +6 -6
  2. {deezer_python-6.1.1 → deezer_python-7.0.0}/README.md +4 -4
  3. {deezer_python-6.1.1 → deezer_python-7.0.0}/pyproject.toml +24 -13
  4. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/__init__.py +1 -1
  5. deezer_python-7.0.0/src/deezer/auth.py +27 -0
  6. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/client.py +51 -59
  7. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/exceptions.py +6 -6
  8. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/pagination.py +7 -9
  9. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/editorial.py +4 -4
  10. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/episode.py +4 -5
  11. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/playlist.py +8 -14
  12. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/resource.py +15 -11
  13. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/user.py +22 -22
  14. {deezer_python-6.1.1 → deezer_python-7.0.0}/LICENSE.txt +0 -0
  15. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/dates.py +0 -0
  16. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/__init__.py +0 -0
  17. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/album.py +0 -0
  18. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/artist.py +0 -0
  19. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/chart.py +0 -0
  20. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/genre.py +0 -0
  21. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/podcast.py +0 -0
  22. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/radio.py +0 -0
  23. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/resources/track.py +0 -0
  24. {deezer_python-6.1.1 → deezer_python-7.0.0}/src/deezer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deezer-python
3
- Version: 6.1.1
3
+ Version: 7.0.0
4
4
  Summary: A friendly wrapper library for the Deezer API
5
5
  Home-page: https://github.com/browniebroke/deezer-python
6
6
  License: MIT
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Topic :: Software Development :: Libraries
23
- Requires-Dist: requests (>=2.18)
23
+ Requires-Dist: httpx (>=0.23.0)
24
24
  Project-URL: Bug Tracker, https://github.com/browniebroke/deezer-python/issues
25
25
  Project-URL: Changelog, https://deezer-python.readthedocs.io/en/latest/changelog.html
26
26
  Project-URL: Documentation, https://deezer-python.readthedocs.io
@@ -44,10 +44,10 @@ Description-Content-Type: text/markdown
44
44
  </p>
45
45
  <p align="center">
46
46
  <a href="https://python-poetry.org/">
47
- <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
47
+ <img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Poetry">
48
48
  </a>
49
- <a href="https://github.com/ambv/black">
50
- <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
49
+ <a href="https://github.com/astral-sh/ruff">
50
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
51
51
  </a>
52
52
  <a href="https://github.com/pre-commit/pre-commit">
53
53
  <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
@@ -89,7 +89,7 @@ The package is published on
89
89
  Easily query the Deezer API from you Python code. The data returned by the Deezer
90
90
  API is mapped to python resources:
91
91
 
92
- ```python
92
+ ```pycon
93
93
  >>> client = deezer.Client()
94
94
  >>> client.get_album(680407).title
95
95
  'Monkey Business'
@@ -13,10 +13,10 @@
13
13
  </p>
14
14
  <p align="center">
15
15
  <a href="https://python-poetry.org/">
16
- <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
16
+ <img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Poetry">
17
17
  </a>
18
- <a href="https://github.com/ambv/black">
19
- <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
18
+ <a href="https://github.com/astral-sh/ruff">
19
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
20
20
  </a>
21
21
  <a href="https://github.com/pre-commit/pre-commit">
22
22
  <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
@@ -58,7 +58,7 @@ The package is published on
58
58
  Easily query the Deezer API from you Python code. The data returned by the Deezer
59
59
  API is mapped to python resources:
60
60
 
61
- ```python
61
+ ```pycon
62
62
  >>> client = deezer.Client()
63
63
  >>> client.get_album(680407).title
64
64
  'Monkey Business'
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "deezer-python"
3
- version = "6.1.1"
3
+ version = "7.0.0"
4
4
  description = "A friendly wrapper library for the Deezer API"
5
5
  authors = ["Bruno Alla <alla.brunoo@gmail.com>"]
6
6
  license = "MIT"
@@ -28,25 +28,25 @@ packages = [
28
28
 
29
29
  [tool.poetry.dependencies]
30
30
  python = "^3.8"
31
- requests = ">=2.18"
31
+ httpx = ">=0.23.0"
32
32
 
33
33
  [tool.poetry.group.docs]
34
34
  optional = true
35
35
 
36
36
  [tool.poetry.group.docs.dependencies]
37
- myst-parser = ">=0.16"
38
- sphinx = ">=4.0"
39
- sphinx-autobuild = ">=2021.0"
40
- furo = ">=2023.5.20"
37
+ myst-parser = { version = ">=0.16", python = ">=3.11" }
38
+ sphinx = { version = ">=4.0", python = ">=3.11" }
39
+ furo = { version = ">=2023.5.20", python = ">=3.11" }
40
+ sphinx-autobuild = { version = ">=2024.0.0", python = ">=3.11" }
41
41
 
42
42
  [tool.poetry.group.dev.dependencies]
43
43
  coverage = "^7.0"
44
- environs = "^9.3"
45
- pytest = "^7.0"
46
- pytest-cov = "^4.0"
44
+ environs = "^11.0.0"
45
+ pytest = "^8.0.0"
46
+ pytest-cov = "^5.0.0"
47
47
  pytest-mock = "^3.6"
48
48
  pytest-vcr = "^1.0"
49
- vcrpy = "^5.0.0"
49
+ vcrpy = "^6.0.0"
50
50
  deezer-oauth-cli = "^1.0.0"
51
51
 
52
52
  [tool.semantic_release]
@@ -54,7 +54,6 @@ version_toml = ["pyproject.toml:tool.poetry.version"]
54
54
  version_variables = [
55
55
  "src/deezer/__init__.py:__version__",
56
56
  ]
57
- branch = "main"
58
57
  build_command = "pip install poetry && poetry build"
59
58
 
60
59
  [tool.semantic_release.changelog]
@@ -66,6 +65,16 @@ exclude_commit_patterns = [
66
65
  [tool.semantic_release.changelog.environment]
67
66
  keep_trailing_newline = true
68
67
 
68
+ [tool.semantic_release.branches.main]
69
+ match = "main"
70
+
71
+ [tool.semantic_release.branches.stable]
72
+ match = "(?!stable/*$)"
73
+
74
+ [tool.semantic_release.branches.noop]
75
+ match = "(?!main$)"
76
+ prerelease = true
77
+
69
78
  [tool.pytest.ini_options]
70
79
  addopts = "-v -Wdefault --cov=deezer"
71
80
  pythonpath = ["src"]
@@ -86,6 +95,8 @@ exclude_lines = [
86
95
  [tool.ruff]
87
96
  target-version = "py38"
88
97
  line-length = 120
98
+
99
+ [tool.ruff.lint]
89
100
  ignore = [
90
101
  "D203", # 1 blank line required before class docstring
91
102
  "D212", # Multi-line docstring summary should start at the first line
@@ -107,7 +118,7 @@ select = [
107
118
  "RUF", # ruff specific
108
119
  ]
109
120
 
110
- [tool.ruff.per-file-ignores]
121
+ [tool.ruff.lint.per-file-ignores]
111
122
  "tests/**/*" = [
112
123
  "D100",
113
124
  "D101",
@@ -120,7 +131,7 @@ select = [
120
131
  "conftest.py" = ["D100"]
121
132
  "docs/conf.py" = ["D100"]
122
133
 
123
- [tool.ruff.isort]
134
+ [tool.ruff.lint.isort]
124
135
  known-first-party = ["deezer"]
125
136
 
126
137
  [tool.mypy]
@@ -15,7 +15,7 @@ from deezer.resources import (
15
15
  User,
16
16
  )
17
17
 
18
- __version__ = "6.1.1"
18
+ __version__ = "7.0.0"
19
19
  __all__ = [
20
20
  "Album",
21
21
  "Artist",
@@ -0,0 +1,27 @@
1
+ import typing
2
+
3
+ import httpx
4
+
5
+
6
+ class DeezerQueryAuth(httpx.Auth):
7
+ """
8
+ Deezer Auth for httpx.
9
+
10
+ Attach given access token to the request,
11
+ by passing it as query parameter.
12
+
13
+ :param access_token: Your Deezer access token.
14
+ """
15
+
16
+ def __init__(self, access_token: str):
17
+ self.access_token = access_token
18
+
19
+ def auth_flow(self, request: httpx.Request) -> typing.Generator[httpx.Request, None, None]:
20
+ """
21
+ Add authentication query parameter to the request.
22
+
23
+ :param request: httpx.Request
24
+ :return: httpx.Request
25
+ """
26
+ request.url = request.url.copy_merge_params({"access_token": self.access_token})
27
+ yield request
@@ -2,8 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, ClassVar
4
4
 
5
- import requests
5
+ import httpx
6
+ from httpx._types import HeaderTypes
6
7
 
8
+ from deezer.auth import DeezerQueryAuth
7
9
  from deezer.exceptions import (
8
10
  DeezerErrorResponse,
9
11
  DeezerHTTPError,
@@ -26,7 +28,7 @@ from deezer.resources import (
26
28
  )
27
29
 
28
30
 
29
- class Client:
31
+ class Client(httpx.Client):
30
32
  """
31
33
  A client to retrieve some basic infos about Deezer resources.
32
34
 
@@ -34,7 +36,7 @@ class Client:
34
36
  be passed in to the constructor as kwargs.
35
37
 
36
38
  >>> import deezer
37
- >>> client = deezer.Client(app_id='foo', app_secret='bar')
39
+ >>> client = deezer.Client()
38
40
 
39
41
  This client provides several methods to retrieve the content most
40
42
  kinds of Deezer objects, based on their json structure.
@@ -45,8 +47,6 @@ class Client:
45
47
  >>> import deezer
46
48
  >>> client = deezer.Client(headers={'Accept-Language': 'fr'})
47
49
 
48
- :param app_id: application ID.
49
- :param app_secret: application secret.
50
50
  :param access_token: user access token.
51
51
  :param headers: a dictionary of headers to be used.
52
52
  """
@@ -66,18 +66,21 @@ class Client:
66
66
  "track": Track,
67
67
  "user": User,
68
68
  }
69
- base_url = "https://api.deezer.com"
70
69
 
71
70
  def __init__(
72
- self, app_id=None, app_secret=None, access_token=None, headers=None, **kwargs
71
+ self,
72
+ access_token: str | None = None,
73
+ headers: HeaderTypes | None = None,
73
74
  ):
74
- self.app_id = app_id
75
- self.app_secret = app_secret
76
- self.access_token = access_token
77
- self.session = requests.Session()
78
-
79
- headers = headers or {}
80
- self.session.headers.update(headers)
75
+ if access_token:
76
+ deezer_auth = DeezerQueryAuth(access_token=access_token)
77
+ else:
78
+ deezer_auth = None
79
+ super().__init__(
80
+ base_url="https://api.deezer.com",
81
+ auth=deezer_auth,
82
+ headers=headers,
83
+ )
81
84
 
82
85
  def _process_json(
83
86
  self,
@@ -98,9 +101,7 @@ class Client:
98
101
  :returns: instance of :class:`~deezer.Resource`
99
102
  """
100
103
  if "data" in item:
101
- parsed_data = [
102
- self._process_json(i, parent, paginate_list=False) for i in item["data"]
103
- ]
104
+ parsed_data = [self._process_json(i, parent, paginate_list=False) for i in item["data"]]
104
105
  if not paginate_list:
105
106
  return parsed_data
106
107
  item["data"] = parsed_data
@@ -139,7 +140,7 @@ class Client:
139
140
  resource_type: type[Resource] | None = None,
140
141
  resource_id: int | None = None,
141
142
  paginate_list=False,
142
- **params,
143
+ **kwargs,
143
144
  ):
144
145
  """
145
146
  Make a request to the API and parse the response.
@@ -150,23 +151,20 @@ class Client:
150
151
  :param resource_type: The resource class to use as top level.
151
152
  :param resource_id: The resource id to use as top level.
152
153
  :param paginate_list: Whether to wrap list into a pagination object.
153
- :param params: Query parameters to add to the request
154
154
  """
155
- if self.access_token is not None:
156
- params["access_token"] = str(self.access_token)
157
- response = self.session.request(
155
+ response = super().request(
158
156
  method,
159
- f"{self.base_url}/{path}",
160
- params=params,
157
+ path,
158
+ **kwargs,
161
159
  )
162
160
  try:
163
161
  response.raise_for_status()
164
- except requests.HTTPError as exc:
162
+ except httpx.HTTPStatusError as exc:
165
163
  raise DeezerHTTPError.from_http_error(exc) from exc
166
164
  json_data = response.json()
167
165
  if not isinstance(json_data, dict):
168
166
  return json_data
169
- if "error" in json_data and json_data["error"]:
167
+ if json_data.get("error"):
170
168
  raise DeezerErrorResponse(json_data)
171
169
  return self._process_json(
172
170
  json_data,
@@ -176,8 +174,8 @@ class Client:
176
174
  paginate_list=paginate_list,
177
175
  )
178
176
 
179
- def _get_paginated_list(self, path, **params):
180
- return PaginatedList(client=self, base_path=path, **params)
177
+ def _get_paginated_list(self, path: str, params: dict | None = None):
178
+ return PaginatedList(client=self, base_path=path, params=params)
181
179
 
182
180
  def get_album(self, album_id: int) -> Album:
183
181
  """
@@ -204,9 +202,7 @@ class Client:
204
202
  :param genre_id: the genre ID, default to `All` genre (genre_id = 0).
205
203
  :returns: a :class:`~deezer.Chart` instance.
206
204
  """
207
- return self.request(
208
- "GET", f"chart/{genre_id}", resource_type=Chart, resource_id=genre_id
209
- )
205
+ return self.request("GET", f"chart/{genre_id}", resource_type=Chart, resource_id=genre_id)
210
206
 
211
207
  def get_tracks_chart(self, genre_id: int = 0) -> list[Track]:
212
208
  """
@@ -359,9 +355,7 @@ class Client:
359
355
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
360
356
  of :class:`Track <deezer.Track>` instances
361
357
  """
362
- return PaginatedList(
363
- client=self, base_path="user/me/recommendations/tracks", **kwargs
364
- )
358
+ return PaginatedList(client=self, base_path="user/me/recommendations/tracks", **kwargs)
365
359
 
366
360
  def get_user_recommended_albums(self, **kwargs) -> PaginatedList[Album]:
367
361
  """
@@ -370,9 +364,7 @@ class Client:
370
364
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
371
365
  of :class:`Track <deezer.Album>` instances
372
366
  """
373
- return PaginatedList(
374
- client=self, base_path="user/me/recommendations/albums", **kwargs
375
- )
367
+ return PaginatedList(client=self, base_path="user/me/recommendations/albums", **kwargs)
376
368
 
377
369
  def get_user_recommended_artists(self, **kwargs) -> PaginatedList[Artist]:
378
370
  """
@@ -381,9 +373,7 @@ class Client:
381
373
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
382
374
  of :class:`Track <deezer.Artist>` instances
383
375
  """
384
- return PaginatedList(
385
- client=self, base_path="user/me/recommendations/artists", **kwargs
386
- )
376
+ return PaginatedList(client=self, base_path="user/me/recommendations/artists", **kwargs)
387
377
 
388
378
  def get_user_recommended_playlists(self, **kwargs) -> PaginatedList[Playlist]:
389
379
  """
@@ -392,9 +382,7 @@ class Client:
392
382
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
393
383
  of :class:`Track <deezer.Playlist>` instances
394
384
  """
395
- return PaginatedList(
396
- client=self, base_path="user/me/recommendations/playlists", **kwargs
397
- )
385
+ return PaginatedList(client=self, base_path="user/me/recommendations/playlists", **kwargs)
398
386
 
399
387
  def get_user_flow(self, **kwargs) -> PaginatedList[Track]:
400
388
  """
@@ -423,7 +411,7 @@ class Client:
423
411
  :param album_id: the ID of the album to add.
424
412
  :return: boolean whether the operation succeeded.
425
413
  """
426
- return self.request("POST", "user/me/albums", album_id=album_id)
414
+ return self.request("POST", "user/me/albums", params={"album_id": album_id})
427
415
 
428
416
  def remove_user_album(self, album_id: int) -> bool:
429
417
  """
@@ -432,7 +420,7 @@ class Client:
432
420
  :param album_id: the ID of the album to remove.
433
421
  :return: boolean whether the operation succeeded.
434
422
  """
435
- return self.request("DELETE", "user/me/albums", album_id=album_id)
423
+ return self.request("DELETE", "user/me/albums", params={"album_id": album_id})
436
424
 
437
425
  def get_user_artists(self, user_id: int | None = None) -> PaginatedList[Artist]:
438
426
  """
@@ -452,7 +440,7 @@ class Client:
452
440
  :param artist_id: the ID of the artist to add.
453
441
  :return: boolean whether the operation succeeded.
454
442
  """
455
- return self.request("POST", "user/me/artists", artist_id=artist_id)
443
+ return self.request("POST", "user/me/artists", params={"artist_id": artist_id})
456
444
 
457
445
  def remove_user_artist(self, artist_id: int) -> bool:
458
446
  """
@@ -461,7 +449,11 @@ class Client:
461
449
  :param artist_id: the ID of the artist to remove.
462
450
  :return: boolean whether the operation succeeded.
463
451
  """
464
- return self.request("DELETE", "user/me/artists", artist_id=artist_id)
452
+ return self.request(
453
+ "DELETE",
454
+ "user/me/artists",
455
+ params={"artist_id": artist_id},
456
+ )
465
457
 
466
458
  def get_user_followers(self, user_id: int | None = None) -> PaginatedList[User]:
467
459
  """
@@ -492,7 +484,7 @@ class Client:
492
484
  :param user_id: the ID of the user to follow.
493
485
  :return: boolean whether the operation succeeded.
494
486
  """
495
- return self.request("POST", "user/me/followings", user_id=user_id)
487
+ return self.request("POST", "user/me/followings", params={"user_id": user_id})
496
488
 
497
489
  def remove_user_following(self, user_id: int) -> bool:
498
490
  """
@@ -501,7 +493,7 @@ class Client:
501
493
  :param user_id: the ID of the user to stop following.
502
494
  :return: boolean whether the operation succeeded.
503
495
  """
504
- return self.request("DELETE", "user/me/followings", user_id=user_id)
496
+ return self.request("DELETE", "user/me/followings", params={"user_id": user_id})
505
497
 
506
498
  def get_user_history(self) -> PaginatedList[Track]:
507
499
  """
@@ -530,7 +522,7 @@ class Client:
530
522
  :param track_id: the ID of the track to add.
531
523
  :return: boolean whether the operation succeeded.
532
524
  """
533
- return self.request("POST", "user/me/tracks", track_id=track_id)
525
+ return self.request("POST", "user/me/tracks", params={"track_id": track_id})
534
526
 
535
527
  def remove_user_track(self, track_id: int) -> bool:
536
528
  """
@@ -539,7 +531,7 @@ class Client:
539
531
  :param track_id: the ID of the track to remove.
540
532
  :return: boolean whether the operation succeeded.
541
533
  """
542
- return self.request("DELETE", "user/me/tracks", track_id=track_id)
534
+ return self.request("DELETE", "user/me/tracks", params={"track_id": track_id})
543
535
 
544
536
  def remove_user_playlist(self, playlist_id: int) -> bool:
545
537
  """
@@ -548,7 +540,7 @@ class Client:
548
540
  :param playlist_id: the ID of the playlist to remove.
549
541
  :return: boolean whether the operation succeeded.
550
542
  """
551
- return self.request("DELETE", "user/me/playlists", playlist_id=playlist_id)
543
+ return self.request("DELETE", "user/me/playlists", params={"playlist_id": playlist_id})
552
544
 
553
545
  def add_user_playlist(self, playlist_id: int) -> bool:
554
546
  """
@@ -557,7 +549,7 @@ class Client:
557
549
  :param playlist_id: the ID of the playlist to add.
558
550
  :return: boolean whether the operation succeeded.
559
551
  """
560
- return self.request("POST", "user/me/playlists", playlist_id=playlist_id)
552
+ return self.request("POST", "user/me/playlists", params={"playlist_id": playlist_id})
561
553
 
562
554
  def create_playlist(self, playlist_name) -> int:
563
555
  """
@@ -566,7 +558,7 @@ class Client:
566
558
  :param playlist_name: the name of the playlist.
567
559
  :return: the ID of the playlist that was created
568
560
  """
569
- result = self.request("POST", "user/me/playlists", title=playlist_name)
561
+ result = self.request("POST", "user/me/playlists", params={"title": playlist_name})
570
562
  # Note: the REST API call returns a dict with just the "id" key in it,
571
563
  # so we return that instead of the full Playlist object
572
564
  return result.id
@@ -597,15 +589,15 @@ class Client:
597
589
  if query:
598
590
  query_parts.append(query)
599
591
  query_parts.extend(
600
- f'{param_name}:"{param_value}"'
601
- for param_name, param_value in advanced_params.items()
602
- if param_value
592
+ f'{param_name}:"{param_value}"' for param_name, param_value in advanced_params.items() if param_value
603
593
  )
604
594
 
605
595
  return self._get_paginated_list(
606
596
  path=f"search/{path}" if path else "search",
607
- q=" ".join(query_parts),
608
- **optional_params,
597
+ params={
598
+ "q": " ".join(query_parts),
599
+ **optional_params,
600
+ },
609
601
  )
610
602
 
611
603
  def search(
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import requests
3
+ import httpx
4
4
 
5
5
 
6
6
  class DeezerAPIException(Exception):
@@ -12,9 +12,9 @@ class DeezerRetryableException(DeezerAPIException):
12
12
 
13
13
 
14
14
  class DeezerHTTPError(DeezerAPIException):
15
- """Specialisation wrapping HTTPError from the requests library."""
15
+ """Specialization wrapping HTTPError from the httpx library."""
16
16
 
17
- def __init__(self, http_exception: requests.HTTPError, *args: object) -> None:
17
+ def __init__(self, http_exception: httpx.HTTPStatusError, *args: object) -> None:
18
18
  if http_exception.response is not None and http_exception.response.text:
19
19
  url = http_exception.response.request.url
20
20
  status_code = http_exception.response.status_code
@@ -24,8 +24,8 @@ class DeezerHTTPError(DeezerAPIException):
24
24
  super().__init__(http_exception, *args)
25
25
 
26
26
  @classmethod
27
- def from_http_error(cls, exc: requests.HTTPError) -> DeezerHTTPError:
28
- """Initialise the appropriate internal exception from a HTTPError."""
27
+ def from_http_error(cls, exc: httpx.HTTPStatusError) -> DeezerHTTPError:
28
+ """Initialize the appropriate internal exception from a HTTPError."""
29
29
  if exc.response is not None:
30
30
  if exc.response.status_code in {502, 503, 504}:
31
31
  return DeezerRetryableHTTPError(exc)
@@ -37,7 +37,7 @@ class DeezerHTTPError(DeezerAPIException):
37
37
 
38
38
 
39
39
  class DeezerRetryableHTTPError(DeezerRetryableException, DeezerHTTPError):
40
- """A HTTP error due to a potentially temporary issue."""
40
+ """An HTTP error due to a potentially temporary issue."""
41
41
 
42
42
 
43
43
  class DeezerForbiddenError(DeezerHTTPError):
@@ -20,14 +20,14 @@ class PaginatedList(Generic[ResourceType]):
20
20
  client: deezer.Client,
21
21
  base_path: str,
22
22
  parent: deezer.Resource | None = None,
23
- **params,
23
+ params: dict | None = None,
24
24
  ):
25
25
  self.__elements: list[ResourceType] = []
26
26
  self.__client = client
27
27
  self.__base_path = base_path
28
- self.__base_params = params
28
+ self.__base_params = params or {}
29
29
  self.__next_path: str | None = base_path
30
- self.__next_params = params
30
+ self.__next_params = params or {}
31
31
  self.__parent = parent
32
32
  self.__total = None
33
33
  self.__iter = iter(self)
@@ -41,12 +41,10 @@ class PaginatedList(Generic[ResourceType]):
41
41
  return f"<{self.__class__.__name__} {data!r}>"
42
42
 
43
43
  @overload
44
- def __getitem__(self, index: int) -> ResourceType:
45
- ...
44
+ def __getitem__(self, index: int) -> ResourceType: ...
46
45
 
47
46
  @overload
48
- def __getitem__(self, index: slice) -> list[ResourceType]:
49
- ...
47
+ def __getitem__(self, index: slice) -> list[ResourceType]: ...
50
48
 
51
49
  def __getitem__(
52
50
  self,
@@ -92,7 +90,7 @@ class PaginatedList(Generic[ResourceType]):
92
90
  self.__next_path,
93
91
  parent=self.__parent,
94
92
  paginate_list=True,
95
- **self.__next_params,
93
+ params=self.__next_params,
96
94
  )
97
95
  self.__next_path = None
98
96
  self.__total = response_payload.get("total")
@@ -118,7 +116,7 @@ class PaginatedList(Generic[ResourceType]):
118
116
  self.__base_path,
119
117
  parent=self.__parent,
120
118
  paginate_list=True,
121
- **params,
119
+ params=params,
122
120
  )
123
121
  self.__total = response_payload["total"]
124
122
  assert self.__total is not None # noqa S101
@@ -26,21 +26,21 @@ class Editorial(Resource):
26
26
  picture_big: str
27
27
  picture_xl: str
28
28
 
29
- def get_selection(self, **kwargs) -> list[Album]:
29
+ def get_selection(self) -> list[Album]:
30
30
  """
31
31
  Get a list of albums selected every week by the Deezer Team.
32
32
 
33
33
  :returns: a list of :class:`Album <deezer.Album>` instances
34
34
  """
35
- return self.get_relation("selection", **kwargs)
35
+ return self.get_relation("selection")
36
36
 
37
- def get_chart(self, **kwargs) -> Chart:
37
+ def get_chart(self) -> Chart:
38
38
  """
39
39
  Get top charts for tracks, albums, artists and playlists.
40
40
 
41
41
  :returns: a :class:`~deezer.Chart` instance
42
42
  """
43
- return self.get_relation("charts", resource_type=Chart, **kwargs)
43
+ return self.get_relation("charts", resource_type=Chart)
44
44
 
45
45
  def get_releases(self, **kwargs) -> PaginatedList[Album]:
46
46
  """
@@ -42,7 +42,7 @@ class Episode(Resource):
42
42
  return f"{self.link}?utm_source=deezer&utm_content=episode-{self.id}&utm_medium=web"
43
43
  return super()._infer_missing_field(item)
44
44
 
45
- def add_bookmark(self, offset: int, **kwargs) -> bool:
45
+ def add_bookmark(self, offset: int) -> bool:
46
46
  """
47
47
  Sets a bookmark on the episode.
48
48
 
@@ -52,14 +52,13 @@ class Episode(Resource):
52
52
  return self.client.request(
53
53
  "POST",
54
54
  f"episode/{self.id}/bookmark",
55
- offset=offset,
56
- **kwargs,
55
+ params={"offset": offset},
57
56
  )
58
57
 
59
- def remove_bookmark(self, **kwargs) -> bool:
58
+ def remove_bookmark(self) -> bool:
60
59
  """
61
60
  Removes the bookmark on the episode.
62
61
 
63
62
  :returns: a boolean that tells if the operation was successful
64
63
  """
65
- return self.client.request("DELETE", f"episode/{self.id}/bookmark", **kwargs)
64
+ return self.client.request("DELETE", f"episode/{self.id}/bookmark")
@@ -58,15 +58,15 @@ class Playlist(Resource):
58
58
  """
59
59
  return self.get_paginated_list("fans", **kwargs)
60
60
 
61
- def mark_seen(self, **kwargs) -> bool:
61
+ def mark_seen(self) -> bool:
62
62
  """
63
63
  Mark the playlist as seen.
64
64
 
65
65
  :returns: a boolean that tells if the operation was successful
66
66
  """
67
- return self.client.request("POST", f"playlist/{self.id}/seen", **kwargs)
67
+ return self.client.request("POST", f"playlist/{self.id}/seen")
68
68
 
69
- def add_tracks(self, tracks: Iterable[int | Track], **kwargs) -> bool:
69
+ def add_tracks(self, tracks: Iterable[int | Track]) -> bool:
70
70
  """
71
71
  Add tracks to a playlist.
72
72
 
@@ -75,11 +75,9 @@ class Playlist(Resource):
75
75
  :returns: a boolean that tells if the operation was successful
76
76
  """
77
77
  track_ids_str = ",".join(str(tid) for tid in gen_ids(tracks))
78
- return self.client.request(
79
- "POST", f"playlist/{self.id}/tracks", songs=track_ids_str, **kwargs
80
- )
78
+ return self.client.request("POST", f"playlist/{self.id}/tracks", params={"songs": track_ids_str})
81
79
 
82
- def delete_tracks(self, tracks: Iterable[int | Track], **kwargs) -> bool:
80
+ def delete_tracks(self, tracks: Iterable[int | Track]) -> bool:
83
81
  """
84
82
  Delete tracks from a playlist.
85
83
 
@@ -88,11 +86,9 @@ class Playlist(Resource):
88
86
  :returns: a boolean that tells if the operation was successful
89
87
  """
90
88
  track_ids_str = ",".join(map(str, gen_ids(tracks)))
91
- return self.client.request(
92
- "DELETE", f"playlist/{self.id}/tracks", songs=track_ids_str, **kwargs
93
- )
89
+ return self.client.request("DELETE", f"playlist/{self.id}/tracks", params={"songs": track_ids_str})
94
90
 
95
- def reorder_tracks(self, order: Iterable[int | Track], **kwargs) -> bool:
91
+ def reorder_tracks(self, order: Iterable[int | Track]) -> bool:
96
92
  """
97
93
  Reorder the tracks of a playlist.
98
94
 
@@ -101,6 +97,4 @@ class Playlist(Resource):
101
97
  :returns: a boolean that tells if the operation was successful
102
98
  """
103
99
  order_track_ids_str = ",".join(map(str, gen_ids(order)))
104
- return self.client.request(
105
- "POST", f"playlist/{self.id}/tracks", order=order_track_ids_str, **kwargs
106
- )
100
+ return self.client.request("POST", f"playlist/{self.id}/tracks", params={"order": order_track_ids_str})
@@ -56,7 +56,12 @@ class Resource:
56
56
  result[key] = value
57
57
  return result
58
58
 
59
- def get_relation(self, relation, **kwargs):
59
+ def get_relation(
60
+ self,
61
+ relation: str,
62
+ resource_type: type[Resource] | None = None, # type: ignore[valid-type]
63
+ params: dict | None = None,
64
+ ):
60
65
  """
61
66
  Generic method to load the relation from any resource.
62
67
 
@@ -69,10 +74,11 @@ class Resource:
69
74
  "GET",
70
75
  f"{self.type}/{self.id}/{relation}",
71
76
  parent=self,
72
- **kwargs,
77
+ resource_type=resource_type,
78
+ params=params,
73
79
  )
74
80
 
75
- def post_relation(self, relation, **kwargs):
81
+ def post_relation(self, relation: str, params: dict):
76
82
  """
77
83
  Generic method to make a POST request to a relation from any resource.
78
84
 
@@ -82,10 +88,10 @@ class Resource:
82
88
  return self.client.request(
83
89
  "POST",
84
90
  f"{self.type}/{self.id}/{relation}",
85
- **kwargs,
91
+ params=params,
86
92
  )
87
93
 
88
- def delete_relation(self, relation, **kwargs):
94
+ def delete_relation(self, relation: str, params: dict | None = None):
89
95
  """
90
96
  Generic method to make a DELETE request to a relation from any resource.
91
97
 
@@ -95,20 +101,20 @@ class Resource:
95
101
  return self.client.request(
96
102
  "DELETE",
97
103
  f"{self.type}/{self.id}/{relation}",
98
- **kwargs,
104
+ params=params,
99
105
  )
100
106
 
101
107
  def get_paginated_list(
102
108
  self,
103
109
  relation: str,
104
- **kwargs,
110
+ params: dict | None = None,
105
111
  ) -> PaginatedList:
106
112
  """Build the pagination object based on the relation."""
107
113
  return PaginatedList(
108
114
  client=self.client,
109
115
  base_path=f"{self.type}/{self.id}/{relation}",
110
116
  parent=self,
111
- **kwargs,
117
+ params=params,
112
118
  )
113
119
 
114
120
  def __getattr__(self, item: str) -> Any:
@@ -132,9 +138,7 @@ class Resource:
132
138
  setattr(self, field_name, getattr(full_resource, field_name))
133
139
  self._fields += (field_name,)
134
140
  return getattr(self, item)
135
- raise AttributeError(
136
- f"'{self.__class__.__name__}' object has no attribute '{item}'"
137
- )
141
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'")
138
142
 
139
143
  def _infer_missing_field(self, item: str) -> Any:
140
144
  """
@@ -48,14 +48,14 @@ class User(Resource):
48
48
  _parse_birthday = staticmethod(parse_date)
49
49
  _parse_inscription_date = staticmethod(parse_date)
50
50
 
51
- def get_albums(self, **kwargs) -> PaginatedList[Album]:
51
+ def get_albums(self, **params) -> PaginatedList[Album]:
52
52
  """
53
53
  Get user's favorite albums.
54
54
 
55
55
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
56
56
  of :class:`Album <deezer.Album>` instances
57
57
  """
58
- return self.get_paginated_list("albums", **kwargs)
58
+ return self.get_paginated_list("albums", params=params)
59
59
 
60
60
  def add_album(self, album: Album | int):
61
61
  """
@@ -64,7 +64,7 @@ class User(Resource):
64
64
  :param album: an :class:`~deezer.Album` instance or its ID
65
65
  :returns: a boolean that tells if the operation was successful
66
66
  """
67
- return self.post_relation("albums", album_id=get_id(album))
67
+ return self.post_relation("albums", params={"album_id": get_id(album)})
68
68
 
69
69
  def remove_album(self, album: Album | int):
70
70
  """
@@ -73,7 +73,7 @@ class User(Resource):
73
73
  :param album: an :class:`~deezer.Album` instance or its ID
74
74
  :returns: a boolean that tells if the operation was successful
75
75
  """
76
- return self.delete_relation("albums", album_id=get_id(album))
76
+ return self.delete_relation("albums", params={"album_id": get_id(album)})
77
77
 
78
78
  def get_tracks(self, **kwargs) -> PaginatedList[Track]:
79
79
  """
@@ -82,7 +82,7 @@ class User(Resource):
82
82
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
83
83
  of :class:`Track <deezer.Track>` instances
84
84
  """
85
- return self.get_paginated_list("tracks", **kwargs)
85
+ return self.get_paginated_list("tracks")
86
86
 
87
87
  def add_track(self, track: Track | int):
88
88
  """
@@ -91,7 +91,7 @@ class User(Resource):
91
91
  :param track: a :class:`~deezer.Track` instance or its ID
92
92
  :returns: a boolean that tells if the operation was successful
93
93
  """
94
- return self.post_relation("tracks", track_id=get_id(track))
94
+ return self.post_relation("tracks", params={"track_id": get_id(track)})
95
95
 
96
96
  def remove_track(self, track: Track | int):
97
97
  """
@@ -100,16 +100,16 @@ class User(Resource):
100
100
  :param track: a :class:`~deezer.Track` instance or its ID
101
101
  :returns: a boolean that tells if the operation was successful
102
102
  """
103
- return self.delete_relation("tracks", track_id=get_id(track))
103
+ return self.delete_relation("tracks", params={"track_id": get_id(track)})
104
104
 
105
- def get_artists(self, **kwargs) -> PaginatedList[Artist]:
105
+ def get_artists(self, **params) -> PaginatedList[Artist]:
106
106
  """
107
107
  Get user's favorite artists.
108
108
 
109
109
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
110
110
  of :class:`Artist <deezer.Artist>` instances
111
111
  """
112
- return self.get_paginated_list("artists", **kwargs)
112
+ return self.get_paginated_list("artists", params=params)
113
113
 
114
114
  def add_artist(self, artist: Artist | int):
115
115
  """
@@ -118,7 +118,7 @@ class User(Resource):
118
118
  :param artist: an :class:`~deezer.Artist` instance or its ID
119
119
  :returns: a boolean that tells if the operation was successful
120
120
  """
121
- return self.post_relation("artists", artist_id=get_id(artist))
121
+ return self.post_relation("artists", params={"artist_id": get_id(artist)})
122
122
 
123
123
  def remove_artist(self, artist: Artist | int):
124
124
  """
@@ -127,25 +127,25 @@ class User(Resource):
127
127
  :param artist: an :class:`~deezer.Artist` instance or its ID
128
128
  :returns: a boolean that tells if the operation was successful
129
129
  """
130
- return self.delete_relation("artists", artist_id=get_id(artist))
130
+ return self.delete_relation("artists", params={"artist_id": get_id(artist)})
131
131
 
132
- def get_followers(self, **kwargs) -> PaginatedList[User]:
132
+ def get_followers(self, **params) -> PaginatedList[User]:
133
133
  """
134
134
  Get user's followers.
135
135
 
136
136
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
137
137
  of :class:`User <deezer.User>` instances
138
138
  """
139
- return self.get_paginated_list("followers", **kwargs)
139
+ return self.get_paginated_list("followers", params=params)
140
140
 
141
- def get_followings(self, **kwargs) -> PaginatedList[User]:
141
+ def get_followings(self, **params) -> PaginatedList[User]:
142
142
  """
143
143
  Get user's followings.
144
144
 
145
145
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
146
146
  of :class:`User <deezer.User>` instances
147
147
  """
148
- return self.get_paginated_list("followings", **kwargs)
148
+ return self.get_paginated_list("followings", params=params)
149
149
 
150
150
  def follow(self, user: User | int):
151
151
  """
@@ -154,7 +154,7 @@ class User(Resource):
154
154
  :param user: a :class:`~deezer.User` instance or its ID
155
155
  :returns: a boolean that tells if the operation was successful
156
156
  """
157
- return self.post_relation("followings", user_id=get_id(user))
157
+ return self.post_relation("followings", params={"user_id": get_id(user)})
158
158
 
159
159
  def unfollow(self, user: User | int):
160
160
  """
@@ -163,16 +163,16 @@ class User(Resource):
163
163
  :param user: a :class:`~deezer.User` instance or its ID
164
164
  :returns: a boolean that tells if the operation was successful
165
165
  """
166
- return self.delete_relation("followings", user_id=get_id(user))
166
+ return self.delete_relation("followings", params={"user_id": get_id(user)})
167
167
 
168
- def get_playlists(self, **kwargs) -> PaginatedList[Playlist]:
168
+ def get_playlists(self, **params) -> PaginatedList[Playlist]:
169
169
  """
170
170
  Get user's public playlists.
171
171
 
172
172
  :returns: a :class:`PaginatedList <deezer.PaginatedList>`
173
173
  of :class:`Playlist <deezer.Playlist>` instances
174
174
  """
175
- return self.get_paginated_list("playlists", **kwargs)
175
+ return self.get_paginated_list("playlists", params=params)
176
176
 
177
177
  def add_playlist(self, playlist: Playlist | int):
178
178
  """
@@ -181,7 +181,7 @@ class User(Resource):
181
181
  :param playlist: a :class:`~deezer.Playlist` instance or its ID
182
182
  :returns: a boolean that tells if the operation was successful
183
183
  """
184
- return self.post_relation("playlists", playlist_id=get_id(playlist))
184
+ return self.post_relation("playlists", params={"playlist_id": get_id(playlist)})
185
185
 
186
186
  def remove_playlist(self, playlist: Playlist | int):
187
187
  """
@@ -190,7 +190,7 @@ class User(Resource):
190
190
  :param playlist: a :class:`~deezer.Playlist` instance or its ID
191
191
  :returns: a boolean that tells if the operation was successful
192
192
  """
193
- return self.delete_relation("playlists", playlist_id=get_id(playlist))
193
+ return self.delete_relation("playlists", params={"playlist_id": get_id(playlist)})
194
194
 
195
195
  def create_playlist(self, title: str) -> int:
196
196
  """
@@ -199,7 +199,7 @@ class User(Resource):
199
199
  :param title: the title of the playlist
200
200
  :returns: the ID of the playlist that was created
201
201
  """
202
- result = self.post_relation("playlists", title=title)
202
+ result = self.post_relation("playlists", params={"title": title})
203
203
  # Note: the REST API call returns a dict with just the "id" key in it,
204
204
  # so we return that instead of the full Playlist object
205
205
  return result.id
File without changes