deezer-python-gql 0.6.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deezer-python-gql
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -257,3 +257,35 @@ class DeezerBaseClient:
257
257
  logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
258
258
 
259
259
  return self._jwt
260
+
261
+ async def check_audiobook_ids(self, album_ids: list[str]) -> set[str]:
262
+ """Check which album IDs are also valid audiobooks on Deezer.
263
+
264
+ Uses GraphQL aliases to batch-check many IDs in a single request.
265
+ Returns the subset of IDs that are audiobooks (i.e., the audiobook
266
+ query returns non-null for them).
267
+
268
+ :param album_ids: List of Deezer album/audiobook IDs to check.
269
+ """
270
+ if not album_ids:
271
+ return set()
272
+
273
+ # Query displayTitle alongside id — querying only { id } echoes back the
274
+ # input without validating that the ID is actually an audiobook.
275
+ parts = [
276
+ f'a{i}: audiobook(audiobookId: "{aid}") {{ id displayTitle }}'
277
+ for i, aid in enumerate(album_ids)
278
+ ]
279
+ query = "{ " + " ".join(parts) + " }"
280
+
281
+ resp = await self.execute(query)
282
+ data = self.get_data(resp)
283
+
284
+ audiobook_ids: set[str] = set()
285
+ for i, aid in enumerate(album_ids):
286
+ node = data.get(f"a{i}")
287
+ # The API echoes back {id} for any valid album, so we must check
288
+ # a real audiobook field like displayTitle to distinguish.
289
+ if node is not None and node.get("displayTitle") is not None:
290
+ audiobook_ids.add(aid)
291
+ return audiobook_ids
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deezer-python-gql
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deezer-python-gql"
3
- version = "0.6.0"
3
+ version = "0.7.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"
@@ -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",
@@ -467,7 +471,51 @@ def test_get_data_returns_data_on_success() -> None:
467
471
 
468
472
 
469
473
  # ---------------------------------------------------------------------------
470
- # 4. Model smoke tests (one per query — fixture-based)
474
+ # 4a. check_audiobook_ids tests
475
+ # ---------------------------------------------------------------------------
476
+
477
+
478
+ @pytest.mark.asyncio
479
+ async def test_check_audiobook_ids_returns_matching() -> None:
480
+ """Verify check_audiobook_ids returns IDs that are valid audiobooks."""
481
+ client = DeezerBaseClient(arl="test_arl")
482
+ client._jwt = _make_jwt(exp=time.time() + 600) # noqa: SLF001
483
+ client._jwt_expires_at = time.time() + 600 # noqa: SLF001
484
+
485
+ # a0 is an audiobook (has displayTitle), a1 is not (displayTitle is null)
486
+ gql_response = httpx.Response(
487
+ 200,
488
+ json={
489
+ "data": {
490
+ "a0": {"id": "111", "displayTitle": "Test Book"},
491
+ "a1": {"id": "222", "displayTitle": None},
492
+ }
493
+ },
494
+ request=httpx.Request("POST", DeezerBaseClient.PIPE_URL),
495
+ )
496
+
497
+ with patch("deezer_python_gql.base_client.httpx.AsyncClient") as mock_client_cls:
498
+ mock_instance = AsyncMock()
499
+ mock_instance.post = AsyncMock(return_value=gql_response)
500
+ mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
501
+ mock_instance.__aexit__ = AsyncMock(return_value=False)
502
+ mock_client_cls.return_value = mock_instance
503
+
504
+ result = await client.check_audiobook_ids(["111", "222"])
505
+
506
+ assert result == {"111"}
507
+
508
+
509
+ @pytest.mark.asyncio
510
+ async def test_check_audiobook_ids_empty_input() -> None:
511
+ """Verify check_audiobook_ids returns empty set for empty input."""
512
+ client = DeezerBaseClient(arl="test_arl")
513
+ result = await client.check_audiobook_ids([])
514
+ assert result == set()
515
+
516
+
517
+ # ---------------------------------------------------------------------------
518
+ # 5. Model smoke tests (one per query — fixture-based)
471
519
  # ---------------------------------------------------------------------------
472
520
 
473
521
 
@@ -995,6 +1043,20 @@ def test_smoke_get_podcast_episode() -> None:
995
1043
  assert ep.podcast.display_title == "Tech Weekly"
996
1044
 
997
1045
 
1046
+ def test_smoke_get_podcast_episodes_by_ids() -> None:
1047
+ """Verify GetPodcastEpisodesByIds fixture parses with nullable entries."""
1048
+ data = _load_fixture("get_podcast_episodes_by_ids.json")
1049
+ episodes = GetPodcastEpisodesByIds.model_validate(data).podcast_episodes_by_ids
1050
+ assert len(episodes) == 3
1051
+ assert episodes[0] is not None
1052
+ assert episodes[0].id == "ep_100"
1053
+ assert episodes[0].title == "Episode 100: AI Revolution"
1054
+ assert episodes[0].duration == 2400
1055
+ assert episodes[1] is not None
1056
+ assert episodes[1].id == "ep_099"
1057
+ assert episodes[2] is None
1058
+
1059
+
998
1060
  def test_smoke_get_favorite_podcasts() -> None:
999
1061
  """Verify GetFavoritePodcasts fixture parses with pagination."""
1000
1062
  data = _load_fixture("get_favorite_podcasts.json")