deezer-python-gql 0.6.0__tar.gz → 0.8.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.6.0/deezer_python_gql.egg-info → deezer_python_gql-0.8.0}/PKG-INFO +7 -1
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/README.md +6 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql/base_client.py +84 -26
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0/deezer_python_gql.egg-info}/PKG-INFO +7 -1
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/pyproject.toml +1 -1
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/tests/test_client.py +119 -28
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/LICENSE +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/MANIFEST.in +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql/__init__.py +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql/py.typed +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/SOURCES.txt +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/not-zip-safe +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/requires.txt +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/top_level.txt +0 -0
- {deezer_python_gql-0.6.0 → deezer_python_gql-0.8.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.8.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
|
|
@@ -184,6 +184,12 @@ asyncio.run(main())
|
|
|
184
184
|
| `music_together_update_group_settings(...)` | Update group settings (name, family mode) |
|
|
185
185
|
| `music_together_generate_group_name()` | Generate a random group name |
|
|
186
186
|
|
|
187
|
+
### Utilities
|
|
188
|
+
|
|
189
|
+
| Method | Description |
|
|
190
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
191
|
+
| `check_audiobook_ids(album_ids)` | Batch-check which album IDs are audiobooks (single GraphQL call) |
|
|
192
|
+
|
|
187
193
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
188
194
|
|
|
189
195
|
## Development
|
|
@@ -156,6 +156,12 @@ asyncio.run(main())
|
|
|
156
156
|
| `music_together_update_group_settings(...)` | Update group settings (name, family mode) |
|
|
157
157
|
| `music_together_generate_group_name()` | Generate a random group name |
|
|
158
158
|
|
|
159
|
+
### Utilities
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
163
|
+
| `check_audiobook_ids(album_ids)` | Batch-check which album IDs are audiobooks (single GraphQL call) |
|
|
164
|
+
|
|
159
165
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
160
166
|
|
|
161
167
|
## Development
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
import time
|
|
8
8
|
from base64 import urlsafe_b64decode
|
|
9
|
-
from typing import Any, ClassVar, cast
|
|
9
|
+
from typing import Any, ClassVar, Self, cast
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
@@ -76,9 +76,13 @@ class DeezerBaseClient:
|
|
|
76
76
|
Handles the ARL cookie → JWT token exchange and automatic refresh.
|
|
77
77
|
This class is used as the base client for ariadne-codegen's generated client.
|
|
78
78
|
|
|
79
|
+
Manages its own httpx connection pool by default. Pass an external
|
|
80
|
+
``http_client`` only if you need to share a pool across multiple clients.
|
|
81
|
+
|
|
79
82
|
:param arl: Deezer ARL cookie value for authentication.
|
|
80
83
|
:param url: GraphQL endpoint URL (defaults to Pipe API).
|
|
81
84
|
:param http_client: Optional pre-configured httpx.AsyncClient.
|
|
85
|
+
If provided, the caller is responsible for closing it.
|
|
82
86
|
"""
|
|
83
87
|
|
|
84
88
|
PIPE_URL = "https://pipe.deezer.com/api"
|
|
@@ -94,9 +98,35 @@ class DeezerBaseClient:
|
|
|
94
98
|
self.url = url
|
|
95
99
|
self._arl = arl
|
|
96
100
|
self._http_client = http_client
|
|
101
|
+
self._owns_http_client = http_client is None
|
|
97
102
|
self._jwt: str | None = None
|
|
98
103
|
self._jwt_expires_at: float = 0
|
|
99
104
|
|
|
105
|
+
def _get_http_client(self) -> httpx.AsyncClient:
|
|
106
|
+
"""Return the HTTP client, creating an internal one if needed."""
|
|
107
|
+
if self._http_client is None:
|
|
108
|
+
self._http_client = httpx.AsyncClient()
|
|
109
|
+
self._owns_http_client = True
|
|
110
|
+
return self._http_client
|
|
111
|
+
|
|
112
|
+
async def close(self) -> None:
|
|
113
|
+
"""Close the internal HTTP client if we own it.
|
|
114
|
+
|
|
115
|
+
Safe to call multiple times. Does nothing if an external
|
|
116
|
+
``http_client`` was provided at construction time.
|
|
117
|
+
"""
|
|
118
|
+
if self._owns_http_client and self._http_client is not None:
|
|
119
|
+
await self._http_client.aclose()
|
|
120
|
+
self._http_client = None
|
|
121
|
+
|
|
122
|
+
async def __aenter__(self) -> Self:
|
|
123
|
+
"""Enter the async context manager."""
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
async def __aexit__(self, *args: object) -> None:
|
|
127
|
+
"""Exit the async context manager, closing internal resources."""
|
|
128
|
+
await self.close()
|
|
129
|
+
|
|
100
130
|
async def execute(
|
|
101
131
|
self,
|
|
102
132
|
query: str,
|
|
@@ -132,24 +162,20 @@ class DeezerBaseClient:
|
|
|
132
162
|
k: v for k, v in variables.items() if not isinstance(v, UnsetType)
|
|
133
163
|
}
|
|
134
164
|
|
|
135
|
-
client = self.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return resp
|
|
150
|
-
finally:
|
|
151
|
-
if not self._http_client:
|
|
152
|
-
await client.aclose()
|
|
165
|
+
client = self._get_http_client()
|
|
166
|
+
resp = await client.post(
|
|
167
|
+
self.url,
|
|
168
|
+
json=payload,
|
|
169
|
+
headers=headers,
|
|
170
|
+
**kwargs,
|
|
171
|
+
)
|
|
172
|
+
logger.debug(
|
|
173
|
+
"GQL response: %s status=%s length=%s",
|
|
174
|
+
operation_name or "<unnamed>",
|
|
175
|
+
resp.status_code,
|
|
176
|
+
len(resp.content),
|
|
177
|
+
)
|
|
178
|
+
return resp
|
|
153
179
|
|
|
154
180
|
def get_data(self, response: httpx.Response) -> dict[str, Any]:
|
|
155
181
|
"""Parse a GraphQL response and return the data dict.
|
|
@@ -236,13 +262,13 @@ class DeezerBaseClient:
|
|
|
236
262
|
logger.debug("JWT expired or missing, refreshing from ARL")
|
|
237
263
|
params = {"jo": "p", "rto": "c", "i": "c"}
|
|
238
264
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
265
|
+
client = self._get_http_client()
|
|
266
|
+
resp = await client.post(
|
|
267
|
+
self.AUTH_URL,
|
|
268
|
+
params=params,
|
|
269
|
+
cookies={"arl": self._arl},
|
|
270
|
+
)
|
|
271
|
+
resp.raise_for_status()
|
|
246
272
|
|
|
247
273
|
# Response body is text/plain containing JSON
|
|
248
274
|
data = json.loads(resp.text)
|
|
@@ -257,3 +283,35 @@ class DeezerBaseClient:
|
|
|
257
283
|
logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
|
|
258
284
|
|
|
259
285
|
return self._jwt
|
|
286
|
+
|
|
287
|
+
async def check_audiobook_ids(self, album_ids: list[str]) -> set[str]:
|
|
288
|
+
"""Check which album IDs are also valid audiobooks on Deezer.
|
|
289
|
+
|
|
290
|
+
Uses GraphQL aliases to batch-check many IDs in a single request.
|
|
291
|
+
Returns the subset of IDs that are audiobooks (i.e., the audiobook
|
|
292
|
+
query returns non-null for them).
|
|
293
|
+
|
|
294
|
+
:param album_ids: List of Deezer album/audiobook IDs to check.
|
|
295
|
+
"""
|
|
296
|
+
if not album_ids:
|
|
297
|
+
return set()
|
|
298
|
+
|
|
299
|
+
# Query displayTitle alongside id — querying only { id } echoes back the
|
|
300
|
+
# input without validating that the ID is actually an audiobook.
|
|
301
|
+
parts = [
|
|
302
|
+
f'a{i}: audiobook(audiobookId: "{aid}") {{ id displayTitle }}'
|
|
303
|
+
for i, aid in enumerate(album_ids)
|
|
304
|
+
]
|
|
305
|
+
query = "{ " + " ".join(parts) + " }"
|
|
306
|
+
|
|
307
|
+
resp = await self.execute(query)
|
|
308
|
+
data = self.get_data(resp)
|
|
309
|
+
|
|
310
|
+
audiobook_ids: set[str] = set()
|
|
311
|
+
for i, aid in enumerate(album_ids):
|
|
312
|
+
node = data.get(f"a{i}")
|
|
313
|
+
# The API echoes back {id} for any valid album, so we must check
|
|
314
|
+
# a real audiobook field like displayTitle to distinguish.
|
|
315
|
+
if node is not None and node.get("displayTitle") is not None:
|
|
316
|
+
audiobook_ids.add(aid)
|
|
317
|
+
return audiobook_ids
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -184,6 +184,12 @@ asyncio.run(main())
|
|
|
184
184
|
| `music_together_update_group_settings(...)` | Update group settings (name, family mode) |
|
|
185
185
|
| `music_together_generate_group_name()` | Generate a random group name |
|
|
186
186
|
|
|
187
|
+
### Utilities
|
|
188
|
+
|
|
189
|
+
| Method | Description |
|
|
190
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
191
|
+
| `check_audiobook_ids(album_ids)` | Batch-check which album IDs are audiobooks (single GraphQL call) |
|
|
192
|
+
|
|
187
193
|
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
188
194
|
|
|
189
195
|
## Development
|
|
@@ -67,6 +67,9 @@ from deezer_python_gql.generated.get_podcast_episode import GetPodcastEpisode
|
|
|
67
67
|
from deezer_python_gql.generated.get_podcast_episode_bookmarks import (
|
|
68
68
|
GetPodcastEpisodeBookmarks,
|
|
69
69
|
)
|
|
70
|
+
from deezer_python_gql.generated.get_podcast_episodes_by_ids import (
|
|
71
|
+
GetPodcastEpisodesByIds,
|
|
72
|
+
)
|
|
70
73
|
from deezer_python_gql.generated.get_recently_played import GetRecentlyPlayed
|
|
71
74
|
from deezer_python_gql.generated.get_recommendations import GetRecommendations
|
|
72
75
|
from deezer_python_gql.generated.get_similar_tracks import GetSimilarTracks
|
|
@@ -209,6 +212,7 @@ def test_client_has_generated_methods() -> None:
|
|
|
209
212
|
"remove_tracks_from_playlist",
|
|
210
213
|
"get_podcast",
|
|
211
214
|
"get_podcast_episode",
|
|
215
|
+
"get_podcast_episodes_by_ids",
|
|
212
216
|
"get_favorite_podcasts",
|
|
213
217
|
"get_podcast_episode_bookmarks",
|
|
214
218
|
"add_podcast_to_favorite",
|
|
@@ -241,7 +245,55 @@ def test_client_has_generated_methods() -> None:
|
|
|
241
245
|
|
|
242
246
|
|
|
243
247
|
# ---------------------------------------------------------------------------
|
|
244
|
-
# 2.
|
|
248
|
+
# 2. Lifecycle (connection pool management)
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.asyncio
|
|
253
|
+
async def test_close_shuts_down_internal_client() -> None:
|
|
254
|
+
"""Verify close() calls aclose() on the internally-created httpx client."""
|
|
255
|
+
client = DeezerBaseClient(arl="test")
|
|
256
|
+
|
|
257
|
+
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_cls:
|
|
258
|
+
mock_instance = AsyncMock()
|
|
259
|
+
mock_cls.return_value = mock_instance
|
|
260
|
+
|
|
261
|
+
# Trigger lazy creation
|
|
262
|
+
client._get_http_client() # noqa: SLF001
|
|
263
|
+
assert client._http_client is mock_instance # noqa: SLF001
|
|
264
|
+
|
|
265
|
+
await client.close()
|
|
266
|
+
|
|
267
|
+
mock_instance.aclose.assert_awaited_once()
|
|
268
|
+
assert client._http_client is None # noqa: SLF001
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_close_skips_external_client() -> None:
|
|
273
|
+
"""Verify close() does NOT close an externally-provided httpx client."""
|
|
274
|
+
external = AsyncMock(spec=httpx.AsyncClient)
|
|
275
|
+
client = DeezerBaseClient(arl="test", http_client=external)
|
|
276
|
+
|
|
277
|
+
await client.close()
|
|
278
|
+
|
|
279
|
+
external.aclose.assert_not_awaited()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@pytest.mark.asyncio
|
|
283
|
+
async def test_context_manager_calls_close() -> None:
|
|
284
|
+
"""Verify the async context manager closes the client on exit."""
|
|
285
|
+
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_cls:
|
|
286
|
+
mock_instance = AsyncMock()
|
|
287
|
+
mock_cls.return_value = mock_instance
|
|
288
|
+
|
|
289
|
+
async with DeezerBaseClient(arl="test") as client:
|
|
290
|
+
client._get_http_client() # noqa: SLF001
|
|
291
|
+
|
|
292
|
+
mock_instance.aclose.assert_awaited_once()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# 3. Auth flow (mocked HTTP)
|
|
245
297
|
# ---------------------------------------------------------------------------
|
|
246
298
|
|
|
247
299
|
|
|
@@ -251,27 +303,19 @@ async def test_auth_acquires_jwt_on_first_request() -> None:
|
|
|
251
303
|
jwt = _make_jwt()
|
|
252
304
|
client = DeezerBaseClient(arl="test_arl")
|
|
253
305
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
json={"data": {"me": {"id": "1"}}},
|
|
259
|
-
request=httpx.Request("POST", DeezerBaseClient.PIPE_URL),
|
|
260
|
-
),
|
|
306
|
+
gql_response = httpx.Response(
|
|
307
|
+
200,
|
|
308
|
+
json={"data": {"me": {"id": "1"}}},
|
|
309
|
+
request=httpx.Request("POST", DeezerBaseClient.PIPE_URL),
|
|
261
310
|
)
|
|
262
311
|
|
|
263
312
|
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
264
313
|
mock_instance = AsyncMock()
|
|
265
|
-
mock_instance.post = mock_auth
|
|
266
|
-
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
|
267
|
-
mock_instance.__aexit__ = AsyncMock(return_value=False)
|
|
268
|
-
mock_client_cls.return_value = mock_instance
|
|
269
|
-
|
|
270
|
-
# First call: should trigger auth, then make GQL request
|
|
271
|
-
# Override post to return auth first, then GQL response
|
|
272
314
|
mock_instance.post = AsyncMock(
|
|
273
|
-
side_effect=[_mock_auth_response(jwt),
|
|
315
|
+
side_effect=[_mock_auth_response(jwt), gql_response],
|
|
274
316
|
)
|
|
317
|
+
mock_client_cls.return_value = mock_instance
|
|
318
|
+
|
|
275
319
|
resp = await client.execute(query="{ me { id } }")
|
|
276
320
|
|
|
277
321
|
assert resp.status_code == 200
|
|
@@ -295,8 +339,6 @@ async def test_auth_reuses_valid_jwt() -> None:
|
|
|
295
339
|
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
296
340
|
mock_instance = AsyncMock()
|
|
297
341
|
mock_instance.post = AsyncMock(return_value=gql_response)
|
|
298
|
-
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
|
299
|
-
mock_instance.__aexit__ = AsyncMock(return_value=False)
|
|
300
342
|
mock_client_cls.return_value = mock_instance
|
|
301
343
|
|
|
302
344
|
await client.execute(query="{ me { id } }")
|
|
@@ -317,9 +359,6 @@ async def test_auth_refreshes_expiring_jwt() -> None:
|
|
|
317
359
|
|
|
318
360
|
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
319
361
|
mock_instance = AsyncMock()
|
|
320
|
-
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
|
321
|
-
mock_instance.__aexit__ = AsyncMock(return_value=False)
|
|
322
|
-
# Auth response, then GQL response
|
|
323
362
|
mock_instance.post = AsyncMock(
|
|
324
363
|
side_effect=[
|
|
325
364
|
_mock_auth_response(new_jwt),
|
|
@@ -344,8 +383,6 @@ async def test_auth_sends_arl_cookie_to_correct_domain() -> None:
|
|
|
344
383
|
|
|
345
384
|
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
346
385
|
mock_instance = AsyncMock()
|
|
347
|
-
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
|
348
|
-
mock_instance.__aexit__ = AsyncMock(return_value=False)
|
|
349
386
|
mock_instance.post = AsyncMock(
|
|
350
387
|
side_effect=[
|
|
351
388
|
_mock_auth_response(),
|
|
@@ -375,8 +412,6 @@ async def test_auth_parses_text_plain_response() -> None:
|
|
|
375
412
|
# Verify _ensure_jwt correctly parses the text/plain body
|
|
376
413
|
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
377
414
|
mock_instance = AsyncMock()
|
|
378
|
-
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
|
379
|
-
mock_instance.__aexit__ = AsyncMock(return_value=False)
|
|
380
415
|
mock_instance.post = AsyncMock(return_value=_mock_auth_response(jwt))
|
|
381
416
|
mock_client_cls.return_value = mock_instance
|
|
382
417
|
|
|
@@ -387,7 +422,7 @@ async def test_auth_parses_text_plain_response() -> None:
|
|
|
387
422
|
|
|
388
423
|
|
|
389
424
|
# ---------------------------------------------------------------------------
|
|
390
|
-
#
|
|
425
|
+
# 4. Error handling (mocked HTTP)
|
|
391
426
|
# ---------------------------------------------------------------------------
|
|
392
427
|
|
|
393
428
|
|
|
@@ -467,7 +502,49 @@ def test_get_data_returns_data_on_success() -> None:
|
|
|
467
502
|
|
|
468
503
|
|
|
469
504
|
# ---------------------------------------------------------------------------
|
|
470
|
-
#
|
|
505
|
+
# 5. check_audiobook_ids tests
|
|
506
|
+
# ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@pytest.mark.asyncio
|
|
510
|
+
async def test_check_audiobook_ids_returns_matching() -> None:
|
|
511
|
+
"""Verify check_audiobook_ids returns IDs that are valid audiobooks."""
|
|
512
|
+
client = DeezerBaseClient(arl="test_arl")
|
|
513
|
+
client._jwt = _make_jwt(exp=time.time() + 600) # noqa: SLF001
|
|
514
|
+
client._jwt_expires_at = time.time() + 600 # noqa: SLF001
|
|
515
|
+
|
|
516
|
+
# a0 is an audiobook (has displayTitle), a1 is not (displayTitle is null)
|
|
517
|
+
gql_response = httpx.Response(
|
|
518
|
+
200,
|
|
519
|
+
json={
|
|
520
|
+
"data": {
|
|
521
|
+
"a0": {"id": "111", "displayTitle": "Test Book"},
|
|
522
|
+
"a1": {"id": "222", "displayTitle": None},
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
request=httpx.Request("POST", DeezerBaseClient.PIPE_URL),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
|
|
529
|
+
mock_instance = AsyncMock()
|
|
530
|
+
mock_instance.post = AsyncMock(return_value=gql_response)
|
|
531
|
+
mock_client_cls.return_value = mock_instance
|
|
532
|
+
|
|
533
|
+
result = await client.check_audiobook_ids(["111", "222"])
|
|
534
|
+
|
|
535
|
+
assert result == {"111"}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@pytest.mark.asyncio
|
|
539
|
+
async def test_check_audiobook_ids_empty_input() -> None:
|
|
540
|
+
"""Verify check_audiobook_ids returns empty set for empty input."""
|
|
541
|
+
client = DeezerBaseClient(arl="test_arl")
|
|
542
|
+
result = await client.check_audiobook_ids([])
|
|
543
|
+
assert result == set()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
# 6. Model smoke tests (one per query — fixture-based)
|
|
471
548
|
# ---------------------------------------------------------------------------
|
|
472
549
|
|
|
473
550
|
|
|
@@ -548,7 +625,7 @@ def test_smoke_search() -> None:
|
|
|
548
625
|
|
|
549
626
|
|
|
550
627
|
# ---------------------------------------------------------------------------
|
|
551
|
-
#
|
|
628
|
+
# 6. Browse-related model smoke tests (new queries)
|
|
552
629
|
# ---------------------------------------------------------------------------
|
|
553
630
|
|
|
554
631
|
|
|
@@ -995,6 +1072,20 @@ def test_smoke_get_podcast_episode() -> None:
|
|
|
995
1072
|
assert ep.podcast.display_title == "Tech Weekly"
|
|
996
1073
|
|
|
997
1074
|
|
|
1075
|
+
def test_smoke_get_podcast_episodes_by_ids() -> None:
|
|
1076
|
+
"""Verify GetPodcastEpisodesByIds fixture parses with nullable entries."""
|
|
1077
|
+
data = _load_fixture("get_podcast_episodes_by_ids.json")
|
|
1078
|
+
episodes = GetPodcastEpisodesByIds.model_validate(data).podcast_episodes_by_ids
|
|
1079
|
+
assert len(episodes) == 3
|
|
1080
|
+
assert episodes[0] is not None
|
|
1081
|
+
assert episodes[0].id == "ep_100"
|
|
1082
|
+
assert episodes[0].title == "Episode 100: AI Revolution"
|
|
1083
|
+
assert episodes[0].duration == 2400
|
|
1084
|
+
assert episodes[1] is not None
|
|
1085
|
+
assert episodes[1].id == "ep_099"
|
|
1086
|
+
assert episodes[2] is None
|
|
1087
|
+
|
|
1088
|
+
|
|
998
1089
|
def test_smoke_get_favorite_podcasts() -> None:
|
|
999
1090
|
"""Verify GetFavoritePodcasts fixture parses with pagination."""
|
|
1000
1091
|
data = _load_fixture("get_favorite_podcasts.json")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.6.0 → deezer_python_gql-0.8.0}/deezer_python_gql.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|