deezer-python-gql 0.21.0__tar.gz → 0.22.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.
Files changed (104) hide show
  1. {deezer_python_gql-0.21.0/deezer_python_gql.egg-info → deezer_python_gql-0.22.2}/PKG-INFO +5 -5
  2. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/base_client.py +9 -4
  3. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/__init__.py +26 -0
  4. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/base_client.py +9 -4
  5. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/client.py +37 -0
  6. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/enums.py +14 -0
  7. deezer_python_gql-0.22.2/deezer_python_gql/generated/input_types.py +62 -0
  8. deezer_python_gql-0.22.2/deezer_python_gql/generated/push_queue_state.py +39 -0
  9. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2/deezer_python_gql.egg-info}/PKG-INFO +5 -5
  10. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql.egg-info/SOURCES.txt +1 -0
  11. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql.egg-info/requires.txt +4 -4
  12. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/pyproject.toml +6 -6
  13. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/tests/test_client.py +98 -5
  14. deezer_python_gql-0.21.0/deezer_python_gql/generated/input_types.py +0 -3
  15. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/LICENSE +0 -0
  16. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/MANIFEST.in +0 -0
  17. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/README.md +0 -0
  18. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/__init__.py +0 -0
  19. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_album_to_favorite.py +0 -0
  20. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_artist_to_favorite.py +0 -0
  21. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_audiobook_to_favorite.py +0 -0
  22. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_playlist_to_favorite.py +0 -0
  23. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_podcast_to_favorite.py +0 -0
  24. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_track_to_favorite.py +0 -0
  25. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/add_tracks_to_playlist.py +0 -0
  26. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/ban_artist_from_recommendation.py +0 -0
  27. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/ban_track_from_recommendation.py +0 -0
  28. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/base_model.py +0 -0
  29. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/bookmark_podcast_episode.py +0 -0
  30. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/create_playlist.py +0 -0
  31. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/delete_playlist.py +0 -0
  32. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/fragments.py +0 -0
  33. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_album.py +0 -0
  34. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_artist.py +0 -0
  35. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_artist_mix.py +0 -0
  36. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_audiobook.py +0 -0
  37. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_audiobook_chapter.py +0 -0
  38. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_banned_artists.py +0 -0
  39. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_banned_tracks.py +0 -0
  40. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_charts.py +0 -0
  41. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_family.py +0 -0
  42. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_albums.py +0 -0
  43. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_artists.py +0 -0
  44. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_audiobooks.py +0 -0
  45. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_playlists.py +0 -0
  46. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_podcasts.py +0 -0
  47. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_favorite_tracks.py +0 -0
  48. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_flow.py +0 -0
  49. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_flow_batch.py +0 -0
  50. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_flow_config_tracks.py +0 -0
  51. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_flow_configs.py +0 -0
  52. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_flow_tuner.py +0 -0
  53. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_infinite_track_mix.py +0 -0
  54. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_latest_podcast_episodes.py +0 -0
  55. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_livestream.py +0 -0
  56. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_made_for_me.py +0 -0
  57. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_me.py +0 -0
  58. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_music_together_affinity.py +0 -0
  59. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_music_together_group.py +0 -0
  60. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_music_together_groups.py +0 -0
  61. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_personal_tracks.py +0 -0
  62. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_playlist.py +0 -0
  63. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_podcast.py +0 -0
  64. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_podcast_episode.py +0 -0
  65. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_podcast_episode_bookmarks.py +0 -0
  66. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_podcast_episodes_by_ids.py +0 -0
  67. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_recently_played.py +0 -0
  68. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_recommendations.py +0 -0
  69. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_similar_artists.py +0 -0
  70. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_similar_tracks.py +0 -0
  71. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_smart_tracklist.py +0 -0
  72. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_track.py +0 -0
  73. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_track_mix.py +0 -0
  74. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_tracks_by_ids.py +0 -0
  75. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_user_charts.py +0 -0
  76. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/get_user_playlists.py +0 -0
  77. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/mark_as_not_played_podcast_episode.py +0 -0
  78. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/mark_as_played_podcast_episode.py +0 -0
  79. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/move_track_in_playlist.py +0 -0
  80. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_create_group.py +0 -0
  81. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_generate_group_name.py +0 -0
  82. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_join_group.py +0 -0
  83. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_leave_group.py +0 -0
  84. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_refresh_suggested_tracklist.py +0 -0
  85. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/music_together_update_group_settings.py +0 -0
  86. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_album_from_favorite.py +0 -0
  87. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_artist_from_favorite.py +0 -0
  88. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_audiobook_from_favorite.py +0 -0
  89. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_playlist_from_favorite.py +0 -0
  90. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_podcast_from_favorite.py +0 -0
  91. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_track_from_favorite.py +0 -0
  92. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/remove_tracks_from_playlist.py +0 -0
  93. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/search.py +0 -0
  94. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/search_flows.py +0 -0
  95. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/set_flow_discovery_tuner.py +0 -0
  96. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/unban_artist_from_recommendation.py +0 -0
  97. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/unban_track_from_recommendation.py +0 -0
  98. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/unbookmark_podcast_episode.py +0 -0
  99. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/generated/update_playlist.py +0 -0
  100. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql/py.typed +0 -0
  101. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
  102. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql.egg-info/not-zip-safe +0 -0
  103. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/deezer_python_gql.egg-info/top_level.txt +0 -0
  104. {deezer_python_gql-0.21.0 → deezer_python_gql-0.22.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deezer-python-gql
3
- Version: 0.21.0
3
+ Version: 0.22.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
@@ -16,13 +16,13 @@ Requires-Dist: pydantic>=2.0.0
16
16
  Requires-Dist: yarl>=1.22.0
17
17
  Provides-Extra: test
18
18
  Requires-Dist: codespell==2.4.3; extra == "test"
19
- Requires-Dist: mypy==2.4.0; extra == "test"
19
+ Requires-Dist: mypy==2.4.1; extra == "test"
20
20
  Requires-Dist: pre-commit==4.7.0; extra == "test"
21
21
  Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
22
- Requires-Dist: pytest==9.1.1; extra == "test"
23
- Requires-Dist: pytest-asyncio==1.4.1; extra == "test"
22
+ Requires-Dist: pytest==9.2.0; extra == "test"
23
+ Requires-Dist: pytest-asyncio==1.4.1.post1; extra == "test"
24
24
  Requires-Dist: pytest-cov==7.1.0; extra == "test"
25
- Requires-Dist: ruff==0.17.2; extra == "test"
25
+ Requires-Dist: ruff==0.17.3; extra == "test"
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
28
28
  Dynamic: license-file
@@ -11,6 +11,7 @@ from dataclasses import dataclass
11
11
  from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
12
12
 
13
13
  from aiohttp import ClientSession, ClientTimeout
14
+ from pydantic_core import to_jsonable_python
14
15
  from yarl import URL
15
16
 
16
17
  if TYPE_CHECKING:
@@ -227,8 +228,11 @@ class DeezerBaseClient:
227
228
  # Inline import avoids depending on generated code at module level.
228
229
  from deezer_python_gql.generated.base_model import UnsetType # noqa: PLC0415
229
230
 
231
+ # input types are pydantic models, sent under their GraphQL field names
230
232
  payload["variables"] = {
231
- k: v for k, v in variables.items() if not isinstance(v, UnsetType)
233
+ k: to_jsonable_python(v, by_alias=True)
234
+ for k, v in variables.items()
235
+ if not isinstance(v, UnsetType)
232
236
  }
233
237
 
234
238
  session = self._get_session()
@@ -284,11 +288,11 @@ class DeezerBaseClient:
284
288
  variables = response.variables
285
289
  if data:
286
290
  # Partial success — some items failed (e.g. deleted albums in favorites).
287
- # Log the errors but return the valid data.
291
+ # That is expected, so log at debug level and return the valid data.
288
292
  error_details = [
289
293
  f"{e.get('message', 'Unknown error')} (path={e.get('path')})" for e in errors
290
294
  ]
291
- logger.warning(
295
+ logger.debug(
292
296
  "GraphQL response for %s (variables=%s) contained %d error(s): %s",
293
297
  op,
294
298
  variables,
@@ -373,7 +377,8 @@ class DeezerBaseClient:
373
377
  text = await resp.text()
374
378
 
375
379
  if not is_success:
376
- if 400 <= status < 500:
380
+ # 429 is rate limiting, not a rejected ARL
381
+ if 400 <= status < 500 and status != 429:
377
382
  msg = f"ARL authentication rejected by auth.deezer.com (HTTP {status})"
378
383
  raise GraphQLClientAuthError(msg)
379
384
  raise GraphQLClientHttpError(
@@ -63,10 +63,12 @@ from .enums import (
63
63
  AudiobookContributorRoles,
64
64
  DiscoveryTuner,
65
65
  DiscoveryTunerInput,
66
+ MediaTypeInput,
66
67
  MusicTogetherRefreshSuggestedTracklistMoodInput,
67
68
  MusicTogetherSuggestedTracklistMoodInput,
68
69
  PodcastEpisodeOrder,
69
70
  PodcastType,
71
+ RepeatModeInput,
70
72
  TrackContributorRoles,
71
73
  )
72
74
  from .fragments import (
@@ -551,6 +553,12 @@ from .get_user_playlists import (
551
553
  GetUserPlaylistsMePlaylistsEdgesNode,
552
554
  GetUserPlaylistsMePlaylistsPageInfo,
553
555
  )
556
+ from .input_types import (
557
+ PushQueueContextInput,
558
+ PushQueueMediaInput,
559
+ PushQueueStateClientDetailsInput,
560
+ PushQueueStateInput,
561
+ )
554
562
  from .mark_as_not_played_podcast_episode import (
555
563
  MarkAsNotPlayedPodcastEpisode,
556
564
  MarkAsNotPlayedPodcastEpisodeMarkAsNotPlayedPodcastEpisode,
@@ -599,6 +607,13 @@ from .music_together_update_group_settings import (
599
607
  MusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsOutput,
600
608
  MusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsOutputGroup,
601
609
  )
610
+ from .push_queue_state import (
611
+ PushQueueState,
612
+ PushQueueStatePushQueueStatePushQueueStateError,
613
+ PushQueueStatePushQueueStatePushQueueStateErrorConflict,
614
+ PushQueueStatePushQueueStatePushQueueStateErrorConflictCurrent,
615
+ PushQueueStatePushQueueStatePushQueueStateOutput,
616
+ )
602
617
  from .remove_album_from_favorite import (
603
618
  RemoveAlbumFromFavorite,
604
619
  RemoveAlbumFromFavoriteRemoveAlbumFromFavorite,
@@ -1147,6 +1162,7 @@ __all__ = [
1147
1162
  "MarkAsPlayedPodcastEpisode",
1148
1163
  "MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisode",
1149
1164
  "MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisodeBookmark",
1165
+ "MediaTypeInput",
1150
1166
  "MoveTrackInPlaylist",
1151
1167
  "MoveTrackInPlaylistMoveTrackInPlaylist",
1152
1168
  "MusicTogetherCreateGroup",
@@ -1185,6 +1201,15 @@ __all__ = [
1185
1201
  "PodcastFields",
1186
1202
  "PodcastFieldsCover",
1187
1203
  "PodcastType",
1204
+ "PushQueueContextInput",
1205
+ "PushQueueMediaInput",
1206
+ "PushQueueState",
1207
+ "PushQueueStateClientDetailsInput",
1208
+ "PushQueueStateInput",
1209
+ "PushQueueStatePushQueueStatePushQueueStateError",
1210
+ "PushQueueStatePushQueueStatePushQueueStateErrorConflict",
1211
+ "PushQueueStatePushQueueStatePushQueueStateErrorConflictCurrent",
1212
+ "PushQueueStatePushQueueStatePushQueueStateOutput",
1188
1213
  "RemoveAlbumFromFavorite",
1189
1214
  "RemoveAlbumFromFavoriteRemoveAlbumFromFavorite",
1190
1215
  "RemoveAlbumFromFavoriteRemoveAlbumFromFavoriteAlbum",
@@ -1204,6 +1229,7 @@ __all__ = [
1204
1229
  "RemoveTrackFromFavoriteRemoveTrackFromFavoriteTrack",
1205
1230
  "RemoveTracksFromPlaylist",
1206
1231
  "RemoveTracksFromPlaylistRemoveTracksFromPlaylist",
1232
+ "RepeatModeInput",
1207
1233
  "Search",
1208
1234
  "SearchFlows",
1209
1235
  "SearchFlowsSearch",
@@ -13,6 +13,7 @@ from dataclasses import dataclass
13
13
  from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
14
14
 
15
15
  from aiohttp import ClientSession, ClientTimeout
16
+ from pydantic_core import to_jsonable_python
16
17
  from yarl import URL
17
18
 
18
19
  if TYPE_CHECKING:
@@ -229,8 +230,11 @@ class DeezerBaseClient:
229
230
  # Inline import avoids depending on generated code at module level.
230
231
  from deezer_python_gql.generated.base_model import UnsetType # noqa: PLC0415
231
232
 
233
+ # input types are pydantic models, sent under their GraphQL field names
232
234
  payload["variables"] = {
233
- k: v for k, v in variables.items() if not isinstance(v, UnsetType)
235
+ k: to_jsonable_python(v, by_alias=True)
236
+ for k, v in variables.items()
237
+ if not isinstance(v, UnsetType)
234
238
  }
235
239
 
236
240
  session = self._get_session()
@@ -286,11 +290,11 @@ class DeezerBaseClient:
286
290
  variables = response.variables
287
291
  if data:
288
292
  # Partial success — some items failed (e.g. deleted albums in favorites).
289
- # Log the errors but return the valid data.
293
+ # That is expected, so log at debug level and return the valid data.
290
294
  error_details = [
291
295
  f"{e.get('message', 'Unknown error')} (path={e.get('path')})" for e in errors
292
296
  ]
293
- logger.warning(
297
+ logger.debug(
294
298
  "GraphQL response for %s (variables=%s) contained %d error(s): %s",
295
299
  op,
296
300
  variables,
@@ -375,7 +379,8 @@ class DeezerBaseClient:
375
379
  text = await resp.text()
376
380
 
377
381
  if not is_success:
378
- if 400 <= status < 500:
382
+ # 429 is rate limiting, not a rejected ARL
383
+ if 400 <= status < 500 and status != 429:
379
384
  msg = f"ARL authentication rejected by auth.deezer.com (HTTP {status})"
380
385
  raise GraphQLClientAuthError(msg)
381
386
  raise GraphQLClientHttpError(
@@ -119,6 +119,7 @@ from .get_track_mix import GetTrackMix, GetTrackMixTrackMix
119
119
  from .get_tracks_by_ids import GetTracksByIds, GetTracksByIdsTracksByIds
120
120
  from .get_user_charts import GetUserCharts, GetUserChartsMe
121
121
  from .get_user_playlists import GetUserPlaylists, GetUserPlaylistsMe
122
+ from .input_types import PushQueueStateInput
122
123
  from .mark_as_not_played_podcast_episode import (
123
124
  MarkAsNotPlayedPodcastEpisode,
124
125
  MarkAsNotPlayedPodcastEpisodeMarkAsNotPlayedPodcastEpisode,
@@ -160,6 +161,11 @@ from .music_together_update_group_settings import (
160
161
  MusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsError,
161
162
  MusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsMusicTogetherUpdateGroupSettingsOutput,
162
163
  )
164
+ from .push_queue_state import (
165
+ PushQueueState,
166
+ PushQueueStatePushQueueStatePushQueueStateError,
167
+ PushQueueStatePushQueueStatePushQueueStateOutput,
168
+ )
163
169
  from .remove_album_from_favorite import (
164
170
  RemoveAlbumFromFavorite,
165
171
  RemoveAlbumFromFavoriteRemoveAlbumFromFavorite,
@@ -4469,6 +4475,37 @@ class DeezerGQLClient(DeezerBaseClient):
4469
4475
  data = self.get_data(response)
4470
4476
  return MoveTrackInPlaylist.model_validate(data).move_track_in_playlist
4471
4477
 
4478
+ async def push_queue_state(
4479
+ self, input: PushQueueStateInput, **kwargs: Any
4480
+ ) -> Union[
4481
+ PushQueueStatePushQueueStatePushQueueStateOutput,
4482
+ PushQueueStatePushQueueStatePushQueueStateError,
4483
+ ]:
4484
+ query = gql("""
4485
+ mutation PushQueueState($input: PushQueueStateInput!) {
4486
+ pushQueueState(input: $input) {
4487
+ __typename
4488
+ ... on PushQueueStateOutput {
4489
+ version
4490
+ }
4491
+ ... on PushQueueStateError {
4492
+ isBadRequest
4493
+ conflict {
4494
+ current {
4495
+ version
4496
+ }
4497
+ }
4498
+ }
4499
+ }
4500
+ }
4501
+ """)
4502
+ variables: dict[str, object] = {"input": input}
4503
+ response = await self.execute(
4504
+ query=query, operation_name="PushQueueState", variables=variables, **kwargs
4505
+ )
4506
+ data = self.get_data(response)
4507
+ return PushQueueState.model_validate(data).push_queue_state
4508
+
4472
4509
  async def ban_track_from_recommendation(
4473
4510
  self, track_id: str, **kwargs: Any
4474
4511
  ) -> BanTrackFromRecommendationBanTrackFromRecommendation:
@@ -46,6 +46,20 @@ class PodcastEpisodeOrder(str, Enum):
46
46
  LATEST = "LATEST"
47
47
 
48
48
 
49
+ class MediaTypeInput(str, Enum):
50
+ TRACK = "TRACK"
51
+ EPISODE = "EPISODE"
52
+ CHAPTER = "CHAPTER"
53
+ LIVE = "LIVE"
54
+ RAW_URL = "RAW_URL"
55
+
56
+
57
+ class RepeatModeInput(str, Enum):
58
+ NONE = "NONE"
59
+ ONE = "ONE"
60
+ ALL = "ALL"
61
+
62
+
49
63
  class DiscoveryTuner(str, Enum):
50
64
  DEFAULT = "DEFAULT"
51
65
  DISCOVERY = "DISCOVERY"
@@ -0,0 +1,62 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: schema.graphql
3
+
4
+ from typing import Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+ from .enums import MediaTypeInput, RepeatModeInput
10
+
11
+
12
+ class PushQueueStateInput(BaseModel):
13
+ client_details: "PushQueueStateClientDetailsInput" = Field(alias="clientDetails")
14
+ "Details about the client pushing the queue state"
15
+ media_list: list["PushQueueMediaInput"] = Field(alias="mediaList")
16
+ "List of all media in the queue in playing order"
17
+ position: int
18
+ "Zero based index of the current playback position in the queue"
19
+ progress_ms: Optional[int] = Field(alias="progressMS", default=None)
20
+ "Progress in the current media in ms"
21
+ previous_version: Optional[str] = Field(alias="previousVersion", default=None)
22
+ "Previous known queue state version\nNote: Except when there is no queue state available, this version MUST be provided otherwise you'll get a conflict error"
23
+ queue_configuration: str = Field(alias="queueConfiguration")
24
+ "Queue's configuration"
25
+ repeat_mode: RepeatModeInput = Field(alias="repeatMode")
26
+ "Queue repeat mode"
27
+ contexts: Optional[list["PushQueueContextInput"]] = None
28
+ "The list of unique contexts referenced by mediaList.contextId"
29
+
30
+
31
+ class PushQueueMediaInput(BaseModel):
32
+ """Details of media that belongs to the pushed queue"""
33
+
34
+ id: str
35
+ "The media id"
36
+ type_: MediaTypeInput = Field(alias="type")
37
+ "The media type"
38
+ context_id: Optional[str] = Field(alias="contextId", default=None)
39
+ 'The context identifier, it should correspond to an entry in "PushQueueStateInput.contexts"\nEach media can have its own context or share multiple time the same context by using the same contextId'
40
+ local_queue_item_id: Optional[str] = Field(alias="localQueueItemId", default=None)
41
+ "Item identifier used on client side"
42
+ unshuffled_position: int = Field(alias="unshuffledPosition")
43
+ "The position within the unshuffled version of the queue"
44
+
45
+
46
+ class PushQueueContextInput(BaseModel):
47
+ id: str
48
+ "Context identifier"
49
+ content: str
50
+ "The context content itself"
51
+
52
+
53
+ class PushQueueStateClientDetailsInput(BaseModel):
54
+ """Details about the client that wants to push the queue state"""
55
+
56
+ local_queue_id: str = Field(alias="localQueueID")
57
+ "Identifier of the client's local queue, doesn't necessarily match a real queue id"
58
+ locally_updated_at: str = Field(alias="locallyUpdatedAt")
59
+ "Date in ISO-8601 format including milliseconds when update was made on client"
60
+
61
+
62
+ PushQueueStateInput.model_rebuild()
@@ -0,0 +1,39 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: queries
3
+
4
+ from typing import Literal, Optional, Union
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+
10
+
11
+ class PushQueueState(BaseModel):
12
+ push_queue_state: Union[
13
+ "PushQueueStatePushQueueStatePushQueueStateOutput",
14
+ "PushQueueStatePushQueueStatePushQueueStateError",
15
+ ] = Field(alias="pushQueueState", discriminator="typename__")
16
+
17
+
18
+ class PushQueueStatePushQueueStatePushQueueStateOutput(BaseModel):
19
+ typename__: Literal["PushQueueStateOutput"] = Field(alias="__typename")
20
+ version: str
21
+
22
+
23
+ class PushQueueStatePushQueueStatePushQueueStateError(BaseModel):
24
+ typename__: Literal["PushQueueStateError"] = Field(alias="__typename")
25
+ is_bad_request: bool = Field(alias="isBadRequest")
26
+ conflict: Optional["PushQueueStatePushQueueStatePushQueueStateErrorConflict"]
27
+
28
+
29
+ class PushQueueStatePushQueueStatePushQueueStateErrorConflict(BaseModel):
30
+ current: Optional["PushQueueStatePushQueueStatePushQueueStateErrorConflictCurrent"]
31
+
32
+
33
+ class PushQueueStatePushQueueStatePushQueueStateErrorConflictCurrent(BaseModel):
34
+ version: str
35
+
36
+
37
+ PushQueueState.model_rebuild()
38
+ PushQueueStatePushQueueStatePushQueueStateError.model_rebuild()
39
+ PushQueueStatePushQueueStatePushQueueStateErrorConflict.model_rebuild()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deezer-python-gql
3
- Version: 0.21.0
3
+ Version: 0.22.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
@@ -16,13 +16,13 @@ Requires-Dist: pydantic>=2.0.0
16
16
  Requires-Dist: yarl>=1.22.0
17
17
  Provides-Extra: test
18
18
  Requires-Dist: codespell==2.4.3; extra == "test"
19
- Requires-Dist: mypy==2.4.0; extra == "test"
19
+ Requires-Dist: mypy==2.4.1; extra == "test"
20
20
  Requires-Dist: pre-commit==4.7.0; extra == "test"
21
21
  Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
22
- Requires-Dist: pytest==9.1.1; extra == "test"
23
- Requires-Dist: pytest-asyncio==1.4.1; extra == "test"
22
+ Requires-Dist: pytest==9.2.0; extra == "test"
23
+ Requires-Dist: pytest-asyncio==1.4.1.post1; extra == "test"
24
24
  Requires-Dist: pytest-cov==7.1.0; extra == "test"
25
- Requires-Dist: ruff==0.17.2; extra == "test"
25
+ Requires-Dist: ruff==0.17.3; extra == "test"
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
28
28
  Dynamic: license-file
@@ -83,6 +83,7 @@ deezer_python_gql/generated/music_together_join_group.py
83
83
  deezer_python_gql/generated/music_together_leave_group.py
84
84
  deezer_python_gql/generated/music_together_refresh_suggested_tracklist.py
85
85
  deezer_python_gql/generated/music_together_update_group_settings.py
86
+ deezer_python_gql/generated/push_queue_state.py
86
87
  deezer_python_gql/generated/remove_album_from_favorite.py
87
88
  deezer_python_gql/generated/remove_artist_from_favorite.py
88
89
  deezer_python_gql/generated/remove_audiobook_from_favorite.py
@@ -7,10 +7,10 @@ ariadne-codegen[subscriptions]
7
7
 
8
8
  [test]
9
9
  codespell==2.4.3
10
- mypy==2.4.0
10
+ mypy==2.4.1
11
11
  pre-commit==4.7.0
12
12
  pre-commit-hooks==6.0.0
13
- pytest==9.1.1
14
- pytest-asyncio==1.4.1
13
+ pytest==9.2.0
14
+ pytest-asyncio==1.4.1.post1
15
15
  pytest-cov==7.1.0
16
- ruff==0.17.2
16
+ ruff==0.17.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deezer-python-gql"
3
- version = "0.21.0"
3
+ version = "0.22.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"
@@ -24,13 +24,13 @@ text = "Apache-2.0"
24
24
  [project.optional-dependencies]
25
25
  test = [
26
26
  "codespell==2.4.3",
27
- "mypy==2.4.0",
27
+ "mypy==2.4.1",
28
28
  "pre-commit==4.7.0",
29
29
  "pre-commit-hooks==6.0.0",
30
- "pytest==9.1.1",
31
- "pytest-asyncio==1.4.1",
30
+ "pytest==9.2.0",
31
+ "pytest-asyncio==1.4.1.post1",
32
32
  "pytest-cov==7.1.0",
33
- "ruff==0.17.2",
33
+ "ruff==0.17.3",
34
34
  ]
35
35
  dev = [
36
36
  "ariadne-codegen[subscriptions]",
@@ -182,5 +182,5 @@ asyncio_mode = "auto"
182
182
 
183
183
  [tool.uv]
184
184
  override-dependencies = [
185
- "ruff==0.17.2",
185
+ "ruff==0.17.3",
186
186
  ]
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import asyncio
13
13
  import base64
14
14
  import json
15
+ import logging
15
16
  import time
16
17
  from http.cookies import SimpleCookie
17
18
  from pathlib import Path
@@ -48,7 +49,7 @@ from deezer_python_gql.generated.ban_track_from_recommendation import (
48
49
  from deezer_python_gql.generated.bookmark_podcast_episode import BookmarkPodcastEpisode
49
50
  from deezer_python_gql.generated.create_playlist import CreatePlaylist
50
51
  from deezer_python_gql.generated.delete_playlist import DeletePlaylist
51
- from deezer_python_gql.generated.enums import PodcastType
52
+ from deezer_python_gql.generated.enums import MediaTypeInput, PodcastType, RepeatModeInput
52
53
  from deezer_python_gql.generated.get_album import GetAlbum
53
54
  from deezer_python_gql.generated.get_artist import GetArtist
54
55
  from deezer_python_gql.generated.get_artist_mix import GetArtistMix
@@ -99,6 +100,11 @@ from deezer_python_gql.generated.get_track_mix import GetTrackMix
99
100
  from deezer_python_gql.generated.get_tracks_by_ids import GetTracksByIds
100
101
  from deezer_python_gql.generated.get_user_charts import GetUserCharts
101
102
  from deezer_python_gql.generated.get_user_playlists import GetUserPlaylists
103
+ from deezer_python_gql.generated.input_types import (
104
+ PushQueueMediaInput,
105
+ PushQueueStateClientDetailsInput,
106
+ PushQueueStateInput,
107
+ )
102
108
  from deezer_python_gql.generated.mark_as_not_played_podcast_episode import (
103
109
  MarkAsNotPlayedPodcastEpisode,
104
110
  )
@@ -114,6 +120,11 @@ from deezer_python_gql.generated.music_together_generate_group_name import (
114
120
  )
115
121
  from deezer_python_gql.generated.music_together_join_group import MusicTogetherJoinGroup
116
122
  from deezer_python_gql.generated.music_together_leave_group import MusicTogetherLeaveGroup
123
+ from deezer_python_gql.generated.push_queue_state import (
124
+ PushQueueState,
125
+ PushQueueStatePushQueueStatePushQueueStateError,
126
+ PushQueueStatePushQueueStatePushQueueStateOutput,
127
+ )
117
128
  from deezer_python_gql.generated.remove_album_from_favorite import RemoveAlbumFromFavorite
118
129
  from deezer_python_gql.generated.remove_artist_from_favorite import RemoveArtistFromFavorite
119
130
  from deezer_python_gql.generated.remove_audiobook_from_favorite import (
@@ -378,6 +389,7 @@ def test_client_has_generated_methods() -> None:
378
389
  "get_latest_podcast_episodes",
379
390
  "get_tracks_by_ids",
380
391
  "set_flow_discovery_tuner",
392
+ "push_queue_state",
381
393
  ]
382
394
  for method in expected_methods:
383
395
  assert hasattr(client, method), f"Missing method: {method}"
@@ -637,10 +649,11 @@ async def test_auth_rejected_arl_raises_auth_error() -> None:
637
649
 
638
650
 
639
651
  @pytest.mark.asyncio
640
- async def test_auth_server_error_raises_http_error() -> None:
641
- """Verify a 5xx from auth.deezer.com raises GraphQLClientHttpError, not AuthError."""
652
+ @pytest.mark.parametrize("status", [429, 503])
653
+ async def test_auth_server_error_raises_http_error(status: int) -> None:
654
+ """Verify a 5xx or 429 from auth.deezer.com raises GraphQLClientHttpError, not AuthError."""
642
655
  client = DeezerBaseClient(arl="test_arl")
643
- auth_cm = _mock_post_context_manager(b"Service Unavailable", status=503)
656
+ auth_cm = _mock_post_context_manager(b"Service Unavailable", status=status)
644
657
 
645
658
  with patch("deezer_python_gql.base_client.ClientSession") as mock_cls:
646
659
  mock_instance = MagicMock()
@@ -650,7 +663,7 @@ async def test_auth_server_error_raises_http_error() -> None:
650
663
 
651
664
  with pytest.raises(GraphQLClientHttpError) as exc_info:
652
665
  await client._ensure_jwt() # noqa: SLF001
653
- assert exc_info.value.status_code == 503
666
+ assert exc_info.value.status_code == status
654
667
 
655
668
 
656
669
  @pytest.mark.asyncio
@@ -773,6 +786,25 @@ def test_get_data_raises_on_missing_data_key() -> None:
773
786
  client.get_data(response)
774
787
 
775
788
 
789
+ def test_get_data_logs_partial_errors_at_debug(caplog: pytest.LogCaptureFixture) -> None:
790
+ """Verify items Deezer no longer has are logged at debug level, not as a warning."""
791
+ client = DeezerBaseClient(arl="test")
792
+ response = GQLResponse(
793
+ status=200,
794
+ data=json.dumps(
795
+ {
796
+ "data": {"me": {"id": "1"}},
797
+ "errors": [{"message": "PodcastEpisode does not exist", "path": ["me"]}],
798
+ }
799
+ ).encode(),
800
+ is_success=True,
801
+ )
802
+
803
+ with caplog.at_level(logging.DEBUG, logger="deezer_python_gql"):
804
+ assert client.get_data(response) == {"me": {"id": "1"}}
805
+ assert [record.levelno for record in caplog.records] == [logging.DEBUG]
806
+
807
+
776
808
  def test_get_data_raises_on_graphql_errors() -> None:
777
809
  """Verify get_data raises GraphQLClientGraphQLMultiError for GraphQL-level errors."""
778
810
  client = DeezerBaseClient(arl="test")
@@ -2030,3 +2062,64 @@ def test_smoke_get_family() -> None:
2030
2062
  ("2002", True),
2031
2063
  ("3003", False),
2032
2064
  ]
2065
+
2066
+
2067
+ def test_smoke_push_queue_state() -> None:
2068
+ """Verify PushQueueState fixture parses the new queue state version."""
2069
+ data = _load_fixture("push_queue_state.json")
2070
+ result = PushQueueState.model_validate(data).push_queue_state
2071
+ assert isinstance(result, PushQueueStatePushQueueStatePushQueueStateOutput)
2072
+ assert result.version == "01a1a613-c68c-731f-8677-15d9c535adbb"
2073
+
2074
+
2075
+ def test_smoke_push_queue_state_conflict() -> None:
2076
+ """Verify a PushQueueState conflict parses the version it conflicts with."""
2077
+ data = _load_fixture("push_queue_state_conflict.json")
2078
+ result = PushQueueState.model_validate(data).push_queue_state
2079
+ assert isinstance(result, PushQueueStatePushQueueStatePushQueueStateError)
2080
+ assert result.is_bad_request is False
2081
+ assert result.conflict is not None
2082
+ assert result.conflict.current is not None
2083
+ assert result.conflict.current.version == "01a1a63d-1f5b-7d29-ae26-099eb9c20ab7"
2084
+
2085
+
2086
+ @pytest.mark.asyncio
2087
+ async def test_execute_sends_input_types_by_their_graphql_names() -> None:
2088
+ """Verify an input type variable is sent as JSON with the API's field names."""
2089
+ client = DeezerGQLClient(arl="test_arl")
2090
+ client._jwt = _make_jwt(exp=time.time() + 600) # noqa: SLF001
2091
+ client._jwt_expires_at = time.time() + 600 # noqa: SLF001
2092
+ state = PushQueueStateInput(
2093
+ clientDetails=PushQueueStateClientDetailsInput(
2094
+ localQueueID="queue", locallyUpdatedAt="2026-11-04T10:21:16.223Z"
2095
+ ),
2096
+ mediaList=[PushQueueMediaInput(id="1", type=MediaTypeInput.TRACK, unshuffledPosition=0)],
2097
+ position=0,
2098
+ queueConfiguration="{}",
2099
+ repeatMode=RepeatModeInput.NONE,
2100
+ )
2101
+ gql_cm = _mock_post_context_manager(
2102
+ json.dumps({"data": _load_fixture("push_queue_state.json")}).encode()
2103
+ )
2104
+
2105
+ # the generated client runs on the generated copy of the base client
2106
+ with patch("deezer_python_gql.generated.base_client.ClientSession") as mock_cls:
2107
+ mock_instance = MagicMock()
2108
+ mock_instance.post = MagicMock(return_value=gql_cm)
2109
+ mock_instance.close = AsyncMock()
2110
+ mock_cls.return_value = mock_instance
2111
+
2112
+ await client.push_queue_state(input=state)
2113
+
2114
+ sent = mock_instance.post.call_args.kwargs["json"]["variables"]["input"]
2115
+ assert sent["clientDetails"]["localQueueID"] == "queue"
2116
+ assert sent["mediaList"] == [
2117
+ {
2118
+ "id": "1",
2119
+ "type": "TRACK",
2120
+ "contextId": None,
2121
+ "localQueueItemId": None,
2122
+ "unshuffledPosition": 0,
2123
+ }
2124
+ ]
2125
+ assert sent["repeatMode"] == "NONE"
@@ -1,3 +0,0 @@
1
- # Generated by ariadne-codegen
2
- # Source: schema.graphql
3
-