deezer-python-gql 0.17.1__tar.gz → 0.17.2__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.17.1/deezer_python_gql.egg-info → deezer_python_gql-0.17.2}/PKG-INFO +12 -5
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/README.md +6 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/base_client.py +35 -3
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/base_client.py +35 -3
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2/deezer_python_gql.egg-info}/PKG-INFO +12 -5
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/requires.txt +5 -4
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/pyproject.toml +7 -5
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/tests/test_client.py +124 -9
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/LICENSE +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/MANIFEST.in +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/__init__.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/__init__.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_album_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_artist_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_audiobook_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_playlist_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_podcast_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_track_to_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/add_tracks_to_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/base_model.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/bookmark_podcast_episode.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/client.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/create_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/delete_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/enums.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/fragments.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_album.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_artist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_artist_mix.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_audiobook.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_audiobook_chapter.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_charts.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_albums.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_artists.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_audiobooks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_playlists.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_podcasts.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_favorite_tracks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow_batch.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow_config_tracks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow_configs.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_livestream.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_made_for_me.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_me.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_music_together_affinity.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_music_together_group.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_music_together_groups.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_personal_tracks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_podcast.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_podcast_episode.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_podcast_episode_bookmarks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_podcast_episodes_by_ids.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_recently_played.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_recommendations.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_similar_artists.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_similar_tracks.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_smart_tracklist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_track.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_track_mix.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_user_charts.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_user_playlists.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/input_types.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/mark_as_not_played_podcast_episode.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/mark_as_played_podcast_episode.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_create_group.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_generate_group_name.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_join_group.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_leave_group.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_refresh_suggested_tracklist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/music_together_update_group_settings.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_album_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_artist_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_audiobook_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_playlist_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_podcast_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_track_from_favorite.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/remove_tracks_from_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/search.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/search_flows.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/unbookmark_podcast_episode.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/update_playlist.py +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/py.typed +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/SOURCES.txt +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/not-zip-safe +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/top_level.txt +0 -0
- {deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.2
|
|
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
|
|
@@ -13,15 +13,16 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.13.5
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: yarl>=1.22.0
|
|
16
17
|
Provides-Extra: test
|
|
17
|
-
Requires-Dist: codespell==2.4.
|
|
18
|
-
Requires-Dist: mypy==2.1
|
|
19
|
-
Requires-Dist: pre-commit==4.6.
|
|
18
|
+
Requires-Dist: codespell==2.4.3; extra == "test"
|
|
19
|
+
Requires-Dist: mypy==2.3.1; extra == "test"
|
|
20
|
+
Requires-Dist: pre-commit==4.6.2; extra == "test"
|
|
20
21
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
21
22
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
22
23
|
Requires-Dist: pytest-asyncio==1.4.0; extra == "test"
|
|
23
24
|
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
24
|
-
Requires-Dist: ruff==0.
|
|
25
|
+
Requires-Dist: ruff==0.16.5; extra == "test"
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
27
28
|
Dynamic: license-file
|
|
@@ -47,6 +48,7 @@ import asyncio
|
|
|
47
48
|
|
|
48
49
|
from deezer_python_gql import DeezerGQLClient
|
|
49
50
|
|
|
51
|
+
|
|
50
52
|
async def main():
|
|
51
53
|
client = DeezerGQLClient(arl="YOUR_ARL_COOKIE")
|
|
52
54
|
|
|
@@ -74,6 +76,7 @@ async def main():
|
|
|
74
76
|
results = await client.search(query="Daft Punk")
|
|
75
77
|
print(len(results.tracks.edges), "tracks found")
|
|
76
78
|
|
|
79
|
+
|
|
77
80
|
asyncio.run(main())
|
|
78
81
|
```
|
|
79
82
|
|
|
@@ -252,6 +255,10 @@ The Pipe API uses short-lived JWTs obtained from an ARL cookie. The base client
|
|
|
252
255
|
handles token acquisition and refresh automatically — you only need to supply a
|
|
253
256
|
valid ARL value.
|
|
254
257
|
|
|
258
|
+
An external `aiohttp.ClientSession` may be shared by clients for different Deezer
|
|
259
|
+
accounts. Each client keeps Deezer's response cookies locally and masks cookies from
|
|
260
|
+
the shared session jar, so authentication state cannot cross between those accounts.
|
|
261
|
+
|
|
255
262
|
If the ARL is rejected (bad or expired), the client raises
|
|
256
263
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
257
264
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
|
@@ -19,6 +19,7 @@ import asyncio
|
|
|
19
19
|
|
|
20
20
|
from deezer_python_gql import DeezerGQLClient
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
async def main():
|
|
23
24
|
client = DeezerGQLClient(arl="YOUR_ARL_COOKIE")
|
|
24
25
|
|
|
@@ -46,6 +47,7 @@ async def main():
|
|
|
46
47
|
results = await client.search(query="Daft Punk")
|
|
47
48
|
print(len(results.tracks.edges), "tracks found")
|
|
48
49
|
|
|
50
|
+
|
|
49
51
|
asyncio.run(main())
|
|
50
52
|
```
|
|
51
53
|
|
|
@@ -224,6 +226,10 @@ The Pipe API uses short-lived JWTs obtained from an ARL cookie. The base client
|
|
|
224
226
|
handles token acquisition and refresh automatically — you only need to supply a
|
|
225
227
|
valid ARL value.
|
|
226
228
|
|
|
229
|
+
An external `aiohttp.ClientSession` may be shared by clients for different Deezer
|
|
230
|
+
accounts. Each client keeps Deezer's response cookies locally and masks cookies from
|
|
231
|
+
the shared session jar, so authentication state cannot cross between those accounts.
|
|
232
|
+
|
|
227
233
|
If the ARL is rejected (bad or expired), the client raises
|
|
228
234
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
229
235
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
|
@@ -8,9 +8,13 @@ import logging
|
|
|
8
8
|
import time
|
|
9
9
|
from base64 import urlsafe_b64decode
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from typing import Any, ClassVar, Self, cast
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
|
|
12
12
|
|
|
13
13
|
from aiohttp import ClientSession, ClientTimeout
|
|
14
|
+
from yarl import URL
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from aiohttp import ClientResponse
|
|
14
18
|
|
|
15
19
|
logger = logging.getLogger(__name__)
|
|
16
20
|
|
|
@@ -137,6 +141,9 @@ class DeezerBaseClient:
|
|
|
137
141
|
self._arl = arl
|
|
138
142
|
self._session = session
|
|
139
143
|
self._owns_session = session is None
|
|
144
|
+
# An external session may be shared by clients for different Deezer accounts.
|
|
145
|
+
# Keep Deezer's response cookies per client instead of trusting that shared jar.
|
|
146
|
+
self._cookies: dict[str, str] = {}
|
|
140
147
|
self._jwt: str | None = None
|
|
141
148
|
self._jwt_expires_at: float = 0
|
|
142
149
|
self._jwt_lock = asyncio.Lock()
|
|
@@ -167,6 +174,21 @@ class DeezerBaseClient:
|
|
|
167
174
|
"""Exit the async context manager, closing internal resources."""
|
|
168
175
|
await self.close()
|
|
169
176
|
|
|
177
|
+
def _request_cookies(self, url: str, *, include_arl: bool = False) -> dict[str, str]:
|
|
178
|
+
"""Return account-local cookies while neutralizing a shared session's jar."""
|
|
179
|
+
session = self._get_session()
|
|
180
|
+
blanked = dict.fromkeys(session.cookie_jar.filter_cookies(URL(url)), "")
|
|
181
|
+
cookies = blanked | self._cookies
|
|
182
|
+
if include_arl:
|
|
183
|
+
cookies["arl"] = self._arl
|
|
184
|
+
return cookies
|
|
185
|
+
|
|
186
|
+
def _store_cookies(self, response: ClientResponse) -> None:
|
|
187
|
+
"""Remember response cookies for this client instead of relying on the session jar."""
|
|
188
|
+
self._cookies.update(
|
|
189
|
+
{name: morsel.value for name, morsel in response.cookies.items() if name != "arl"}
|
|
190
|
+
)
|
|
191
|
+
|
|
170
192
|
async def execute(
|
|
171
193
|
self,
|
|
172
194
|
query: str,
|
|
@@ -189,6 +211,8 @@ class DeezerBaseClient:
|
|
|
189
211
|
headers["Authorization"] = f"Bearer {jwt}"
|
|
190
212
|
headers["Content-Type"] = "application/json"
|
|
191
213
|
kwargs.setdefault("timeout", ClientTimeout(total=self.REQUEST_TIMEOUT_SECONDS))
|
|
214
|
+
request_cookies = self._request_cookies(self.url)
|
|
215
|
+
request_cookies.update(kwargs.pop("cookies", None) or {})
|
|
192
216
|
|
|
193
217
|
payload: dict[str, Any] = {"query": query}
|
|
194
218
|
if operation_name:
|
|
@@ -203,7 +227,14 @@ class DeezerBaseClient:
|
|
|
203
227
|
}
|
|
204
228
|
|
|
205
229
|
session = self._get_session()
|
|
206
|
-
async with session.post(
|
|
230
|
+
async with session.post(
|
|
231
|
+
self.url,
|
|
232
|
+
json=payload,
|
|
233
|
+
headers=headers,
|
|
234
|
+
cookies=request_cookies,
|
|
235
|
+
**kwargs,
|
|
236
|
+
) as resp:
|
|
237
|
+
self._store_cookies(resp)
|
|
207
238
|
body = await resp.read()
|
|
208
239
|
gql_response = GQLResponse(
|
|
209
240
|
status=resp.status,
|
|
@@ -321,9 +352,10 @@ class DeezerBaseClient:
|
|
|
321
352
|
async with session.post(
|
|
322
353
|
self.AUTH_URL,
|
|
323
354
|
params=params,
|
|
324
|
-
cookies=
|
|
355
|
+
cookies=self._request_cookies(self.AUTH_URL, include_arl=True),
|
|
325
356
|
timeout=ClientTimeout(total=10),
|
|
326
357
|
) as resp:
|
|
358
|
+
self._store_cookies(resp)
|
|
327
359
|
status = resp.status
|
|
328
360
|
is_success = resp.ok
|
|
329
361
|
# Response body is text/plain containing JSON
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/base_client.py
RENAMED
|
@@ -10,9 +10,13 @@ import logging
|
|
|
10
10
|
import time
|
|
11
11
|
from base64 import urlsafe_b64decode
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from typing import Any, ClassVar, Self, cast
|
|
13
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
|
|
14
14
|
|
|
15
15
|
from aiohttp import ClientSession, ClientTimeout
|
|
16
|
+
from yarl import URL
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from aiohttp import ClientResponse
|
|
16
20
|
|
|
17
21
|
logger = logging.getLogger(__name__)
|
|
18
22
|
|
|
@@ -139,6 +143,9 @@ class DeezerBaseClient:
|
|
|
139
143
|
self._arl = arl
|
|
140
144
|
self._session = session
|
|
141
145
|
self._owns_session = session is None
|
|
146
|
+
# An external session may be shared by clients for different Deezer accounts.
|
|
147
|
+
# Keep Deezer's response cookies per client instead of trusting that shared jar.
|
|
148
|
+
self._cookies: dict[str, str] = {}
|
|
142
149
|
self._jwt: str | None = None
|
|
143
150
|
self._jwt_expires_at: float = 0
|
|
144
151
|
self._jwt_lock = asyncio.Lock()
|
|
@@ -169,6 +176,21 @@ class DeezerBaseClient:
|
|
|
169
176
|
"""Exit the async context manager, closing internal resources."""
|
|
170
177
|
await self.close()
|
|
171
178
|
|
|
179
|
+
def _request_cookies(self, url: str, *, include_arl: bool = False) -> dict[str, str]:
|
|
180
|
+
"""Return account-local cookies while neutralizing a shared session's jar."""
|
|
181
|
+
session = self._get_session()
|
|
182
|
+
blanked = dict.fromkeys(session.cookie_jar.filter_cookies(URL(url)), "")
|
|
183
|
+
cookies = blanked | self._cookies
|
|
184
|
+
if include_arl:
|
|
185
|
+
cookies["arl"] = self._arl
|
|
186
|
+
return cookies
|
|
187
|
+
|
|
188
|
+
def _store_cookies(self, response: ClientResponse) -> None:
|
|
189
|
+
"""Remember response cookies for this client instead of relying on the session jar."""
|
|
190
|
+
self._cookies.update(
|
|
191
|
+
{name: morsel.value for name, morsel in response.cookies.items() if name != "arl"}
|
|
192
|
+
)
|
|
193
|
+
|
|
172
194
|
async def execute(
|
|
173
195
|
self,
|
|
174
196
|
query: str,
|
|
@@ -191,6 +213,8 @@ class DeezerBaseClient:
|
|
|
191
213
|
headers["Authorization"] = f"Bearer {jwt}"
|
|
192
214
|
headers["Content-Type"] = "application/json"
|
|
193
215
|
kwargs.setdefault("timeout", ClientTimeout(total=self.REQUEST_TIMEOUT_SECONDS))
|
|
216
|
+
request_cookies = self._request_cookies(self.url)
|
|
217
|
+
request_cookies.update(kwargs.pop("cookies", None) or {})
|
|
194
218
|
|
|
195
219
|
payload: dict[str, Any] = {"query": query}
|
|
196
220
|
if operation_name:
|
|
@@ -205,7 +229,14 @@ class DeezerBaseClient:
|
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
session = self._get_session()
|
|
208
|
-
async with session.post(
|
|
232
|
+
async with session.post(
|
|
233
|
+
self.url,
|
|
234
|
+
json=payload,
|
|
235
|
+
headers=headers,
|
|
236
|
+
cookies=request_cookies,
|
|
237
|
+
**kwargs,
|
|
238
|
+
) as resp:
|
|
239
|
+
self._store_cookies(resp)
|
|
209
240
|
body = await resp.read()
|
|
210
241
|
gql_response = GQLResponse(
|
|
211
242
|
status=resp.status,
|
|
@@ -323,9 +354,10 @@ class DeezerBaseClient:
|
|
|
323
354
|
async with session.post(
|
|
324
355
|
self.AUTH_URL,
|
|
325
356
|
params=params,
|
|
326
|
-
cookies=
|
|
357
|
+
cookies=self._request_cookies(self.AUTH_URL, include_arl=True),
|
|
327
358
|
timeout=ClientTimeout(total=10),
|
|
328
359
|
) as resp:
|
|
360
|
+
self._store_cookies(resp)
|
|
329
361
|
status = resp.status
|
|
330
362
|
is_success = resp.ok
|
|
331
363
|
# Response body is text/plain containing JSON
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.2
|
|
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
|
|
@@ -13,15 +13,16 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.13.5
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: yarl>=1.22.0
|
|
16
17
|
Provides-Extra: test
|
|
17
|
-
Requires-Dist: codespell==2.4.
|
|
18
|
-
Requires-Dist: mypy==2.1
|
|
19
|
-
Requires-Dist: pre-commit==4.6.
|
|
18
|
+
Requires-Dist: codespell==2.4.3; extra == "test"
|
|
19
|
+
Requires-Dist: mypy==2.3.1; extra == "test"
|
|
20
|
+
Requires-Dist: pre-commit==4.6.2; extra == "test"
|
|
20
21
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
21
22
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
22
23
|
Requires-Dist: pytest-asyncio==1.4.0; extra == "test"
|
|
23
24
|
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
24
|
-
Requires-Dist: ruff==0.
|
|
25
|
+
Requires-Dist: ruff==0.16.5; extra == "test"
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
27
28
|
Dynamic: license-file
|
|
@@ -47,6 +48,7 @@ import asyncio
|
|
|
47
48
|
|
|
48
49
|
from deezer_python_gql import DeezerGQLClient
|
|
49
50
|
|
|
51
|
+
|
|
50
52
|
async def main():
|
|
51
53
|
client = DeezerGQLClient(arl="YOUR_ARL_COOKIE")
|
|
52
54
|
|
|
@@ -74,6 +76,7 @@ async def main():
|
|
|
74
76
|
results = await client.search(query="Daft Punk")
|
|
75
77
|
print(len(results.tracks.edges), "tracks found")
|
|
76
78
|
|
|
79
|
+
|
|
77
80
|
asyncio.run(main())
|
|
78
81
|
```
|
|
79
82
|
|
|
@@ -252,6 +255,10 @@ The Pipe API uses short-lived JWTs obtained from an ARL cookie. The base client
|
|
|
252
255
|
handles token acquisition and refresh automatically — you only need to supply a
|
|
253
256
|
valid ARL value.
|
|
254
257
|
|
|
258
|
+
An external `aiohttp.ClientSession` may be shared by clients for different Deezer
|
|
259
|
+
accounts. Each client keeps Deezer's response cookies locally and masks cookies from
|
|
260
|
+
the shared session jar, so authentication state cannot cross between those accounts.
|
|
261
|
+
|
|
255
262
|
If the ARL is rejected (bad or expired), the client raises
|
|
256
263
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
257
264
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/requires.txt
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
aiohttp>=3.13.5
|
|
2
2
|
pydantic>=2.0.0
|
|
3
|
+
yarl>=1.22.0
|
|
3
4
|
|
|
4
5
|
[dev]
|
|
5
6
|
ariadne-codegen[subscriptions]
|
|
6
7
|
|
|
7
8
|
[test]
|
|
8
|
-
codespell==2.4.
|
|
9
|
-
mypy==2.1
|
|
10
|
-
pre-commit==4.6.
|
|
9
|
+
codespell==2.4.3
|
|
10
|
+
mypy==2.3.1
|
|
11
|
+
pre-commit==4.6.2
|
|
11
12
|
pre-commit-hooks==6.0.0
|
|
12
13
|
pytest==9.1.1
|
|
13
14
|
pytest-asyncio==1.4.0
|
|
14
15
|
pytest-cov==7.1.0
|
|
15
|
-
ruff==0.
|
|
16
|
+
ruff==0.16.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deezer-python-gql"
|
|
3
|
-
version = "0.17.
|
|
3
|
+
version = "0.17.2"
|
|
4
4
|
description = "Async typed Python client for Deezer's Pipe GraphQL API."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -15,6 +15,7 @@ classifiers = [
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"aiohttp>=3.13.5",
|
|
17
17
|
"pydantic>=2.0.0",
|
|
18
|
+
"yarl>=1.22.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[project.license]
|
|
@@ -22,14 +23,14 @@ text = "Apache-2.0"
|
|
|
22
23
|
|
|
23
24
|
[project.optional-dependencies]
|
|
24
25
|
test = [
|
|
25
|
-
"codespell==2.4.
|
|
26
|
-
"mypy==2.1
|
|
27
|
-
"pre-commit==4.6.
|
|
26
|
+
"codespell==2.4.3",
|
|
27
|
+
"mypy==2.3.1",
|
|
28
|
+
"pre-commit==4.6.2",
|
|
28
29
|
"pre-commit-hooks==6.0.0",
|
|
29
30
|
"pytest==9.1.1",
|
|
30
31
|
"pytest-asyncio==1.4.0",
|
|
31
32
|
"pytest-cov==7.1.0",
|
|
32
|
-
"ruff==0.
|
|
33
|
+
"ruff==0.16.5",
|
|
33
34
|
]
|
|
34
35
|
dev = [
|
|
35
36
|
"ariadne-codegen[subscriptions]",
|
|
@@ -99,6 +100,7 @@ ignore = [
|
|
|
99
100
|
"TD004",
|
|
100
101
|
"TRY003",
|
|
101
102
|
"COM812",
|
|
103
|
+
"CPY001",
|
|
102
104
|
"ISC001",
|
|
103
105
|
]
|
|
104
106
|
select = [
|
|
@@ -13,11 +13,14 @@ import asyncio
|
|
|
13
13
|
import base64
|
|
14
14
|
import json
|
|
15
15
|
import time
|
|
16
|
+
from http.cookies import SimpleCookie
|
|
16
17
|
from pathlib import Path
|
|
17
|
-
from typing import Any
|
|
18
|
+
from typing import Any, Self, cast
|
|
18
19
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
19
20
|
|
|
20
21
|
import pytest
|
|
22
|
+
from aiohttp import ClientSession, CookieJar
|
|
23
|
+
from yarl import URL
|
|
21
24
|
|
|
22
25
|
from deezer_python_gql import DeezerGQLClient
|
|
23
26
|
from deezer_python_gql.base_client import (
|
|
@@ -124,7 +127,7 @@ def _load_fixture(name: str) -> dict[str, Any]:
|
|
|
124
127
|
return result
|
|
125
128
|
|
|
126
129
|
|
|
127
|
-
def _make_jwt(exp: float | None = None) -> str:
|
|
130
|
+
def _make_jwt(exp: float | None = None, subject: str | None = None) -> str:
|
|
128
131
|
"""
|
|
129
132
|
Build a fake JWT with a configurable expiration timestamp.
|
|
130
133
|
|
|
@@ -133,13 +136,10 @@ def _make_jwt(exp: float | None = None) -> str:
|
|
|
133
136
|
if exp is None:
|
|
134
137
|
exp = time.time() + 360 # 6 min, matching Deezer's real TTL
|
|
135
138
|
header = base64.urlsafe_b64encode(b'{"alg":"ES256"}').rstrip(b"=").decode()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.rstrip(b"=")
|
|
141
|
-
.decode()
|
|
142
|
-
)
|
|
139
|
+
payload_data: dict[str, float | str] = {"exp": exp}
|
|
140
|
+
if subject is not None:
|
|
141
|
+
payload_data["sub"] = subject
|
|
142
|
+
payload = base64.urlsafe_b64encode(json.dumps(payload_data).encode()).rstrip(b"=").decode()
|
|
143
143
|
return f"{header}.{payload}.fake_signature"
|
|
144
144
|
|
|
145
145
|
|
|
@@ -156,6 +156,7 @@ def _mock_post_context_manager(response_body: bytes, status: int = 200) -> Async
|
|
|
156
156
|
mock_resp.read = AsyncMock(return_value=response_body)
|
|
157
157
|
mock_resp.text = AsyncMock(return_value=response_body.decode())
|
|
158
158
|
mock_resp.raise_for_status = MagicMock()
|
|
159
|
+
mock_resp.cookies = SimpleCookie()
|
|
159
160
|
|
|
160
161
|
cm = AsyncMock()
|
|
161
162
|
cm.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
@@ -171,6 +172,86 @@ def _mock_auth_cm(jwt: str | None = None) -> AsyncMock:
|
|
|
171
172
|
return _mock_post_context_manager(body)
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
class _FakeCookieResponse:
|
|
176
|
+
"""Small aiohttp response stand-in for the shared-cookie regression test."""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
body: bytes,
|
|
181
|
+
response_cookies: SimpleCookie | None = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.status = 200
|
|
184
|
+
self.ok = True
|
|
185
|
+
self._body = body
|
|
186
|
+
self.cookies = response_cookies or SimpleCookie()
|
|
187
|
+
|
|
188
|
+
async def __aenter__(self) -> Self:
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
async def __aexit__(self, *args: object) -> None:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
async def read(self) -> bytes:
|
|
195
|
+
return self._body
|
|
196
|
+
|
|
197
|
+
async def text(self) -> str:
|
|
198
|
+
return self._body.decode()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _FakeSharedSession:
|
|
202
|
+
"""Model Deezer preferring an existing refresh cookie over a new ARL."""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
self.cookie_jar = CookieJar()
|
|
206
|
+
self._arl_accounts = {"arl-a": "1001", "arl-b": "2002"}
|
|
207
|
+
self._refresh_accounts = {"refresh-a": "1001", "refresh-b": "2002"}
|
|
208
|
+
self._account_refresh = {value: key for key, value in self._refresh_accounts.items()}
|
|
209
|
+
self._account_cookies = {
|
|
210
|
+
account: {
|
|
211
|
+
"refresh-token": refresh,
|
|
212
|
+
"sid": f"sid-{account}",
|
|
213
|
+
"dzr_uniq_id": f"unique-{account}",
|
|
214
|
+
"_abck": f"abck-{account}",
|
|
215
|
+
"bm_sz": f"bm-{account}",
|
|
216
|
+
}
|
|
217
|
+
for account, refresh in self._account_refresh.items()
|
|
218
|
+
}
|
|
219
|
+
self._account_jwt = {
|
|
220
|
+
account: _make_jwt(subject=account) for account in self._arl_accounts.values()
|
|
221
|
+
}
|
|
222
|
+
self._jwt_account = {jwt: account for account, jwt in self._account_jwt.items()}
|
|
223
|
+
self.requests: list[tuple[str, dict[str, str]]] = []
|
|
224
|
+
|
|
225
|
+
def post(self, url: str, **kwargs: Any) -> _FakeCookieResponse:
|
|
226
|
+
"""Resolve the request identity and update the shared response-cookie jar."""
|
|
227
|
+
request_cookies = {
|
|
228
|
+
name: morsel.value for name, morsel in self.cookie_jar.filter_cookies(URL(url)).items()
|
|
229
|
+
}
|
|
230
|
+
request_cookies.update(kwargs.get("cookies") or {})
|
|
231
|
+
self.requests.append((url, request_cookies))
|
|
232
|
+
|
|
233
|
+
if url == DeezerBaseClient.AUTH_URL:
|
|
234
|
+
if refresh := request_cookies.get("refresh-token"):
|
|
235
|
+
account = self._refresh_accounts[refresh]
|
|
236
|
+
else:
|
|
237
|
+
account = self._arl_accounts[request_cookies["arl"]]
|
|
238
|
+
response_cookies = SimpleCookie()
|
|
239
|
+
for name, value in self._account_cookies[account].items():
|
|
240
|
+
response_cookies[name] = value
|
|
241
|
+
response_cookies[name]["domain"] = ".deezer.com"
|
|
242
|
+
response_cookies[name]["path"] = "/"
|
|
243
|
+
self.cookie_jar.update_cookies(response_cookies, response_url=URL(url))
|
|
244
|
+
return _FakeCookieResponse(
|
|
245
|
+
json.dumps({"jwt": self._account_jwt[account]}).encode(),
|
|
246
|
+
response_cookies,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
authorization = cast("dict[str, str]", kwargs["headers"])["Authorization"]
|
|
250
|
+
jwt = authorization.removeprefix("Bearer ")
|
|
251
|
+
account = self._jwt_account[jwt]
|
|
252
|
+
return _FakeCookieResponse(json.dumps({"data": {"me": {"id": account}}}).encode())
|
|
253
|
+
|
|
254
|
+
|
|
174
255
|
# ---------------------------------------------------------------------------
|
|
175
256
|
# 1. Client setup
|
|
176
257
|
# ---------------------------------------------------------------------------
|
|
@@ -413,6 +494,40 @@ async def test_auth_sends_arl_cookie_to_correct_domain() -> None:
|
|
|
413
494
|
assert auth_call.kwargs["cookies"] == {"arl": "my_secret_arl"}
|
|
414
495
|
|
|
415
496
|
|
|
497
|
+
@pytest.mark.parametrize(("first_arl", "second_arl"), [("arl-a", "arl-b"), ("arl-b", "arl-a")])
|
|
498
|
+
@pytest.mark.asyncio
|
|
499
|
+
async def test_shared_session_keeps_account_cookies_isolated(
|
|
500
|
+
first_arl: str, second_arl: str
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Verify a shared jar cannot make the first authenticated account win."""
|
|
503
|
+
session = _FakeSharedSession()
|
|
504
|
+
expected = session._arl_accounts # noqa: SLF001
|
|
505
|
+
first = DeezerGQLClient(arl=first_arl, session=cast("ClientSession", session))
|
|
506
|
+
second = DeezerGQLClient(arl=second_arl, session=cast("ClientSession", session))
|
|
507
|
+
|
|
508
|
+
first_me = await first.get_me()
|
|
509
|
+
second_me = await second.get_me()
|
|
510
|
+
|
|
511
|
+
assert first_me is not None
|
|
512
|
+
assert second_me is not None
|
|
513
|
+
assert first_me.id == expected[first_arl]
|
|
514
|
+
assert second_me.id == expected[second_arl]
|
|
515
|
+
|
|
516
|
+
auth_requests = [cookies for url, cookies in session.requests if url == first.AUTH_URL]
|
|
517
|
+
pipe_requests = [cookies for url, cookies in session.requests if url == first.url]
|
|
518
|
+
assert auth_requests == [
|
|
519
|
+
{"arl": first_arl},
|
|
520
|
+
{
|
|
521
|
+
**dict.fromkeys(session._account_cookies[first_me.id], ""), # noqa: SLF001
|
|
522
|
+
"arl": second_arl,
|
|
523
|
+
},
|
|
524
|
+
]
|
|
525
|
+
assert pipe_requests == [
|
|
526
|
+
session._account_cookies[first_me.id], # noqa: SLF001
|
|
527
|
+
session._account_cookies[second_me.id], # noqa: SLF001
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
|
|
416
531
|
@pytest.mark.asyncio
|
|
417
532
|
async def test_auth_parses_text_plain_response() -> None:
|
|
418
533
|
"""Verify the client handles auth.deezer.com's text/plain JSON response."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/base_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/create_playlist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/delete_playlist.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/fragments.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_album.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_artist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_artist_mix.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_audiobook.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_charts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_flow_batch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_livestream.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_made_for_me.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_playlist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_podcast.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_track.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_track_mix.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/get_user_charts.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/input_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/search_flows.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql/generated/update_playlist.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{deezer_python_gql-0.17.1 → deezer_python_gql-0.17.2}/deezer_python_gql.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|