deezer-python-gql 0.1.0__tar.gz → 0.2.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.
- {deezer_python_gql-0.1.0/deezer_python_gql.egg-info → deezer_python_gql-0.2.0}/PKG-INFO +24 -12
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/README.md +20 -8
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql/base_client.py +65 -4
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0/deezer_python_gql.egg-info}/PKG-INFO +24 -12
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/requires.txt +3 -3
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/pyproject.toml +4 -4
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/tests/test_client.py +264 -1
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/LICENSE +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/MANIFEST.in +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql/__init__.py +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql/py.typed +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/SOURCES.txt +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/not-zip-safe +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/top_level.txt +0 -0
- {deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Async typed Python client for Deezer's Pipe GraphQL API.
|
|
5
5
|
Author-email: Julian Daberkow <jdaberkow@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,14 +14,14 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: httpx>=0.27.0
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
16
|
Provides-Extra: test
|
|
17
|
-
Requires-Dist: codespell==2.4.
|
|
17
|
+
Requires-Dist: codespell==2.4.2; extra == "test"
|
|
18
18
|
Requires-Dist: mypy==1.19.1; extra == "test"
|
|
19
19
|
Requires-Dist: pre-commit==4.5.1; extra == "test"
|
|
20
20
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
21
21
|
Requires-Dist: pytest==9.0.2; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "test"
|
|
23
|
-
Requires-Dist: pytest-cov==7.
|
|
24
|
-
Requires-Dist: ruff==0.15.
|
|
23
|
+
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
24
|
+
Requires-Dist: ruff==0.15.7; extra == "test"
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
27
27
|
Dynamic: license-file
|
|
@@ -79,14 +79,26 @@ asyncio.run(main())
|
|
|
79
79
|
|
|
80
80
|
## Available Queries
|
|
81
81
|
|
|
82
|
-
| Method
|
|
83
|
-
|
|
|
84
|
-
| `get_me()`
|
|
85
|
-
| `get_track(track_id)`
|
|
86
|
-
| `get_album(album_id)`
|
|
87
|
-
| `get_artist(artist_id)`
|
|
88
|
-
| `get_playlist(playlist_id)`
|
|
89
|
-
| `search(query, ...)`
|
|
82
|
+
| Method | Description |
|
|
83
|
+
| ------------------------------------------------ | ------------------------------------------------------------- |
|
|
84
|
+
| `get_me()` | Current authenticated user |
|
|
85
|
+
| `get_track(track_id)` | Full track details — ISRC, media tokens, lyrics, contributors |
|
|
86
|
+
| `get_album(album_id)` | Album with cover, label, paginated tracks, fallback |
|
|
87
|
+
| `get_artist(artist_id)` | Artist with bio, top tracks, albums (ordered by release date) |
|
|
88
|
+
| `get_playlist(playlist_id)` | Playlist with owner, picture, paginated tracks |
|
|
89
|
+
| `search(query, ...)` | Unified search across tracks, albums, artists, playlists |
|
|
90
|
+
| `get_flow()` | User's default Flow with tracks |
|
|
91
|
+
| `get_flow_configs(moods_first, genres_first)` | Mood & genre flow config lists for discovery |
|
|
92
|
+
| `get_flow_config_tracks(flow_config_id)` | Tracks for a specific mood/genre flow config |
|
|
93
|
+
| `get_made_for_me(first)` | "Made For You" SmartTracklist & Flow items |
|
|
94
|
+
| `get_smart_tracklist(smart_tracklist_id, first)` | Smart tracklist with paginated tracks |
|
|
95
|
+
| `get_charts(country_code, ...)` | Country charts — tracks, albums, artists, playlists |
|
|
96
|
+
| `get_recommendations(playlists_first, ...)` | Personalized recommendations across categories |
|
|
97
|
+
| `get_recently_played(first)` | Recently played mixed content (albums, playlists, artists...) |
|
|
98
|
+
| `get_favorite_artists(first, after)` | Paginated favorite artists |
|
|
99
|
+
| `get_favorite_albums(first, after)` | Paginated favorite albums |
|
|
100
|
+
| `get_favorite_tracks(first, after)` | Paginated favorite tracks |
|
|
101
|
+
| `get_favorite_playlists(first, after)` | Paginated favorite playlists |
|
|
90
102
|
|
|
91
103
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
92
104
|
|
|
@@ -51,14 +51,26 @@ asyncio.run(main())
|
|
|
51
51
|
|
|
52
52
|
## Available Queries
|
|
53
53
|
|
|
54
|
-
| Method
|
|
55
|
-
|
|
|
56
|
-
| `get_me()`
|
|
57
|
-
| `get_track(track_id)`
|
|
58
|
-
| `get_album(album_id)`
|
|
59
|
-
| `get_artist(artist_id)`
|
|
60
|
-
| `get_playlist(playlist_id)`
|
|
61
|
-
| `search(query, ...)`
|
|
54
|
+
| Method | Description |
|
|
55
|
+
| ------------------------------------------------ | ------------------------------------------------------------- |
|
|
56
|
+
| `get_me()` | Current authenticated user |
|
|
57
|
+
| `get_track(track_id)` | Full track details — ISRC, media tokens, lyrics, contributors |
|
|
58
|
+
| `get_album(album_id)` | Album with cover, label, paginated tracks, fallback |
|
|
59
|
+
| `get_artist(artist_id)` | Artist with bio, top tracks, albums (ordered by release date) |
|
|
60
|
+
| `get_playlist(playlist_id)` | Playlist with owner, picture, paginated tracks |
|
|
61
|
+
| `search(query, ...)` | Unified search across tracks, albums, artists, playlists |
|
|
62
|
+
| `get_flow()` | User's default Flow with tracks |
|
|
63
|
+
| `get_flow_configs(moods_first, genres_first)` | Mood & genre flow config lists for discovery |
|
|
64
|
+
| `get_flow_config_tracks(flow_config_id)` | Tracks for a specific mood/genre flow config |
|
|
65
|
+
| `get_made_for_me(first)` | "Made For You" SmartTracklist & Flow items |
|
|
66
|
+
| `get_smart_tracklist(smart_tracklist_id, first)` | Smart tracklist with paginated tracks |
|
|
67
|
+
| `get_charts(country_code, ...)` | Country charts — tracks, albums, artists, playlists |
|
|
68
|
+
| `get_recommendations(playlists_first, ...)` | Personalized recommendations across categories |
|
|
69
|
+
| `get_recently_played(first)` | Recently played mixed content (albums, playlists, artists...) |
|
|
70
|
+
| `get_favorite_artists(first, after)` | Paginated favorite artists |
|
|
71
|
+
| `get_favorite_albums(first, after)` | Paginated favorite albums |
|
|
72
|
+
| `get_favorite_tracks(first, after)` | Paginated favorite tracks |
|
|
73
|
+
| `get_favorite_playlists(first, after)` | Paginated favorite playlists |
|
|
62
74
|
|
|
63
75
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
64
76
|
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import time
|
|
7
8
|
from base64 import urlsafe_b64decode
|
|
8
|
-
from typing import Any, cast
|
|
9
|
+
from typing import Any, ClassVar, cast
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class GraphQLClientError(Exception):
|
|
14
17
|
"""Base exception for GraphQL client errors."""
|
|
@@ -110,6 +113,7 @@ class DeezerBaseClient:
|
|
|
110
113
|
:param variables: Optional query variables.
|
|
111
114
|
:param kwargs: Additional keyword arguments passed to httpx.
|
|
112
115
|
"""
|
|
116
|
+
logger.debug("GQL execute: %s (variables=%s)", operation_name or "<unnamed>", variables)
|
|
113
117
|
jwt = await self._ensure_jwt()
|
|
114
118
|
|
|
115
119
|
headers: dict[str, str] = kwargs.pop("headers", None) or {}
|
|
@@ -120,16 +124,29 @@ class DeezerBaseClient:
|
|
|
120
124
|
if operation_name:
|
|
121
125
|
payload["operationName"] = operation_name
|
|
122
126
|
if variables:
|
|
123
|
-
|
|
127
|
+
# Filter out UNSET sentinel values that are not JSON-serializable.
|
|
128
|
+
# Inline import avoids depending on generated code at module level.
|
|
129
|
+
from deezer_python_gql.generated.base_model import UnsetType # noqa: PLC0415
|
|
130
|
+
|
|
131
|
+
payload["variables"] = {
|
|
132
|
+
k: v for k, v in variables.items() if not isinstance(v, UnsetType)
|
|
133
|
+
}
|
|
124
134
|
|
|
125
135
|
client = self._http_client or httpx.AsyncClient()
|
|
126
136
|
try:
|
|
127
|
-
|
|
137
|
+
resp = await client.post(
|
|
128
138
|
self.url,
|
|
129
139
|
json=payload,
|
|
130
140
|
headers=headers,
|
|
131
141
|
**kwargs,
|
|
132
142
|
)
|
|
143
|
+
logger.debug(
|
|
144
|
+
"GQL response: %s status=%s length=%s",
|
|
145
|
+
operation_name or "<unnamed>",
|
|
146
|
+
resp.status_code,
|
|
147
|
+
len(resp.content),
|
|
148
|
+
)
|
|
149
|
+
return resp
|
|
133
150
|
finally:
|
|
134
151
|
if not self._http_client:
|
|
135
152
|
await client.aclose()
|
|
@@ -159,10 +176,52 @@ class DeezerBaseClient:
|
|
|
159
176
|
errors = response_json.get("errors")
|
|
160
177
|
|
|
161
178
|
if errors:
|
|
162
|
-
|
|
179
|
+
if data:
|
|
180
|
+
# Partial success — some items failed (e.g. deleted albums in favorites).
|
|
181
|
+
# Log the errors but return the valid data.
|
|
182
|
+
logger.warning(
|
|
183
|
+
"GraphQL response contained %d error(s): %s",
|
|
184
|
+
len(errors),
|
|
185
|
+
[e.get("message", "Unknown error") for e in errors],
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
raise GraphQLClientGraphQLMultiError.from_errors_dicts(
|
|
189
|
+
errors_dicts=errors, data=data
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# The Deezer API omits __typename for single-member union types
|
|
193
|
+
# (e.g. Contributor = Artist). Pydantic discriminated unions require it,
|
|
194
|
+
# so we inject the missing field before model validation.
|
|
195
|
+
self._inject_missing_typenames(data)
|
|
163
196
|
|
|
164
197
|
return cast("dict[str, Any]", data)
|
|
165
198
|
|
|
199
|
+
# Map of parent key → child key → __typename value for single-member unions
|
|
200
|
+
# where the Deezer API omits the discriminator.
|
|
201
|
+
_TYPENAME_PATCHES: ClassVar[dict[str, dict[str, str]]] = {
|
|
202
|
+
"contributors": {"node": "Artist"},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def _inject_missing_typenames(cls, obj: Any) -> None:
|
|
207
|
+
"""Recursively inject __typename into nodes where the API omits it."""
|
|
208
|
+
if isinstance(obj, dict):
|
|
209
|
+
for parent_key, patches in cls._TYPENAME_PATCHES.items():
|
|
210
|
+
if parent_key in obj:
|
|
211
|
+
container = obj[parent_key]
|
|
212
|
+
if isinstance(container, dict) and "edges" in container:
|
|
213
|
+
for edge in container["edges"]:
|
|
214
|
+
if isinstance(edge, dict):
|
|
215
|
+
for child_key, typename in patches.items():
|
|
216
|
+
node = edge.get(child_key)
|
|
217
|
+
if isinstance(node, dict) and "__typename" not in node:
|
|
218
|
+
node["__typename"] = typename
|
|
219
|
+
for value in obj.values():
|
|
220
|
+
cls._inject_missing_typenames(value)
|
|
221
|
+
elif isinstance(obj, list):
|
|
222
|
+
for item in obj:
|
|
223
|
+
cls._inject_missing_typenames(item)
|
|
224
|
+
|
|
166
225
|
async def _ensure_jwt(self) -> str:
|
|
167
226
|
"""Acquire or refresh the JWT token from ARL cookie.
|
|
168
227
|
|
|
@@ -174,6 +233,7 @@ class DeezerBaseClient:
|
|
|
174
233
|
if self._jwt and now < (self._jwt_expires_at - self.JWT_REFRESH_MARGIN_SECONDS):
|
|
175
234
|
return self._jwt
|
|
176
235
|
|
|
236
|
+
logger.debug("JWT expired or missing, refreshing from ARL")
|
|
177
237
|
params = {"jo": "p", "rto": "c", "i": "c"}
|
|
178
238
|
|
|
179
239
|
async with httpx.AsyncClient() as http:
|
|
@@ -194,5 +254,6 @@ class DeezerBaseClient:
|
|
|
194
254
|
padded = payload_segment + "=" * (-len(payload_segment) % 4)
|
|
195
255
|
payload = json.loads(urlsafe_b64decode(padded))
|
|
196
256
|
self._jwt_expires_at = float(payload["exp"])
|
|
257
|
+
logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
|
|
197
258
|
|
|
198
259
|
return self._jwt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Async typed Python client for Deezer's Pipe GraphQL API.
|
|
5
5
|
Author-email: Julian Daberkow <jdaberkow@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,14 +14,14 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: httpx>=0.27.0
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
16
|
Provides-Extra: test
|
|
17
|
-
Requires-Dist: codespell==2.4.
|
|
17
|
+
Requires-Dist: codespell==2.4.2; extra == "test"
|
|
18
18
|
Requires-Dist: mypy==1.19.1; extra == "test"
|
|
19
19
|
Requires-Dist: pre-commit==4.5.1; extra == "test"
|
|
20
20
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
21
21
|
Requires-Dist: pytest==9.0.2; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "test"
|
|
23
|
-
Requires-Dist: pytest-cov==7.
|
|
24
|
-
Requires-Dist: ruff==0.15.
|
|
23
|
+
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
24
|
+
Requires-Dist: ruff==0.15.7; extra == "test"
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
27
27
|
Dynamic: license-file
|
|
@@ -79,14 +79,26 @@ asyncio.run(main())
|
|
|
79
79
|
|
|
80
80
|
## Available Queries
|
|
81
81
|
|
|
82
|
-
| Method
|
|
83
|
-
|
|
|
84
|
-
| `get_me()`
|
|
85
|
-
| `get_track(track_id)`
|
|
86
|
-
| `get_album(album_id)`
|
|
87
|
-
| `get_artist(artist_id)`
|
|
88
|
-
| `get_playlist(playlist_id)`
|
|
89
|
-
| `search(query, ...)`
|
|
82
|
+
| Method | Description |
|
|
83
|
+
| ------------------------------------------------ | ------------------------------------------------------------- |
|
|
84
|
+
| `get_me()` | Current authenticated user |
|
|
85
|
+
| `get_track(track_id)` | Full track details — ISRC, media tokens, lyrics, contributors |
|
|
86
|
+
| `get_album(album_id)` | Album with cover, label, paginated tracks, fallback |
|
|
87
|
+
| `get_artist(artist_id)` | Artist with bio, top tracks, albums (ordered by release date) |
|
|
88
|
+
| `get_playlist(playlist_id)` | Playlist with owner, picture, paginated tracks |
|
|
89
|
+
| `search(query, ...)` | Unified search across tracks, albums, artists, playlists |
|
|
90
|
+
| `get_flow()` | User's default Flow with tracks |
|
|
91
|
+
| `get_flow_configs(moods_first, genres_first)` | Mood & genre flow config lists for discovery |
|
|
92
|
+
| `get_flow_config_tracks(flow_config_id)` | Tracks for a specific mood/genre flow config |
|
|
93
|
+
| `get_made_for_me(first)` | "Made For You" SmartTracklist & Flow items |
|
|
94
|
+
| `get_smart_tracklist(smart_tracklist_id, first)` | Smart tracklist with paginated tracks |
|
|
95
|
+
| `get_charts(country_code, ...)` | Country charts — tracks, albums, artists, playlists |
|
|
96
|
+
| `get_recommendations(playlists_first, ...)` | Personalized recommendations across categories |
|
|
97
|
+
| `get_recently_played(first)` | Recently played mixed content (albums, playlists, artists...) |
|
|
98
|
+
| `get_favorite_artists(first, after)` | Paginated favorite artists |
|
|
99
|
+
| `get_favorite_albums(first, after)` | Paginated favorite albums |
|
|
100
|
+
| `get_favorite_tracks(first, after)` | Paginated favorite tracks |
|
|
101
|
+
| `get_favorite_playlists(first, after)` | Paginated favorite playlists |
|
|
90
102
|
|
|
91
103
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
92
104
|
|
|
@@ -5,11 +5,11 @@ pydantic>=2.0.0
|
|
|
5
5
|
ariadne-codegen[subscriptions]
|
|
6
6
|
|
|
7
7
|
[test]
|
|
8
|
-
codespell==2.4.
|
|
8
|
+
codespell==2.4.2
|
|
9
9
|
mypy==1.19.1
|
|
10
10
|
pre-commit==4.5.1
|
|
11
11
|
pre-commit-hooks==6.0.0
|
|
12
12
|
pytest==9.0.2
|
|
13
13
|
pytest-asyncio==1.3.0
|
|
14
|
-
pytest-cov==7.
|
|
15
|
-
ruff==0.15.
|
|
14
|
+
pytest-cov==7.1.0
|
|
15
|
+
ruff==0.15.7
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deezer-python-gql"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Async typed Python client for Deezer's Pipe GraphQL API."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -22,14 +22,14 @@ text = "Apache-2.0"
|
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
24
|
test = [
|
|
25
|
-
"codespell==2.4.
|
|
25
|
+
"codespell==2.4.2",
|
|
26
26
|
"mypy==1.19.1",
|
|
27
27
|
"pre-commit==4.5.1",
|
|
28
28
|
"pre-commit-hooks==6.0.0",
|
|
29
29
|
"pytest==9.0.2",
|
|
30
30
|
"pytest-asyncio==1.3.0",
|
|
31
|
-
"pytest-cov==7.
|
|
32
|
-
"ruff==0.15.
|
|
31
|
+
"pytest-cov==7.1.0",
|
|
32
|
+
"ruff==0.15.7",
|
|
33
33
|
]
|
|
34
34
|
dev = [
|
|
35
35
|
"ariadne-codegen[subscriptions]",
|
|
@@ -28,10 +28,24 @@ from deezer_python_gql.base_client import (
|
|
|
28
28
|
)
|
|
29
29
|
from deezer_python_gql.generated.get_album import GetAlbum
|
|
30
30
|
from deezer_python_gql.generated.get_artist import GetArtist
|
|
31
|
+
from deezer_python_gql.generated.get_charts import GetCharts
|
|
32
|
+
from deezer_python_gql.generated.get_favorite_albums import GetFavoriteAlbums
|
|
33
|
+
from deezer_python_gql.generated.get_favorite_artists import GetFavoriteArtists
|
|
34
|
+
from deezer_python_gql.generated.get_favorite_playlists import GetFavoritePlaylists
|
|
35
|
+
from deezer_python_gql.generated.get_favorite_tracks import GetFavoriteTracks
|
|
36
|
+
from deezer_python_gql.generated.get_flow import GetFlow
|
|
37
|
+
from deezer_python_gql.generated.get_flow_config_tracks import GetFlowConfigTracks
|
|
38
|
+
from deezer_python_gql.generated.get_flow_configs import GetFlowConfigs
|
|
39
|
+
from deezer_python_gql.generated.get_made_for_me import GetMadeForMe
|
|
31
40
|
from deezer_python_gql.generated.get_me import GetMe
|
|
32
41
|
from deezer_python_gql.generated.get_playlist import GetPlaylist
|
|
42
|
+
from deezer_python_gql.generated.get_recently_played import GetRecentlyPlayed
|
|
43
|
+
from deezer_python_gql.generated.get_recommendations import GetRecommendations
|
|
44
|
+
from deezer_python_gql.generated.get_smart_tracklist import GetSmartTracklist
|
|
33
45
|
from deezer_python_gql.generated.get_track import GetTrack
|
|
46
|
+
from deezer_python_gql.generated.get_user_charts import GetUserCharts
|
|
34
47
|
from deezer_python_gql.generated.search import Search
|
|
48
|
+
from deezer_python_gql.generated.search_flows import SearchFlows
|
|
35
49
|
|
|
36
50
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
37
51
|
|
|
@@ -96,7 +110,28 @@ def test_client_instantiation() -> None:
|
|
|
96
110
|
def test_client_has_generated_methods() -> None:
|
|
97
111
|
"""Verify that codegen produced all expected query methods."""
|
|
98
112
|
client = DeezerGQLClient(arl="test")
|
|
99
|
-
expected_methods = [
|
|
113
|
+
expected_methods = [
|
|
114
|
+
"get_me",
|
|
115
|
+
"get_track",
|
|
116
|
+
"get_album",
|
|
117
|
+
"get_artist",
|
|
118
|
+
"get_playlist",
|
|
119
|
+
"search",
|
|
120
|
+
"get_flow",
|
|
121
|
+
"get_flow_configs",
|
|
122
|
+
"get_flow_config_tracks",
|
|
123
|
+
"get_made_for_me",
|
|
124
|
+
"get_smart_tracklist",
|
|
125
|
+
"get_charts",
|
|
126
|
+
"get_recommendations",
|
|
127
|
+
"get_recently_played",
|
|
128
|
+
"get_favorite_artists",
|
|
129
|
+
"get_favorite_albums",
|
|
130
|
+
"get_favorite_tracks",
|
|
131
|
+
"get_favorite_playlists",
|
|
132
|
+
"search_flows",
|
|
133
|
+
"get_user_charts",
|
|
134
|
+
]
|
|
100
135
|
for method in expected_methods:
|
|
101
136
|
assert hasattr(client, method), f"Missing method: {method}"
|
|
102
137
|
assert callable(getattr(client, method))
|
|
@@ -404,3 +439,231 @@ def test_smoke_search() -> None:
|
|
|
404
439
|
assert len(results.artists.edges) > 0
|
|
405
440
|
assert len(results.playlists.edges) > 0
|
|
406
441
|
assert isinstance(results.tracks.page_info.has_next_page, bool)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
# 5. Browse-related model smoke tests (new queries)
|
|
446
|
+
# ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def test_smoke_get_flow() -> None:
|
|
450
|
+
"""Verify GetFlow fixture parses with flow tracks."""
|
|
451
|
+
data = _load_fixture("get_flow.json")
|
|
452
|
+
me = GetFlow.model_validate(data).me
|
|
453
|
+
assert me is not None
|
|
454
|
+
assert me.flow is not None
|
|
455
|
+
assert me.flow.id == "flow:default"
|
|
456
|
+
assert me.flow.title == "Flow"
|
|
457
|
+
assert len(me.flow.tracks) == 2
|
|
458
|
+
assert me.flow.tracks[0].track is not None
|
|
459
|
+
assert me.flow.tracks[0].track.title == "Harder, Better, Faster, Stronger"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_smoke_get_flow_configs() -> None:
|
|
463
|
+
"""Verify GetFlowConfigs fixture parses mood and genre flow configs."""
|
|
464
|
+
data = _load_fixture("get_flow_configs.json")
|
|
465
|
+
me = GetFlowConfigs.model_validate(data).me
|
|
466
|
+
assert me is not None
|
|
467
|
+
configs = me.flow_configs
|
|
468
|
+
assert len(configs.moods.edges) == 3
|
|
469
|
+
assert len(configs.genres.edges) == 3
|
|
470
|
+
mood_node = configs.moods.edges[0].node
|
|
471
|
+
assert mood_node is not None
|
|
472
|
+
assert mood_node.title == "Chill"
|
|
473
|
+
genre_node = configs.genres.edges[0].node
|
|
474
|
+
assert genre_node is not None
|
|
475
|
+
assert genre_node.title == "Rock"
|
|
476
|
+
assert configs.moods.page_info.has_next_page is True
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_smoke_get_flow_config_tracks() -> None:
|
|
480
|
+
"""Verify GetFlowConfigTracks fixture parses tracks for a flow config."""
|
|
481
|
+
data = _load_fixture("get_flow_config_tracks.json")
|
|
482
|
+
flow_config = GetFlowConfigTracks.model_validate(data).flow_config
|
|
483
|
+
assert flow_config is not None
|
|
484
|
+
assert flow_config.id == "flow_config:chill"
|
|
485
|
+
assert flow_config.title == "Chill"
|
|
486
|
+
assert len(flow_config.tracks) == 1
|
|
487
|
+
assert flow_config.tracks[0].track is not None
|
|
488
|
+
assert flow_config.tracks[0].track.title == "Around the World"
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def test_smoke_get_made_for_me() -> None:
|
|
492
|
+
"""Verify GetMadeForMe fixture parses SmartTracklist and Flow items."""
|
|
493
|
+
data = _load_fixture("get_made_for_me.json")
|
|
494
|
+
me = GetMadeForMe.model_validate(data).me
|
|
495
|
+
assert me is not None
|
|
496
|
+
edges = me.made_for_me.edges
|
|
497
|
+
assert len(edges) == 3
|
|
498
|
+
# First two are SmartTracklists, third is a Flow
|
|
499
|
+
node_0 = edges[0].node
|
|
500
|
+
assert node_0 is not None
|
|
501
|
+
assert node_0.typename__ == "SmartTracklist"
|
|
502
|
+
node_2 = edges[2].node
|
|
503
|
+
assert node_2 is not None
|
|
504
|
+
assert node_2.typename__ == "Flow"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_smoke_get_smart_tracklist() -> None:
|
|
508
|
+
"""Verify GetSmartTracklist fixture parses with paginated tracks."""
|
|
509
|
+
data = _load_fixture("get_smart_tracklist.json")
|
|
510
|
+
st = GetSmartTracklist.model_validate(data).smart_tracklist
|
|
511
|
+
assert st is not None
|
|
512
|
+
assert st.id == "smart:daily_mix_1"
|
|
513
|
+
assert st.title == "Your Daily Mix 1"
|
|
514
|
+
assert len(st.tracks.edges) == 2
|
|
515
|
+
track_node = st.tracks.edges[0].node
|
|
516
|
+
assert track_node is not None
|
|
517
|
+
assert track_node.title == "Harder, Better, Faster, Stronger"
|
|
518
|
+
assert st.tracks.page_info.has_next_page is True
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_smoke_get_charts() -> None:
|
|
522
|
+
"""Verify GetCharts fixture parses all chart categories."""
|
|
523
|
+
data = _load_fixture("get_charts.json")
|
|
524
|
+
charts = GetCharts.model_validate(data).charts
|
|
525
|
+
assert charts is not None
|
|
526
|
+
country = charts.country
|
|
527
|
+
assert country is not None
|
|
528
|
+
assert country.tracks is not None
|
|
529
|
+
assert country.albums is not None
|
|
530
|
+
assert country.artists is not None
|
|
531
|
+
assert country.playlists is not None
|
|
532
|
+
assert len(country.tracks.edges) > 0
|
|
533
|
+
assert len(country.albums.edges) > 0
|
|
534
|
+
assert len(country.artists.edges) > 0
|
|
535
|
+
assert len(country.playlists.edges) > 0
|
|
536
|
+
first_track = country.tracks.edges[0].node
|
|
537
|
+
assert first_track is not None
|
|
538
|
+
assert first_track.title == "Greedy"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_smoke_get_recommendations() -> None:
|
|
542
|
+
"""Verify GetRecommendations fixture parses all recommendation categories."""
|
|
543
|
+
data = _load_fixture("get_recommendations.json")
|
|
544
|
+
me = GetRecommendations.model_validate(data).me
|
|
545
|
+
assert me is not None
|
|
546
|
+
reco = me.recommendations
|
|
547
|
+
assert len(reco.playlists.edges) > 0
|
|
548
|
+
assert len(reco.artist_playlists.edges) == 2
|
|
549
|
+
assert reco.artist_playlists.edges[0].node is not None
|
|
550
|
+
assert reco.artist_playlists.edges[0].node.title == "This Is Daft Punk"
|
|
551
|
+
assert len(reco.new_releases.edges) > 0
|
|
552
|
+
assert len(reco.artists.edges) > 0
|
|
553
|
+
assert reco.hot_tracks is not None
|
|
554
|
+
assert len(reco.hot_tracks) > 0
|
|
555
|
+
assert reco.hot_tracks[0].title == "Harder, Better, Faster, Stronger"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_smoke_get_recently_played() -> None:
|
|
559
|
+
"""Verify GetRecentlyPlayed fixture parses mixed content types."""
|
|
560
|
+
data = _load_fixture("get_recently_played.json")
|
|
561
|
+
me = GetRecentlyPlayed.model_validate(data).me
|
|
562
|
+
assert me is not None
|
|
563
|
+
edges = me.recently_played.edges
|
|
564
|
+
assert len(edges) == 4
|
|
565
|
+
# Check discriminated union types
|
|
566
|
+
node_0 = edges[0].node
|
|
567
|
+
assert node_0 is not None
|
|
568
|
+
assert node_0.typename__ == "Album"
|
|
569
|
+
node_1 = edges[1].node
|
|
570
|
+
assert node_1 is not None
|
|
571
|
+
assert node_1.typename__ == "Playlist"
|
|
572
|
+
node_2 = edges[2].node
|
|
573
|
+
assert node_2 is not None
|
|
574
|
+
assert node_2.typename__ == "Artist"
|
|
575
|
+
node_3 = edges[3].node
|
|
576
|
+
assert node_3 is not None
|
|
577
|
+
assert node_3.typename__ == "Flow"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_smoke_get_favorite_artists() -> None:
|
|
581
|
+
"""Verify GetFavoriteArtists fixture parses with pagination."""
|
|
582
|
+
data = _load_fixture("get_favorite_artists.json")
|
|
583
|
+
me = GetFavoriteArtists.model_validate(data).me
|
|
584
|
+
assert me is not None
|
|
585
|
+
artists = me.user_favorites.artists
|
|
586
|
+
assert artists is not None
|
|
587
|
+
assert len(artists.edges) == 2
|
|
588
|
+
artist_node = artists.edges[0].node
|
|
589
|
+
assert artist_node is not None
|
|
590
|
+
assert artist_node.name == "Daft Punk"
|
|
591
|
+
assert artists.page_info.has_next_page is True
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_smoke_get_favorite_albums() -> None:
|
|
595
|
+
"""Verify GetFavoriteAlbums fixture parses with pagination."""
|
|
596
|
+
data = _load_fixture("get_favorite_albums.json")
|
|
597
|
+
me = GetFavoriteAlbums.model_validate(data).me
|
|
598
|
+
assert me is not None
|
|
599
|
+
albums = me.user_favorites.albums
|
|
600
|
+
assert albums is not None
|
|
601
|
+
assert len(albums.edges) == 1
|
|
602
|
+
album_node = albums.edges[0].node
|
|
603
|
+
assert album_node is not None
|
|
604
|
+
assert album_node.display_title == "Discovery"
|
|
605
|
+
assert albums.page_info.has_next_page is True
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def test_smoke_get_favorite_tracks() -> None:
|
|
609
|
+
"""Verify GetFavoriteTracks fixture parses with pagination."""
|
|
610
|
+
data = _load_fixture("get_favorite_tracks.json")
|
|
611
|
+
me = GetFavoriteTracks.model_validate(data).me
|
|
612
|
+
assert me is not None
|
|
613
|
+
tracks = me.user_favorites.tracks
|
|
614
|
+
assert tracks is not None
|
|
615
|
+
assert len(tracks.edges) == 1
|
|
616
|
+
track_node = tracks.edges[0].node
|
|
617
|
+
assert track_node is not None
|
|
618
|
+
assert track_node.title == "Harder, Better, Faster, Stronger"
|
|
619
|
+
assert tracks.page_info.has_next_page is True
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def test_smoke_get_favorite_playlists() -> None:
|
|
623
|
+
"""Verify GetFavoritePlaylists fixture parses with pagination."""
|
|
624
|
+
data = _load_fixture("get_favorite_playlists.json")
|
|
625
|
+
me = GetFavoritePlaylists.model_validate(data).me
|
|
626
|
+
assert me is not None
|
|
627
|
+
playlists = me.user_favorites.playlists
|
|
628
|
+
assert playlists is not None
|
|
629
|
+
assert len(playlists.edges) == 1
|
|
630
|
+
playlist_node = playlists.edges[0].node
|
|
631
|
+
assert playlist_node is not None
|
|
632
|
+
assert playlist_node.title == "Electronic Hits"
|
|
633
|
+
assert playlists.page_info.has_next_page is True
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def test_smoke_search_flows() -> None:
|
|
637
|
+
"""Verify SearchFlows fixture parses with flow config nodes."""
|
|
638
|
+
data = _load_fixture("search_flows.json")
|
|
639
|
+
search = SearchFlows.model_validate(data).search
|
|
640
|
+
assert search is not None
|
|
641
|
+
flow_configs = search.results.flow_configs
|
|
642
|
+
assert len(flow_configs.edges) == 5
|
|
643
|
+
first_node = flow_configs.edges[0].node
|
|
644
|
+
assert first_node is not None
|
|
645
|
+
assert first_node.id == "flow_config:chill"
|
|
646
|
+
assert first_node.title == "Chill"
|
|
647
|
+
assert first_node.visuals.hardware_square_icon is not None
|
|
648
|
+
assert len(first_node.visuals.hardware_square_icon.urls) == 1
|
|
649
|
+
assert flow_configs.page_info.has_next_page is True
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_smoke_get_user_charts() -> None:
|
|
653
|
+
"""Verify GetUserCharts fixture parses personal top tracks/artists/albums."""
|
|
654
|
+
data = _load_fixture("get_user_charts.json")
|
|
655
|
+
me = GetUserCharts.model_validate(data).me
|
|
656
|
+
assert me is not None
|
|
657
|
+
charts = me.charts
|
|
658
|
+
assert charts.tracks is not None
|
|
659
|
+
assert len(charts.tracks.edges) == 2
|
|
660
|
+
assert charts.tracks.edges[0].node is not None
|
|
661
|
+
assert charts.tracks.edges[0].node.title == "Harder, Better, Faster, Stronger"
|
|
662
|
+
assert charts.artists is not None
|
|
663
|
+
assert len(charts.artists.edges) == 2
|
|
664
|
+
assert charts.artists.edges[0].node is not None
|
|
665
|
+
assert charts.artists.edges[0].node.name == "Daft Punk"
|
|
666
|
+
assert charts.albums is not None
|
|
667
|
+
assert len(charts.albums.edges) == 1
|
|
668
|
+
assert charts.albums.edges[0].node is not None
|
|
669
|
+
assert charts.albums.edges[0].node.display_title == "Discovery"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.1.0 → deezer_python_gql-0.2.0}/deezer_python_gql.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|