Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e11e8c1
feat: add async Deezer client in deezer.asyncio sub-package
claude Apr 3, 2026
c69691e
refactor: move async tests to tests/asyncio/ to mirror source structure
claude Apr 3, 2026
a836ff7
refactor: simplify async tests to use pytest.mark.vcr auto-discovery
claude Apr 3, 2026
74ef87b
feat: add async resources with AsyncResource, AsyncAlbum, and AsyncAr…
claude Apr 4, 2026
5e93fa7
feat: add async Artist resource with relation methods
claude Apr 4, 2026
36025ce
feat: add async Track resource with get_artist/get_album methods
claude Apr 4, 2026
65b4458
feat: add async Genre resource with get_artists/get_radios/get_podcasts
claude Apr 4, 2026
4b64f20
feat: add async Radio resource with get_tracks method
claude Apr 4, 2026
11ad239
feat: add async Podcast resource with get_episodes method
claude Apr 4, 2026
ae62a22
feat: add async Episode resource with add/remove_bookmark methods
claude Apr 4, 2026
ae0aa29
feat: add async Editorial resource with get_selection/get_chart/get_r…
claude Apr 4, 2026
c362306
feat: add async Chart resource with get_tracks/albums/artists/playlis…
claude Apr 4, 2026
8c6a974
feat: add async Playlist resource with relation methods
claude Apr 4, 2026
daedaf6
feat: add async User resource with relation methods
claude Apr 4, 2026
fc7e85b
chore: remove unused cassette files for async user tests
claude Apr 4, 2026
f814eea
feat: add AsyncPaginatedList for async paginated API responses
claude Apr 4, 2026
cb06441
feat: wire up get_paginated_list on AsyncResource and AsyncClient
claude Apr 4, 2026
004aaf0
refactor: clean up imports in async sub-package
claude Apr 4, 2026
d37ac83
refactor: return AsyncPaginatedList from async resource relation methods
claude Apr 4, 2026
c57dc44
feat: add search, list, and user convenience methods to AsyncClient
claude Apr 4, 2026
c51fd2a
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Apr 4, 2026
b5ec91a
chore: remove version restriction for pytest-asyncio
claude Apr 4, 2026
5cf2c45
fix: resolve ty type check errors
claude Apr 4, 2026
fa3bfae
fix: suppress S106 lint warning in async client test
claude Apr 4, 2026
3cd2628
test: add async client tests for error response, current user, user l…
claude Apr 4, 2026
82f8f67
test: add AsyncPaginatedList repr and __anext__ StopAsyncIteration tests
claude Apr 4, 2026
f2f5ee8
test: add async tests for editorial get_chart, genre get_podcasts, pl…
claude Apr 4, 2026
5ccb5a5
test: cover remaining branch gaps in async client and pagination
claude Apr 4, 2026
46d11c6
test: add missing cassette for anext stop iteration test
claude Apr 4, 2026
4a292dd
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Apr 4, 2026
b29cc12
docs: add async client usage guide and API reference pages
claude Apr 4, 2026
a78545e
refactor: make async paginated list methods awaitable with eager firs…
claude Apr 4, 2026
ba7e927
test: update async tests for awaitable paginated list methods
claude Apr 4, 2026
1c8f2ff
refactor: simplify AsyncPaginatedList after eager first page fetch
claude Apr 4, 2026
3e27d6f
chore: apply pre-commit auto-fixes
claude Apr 4, 2026
d8b48e7
test: cover edge case where _grow returns empty in async pagination
claude Apr 4, 2026
461f16b
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Apr 4, 2026
733c718
refactor: make total a property and replace length with __len__
claude Apr 6, 2026
a67ef6f
refactor: use double-underscore private attributes in AsyncPaginatedList
claude Apr 6, 2026
c67fcdf
docs: note that async pagination does not support [] syntax
claude Apr 6, 2026
7b40561
chore: add 'watch' target to docs
browniebroke Apr 6, 2026
612572e
docs: fix note formatting
browniebroke Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ BUILDDIR = _build
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

# Build, watch and serve docs with live reload
watch:
sphinx-autobuild -b html -c . $(SOURCEDIR) $(BUILDDIR)/html

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
Expand Down
7 changes: 7 additions & 0 deletions docs/api_reference/async_client.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Async Client
------------

The async client class is the main entry point to start querying the `Deezer API <https://developers.deezer.com/api>`_ using ``async``/``await``.

.. autoclass:: deezer.asyncio.AsyncClient
:members:
7 changes: 7 additions & 0 deletions docs/api_reference/async_pagination.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.. _async-pagination-reference:

Async Pagination
----------------

.. autoclass:: deezer.asyncio.AsyncPaginatedList
:members:
21 changes: 21 additions & 0 deletions docs/api_reference/async_resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
(async-resources-reference)=

# Async Resources

A collection of async Python classes modelling each type of content that is returned by the Deezer API when using the {class}`~deezer.asyncio.AsyncClient`.

```{toctree}
:maxdepth: 1

async_resources/album
async_resources/artist
async_resources/chart
async_resources/editorial
async_resources/episode
async_resources/genre
async_resources/playlist
async_resources/podcast
async_resources/radio
async_resources/track
async_resources/user
```
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/album.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncAlbum
----------

.. autoclass:: deezer.asyncio.AsyncAlbum
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/artist.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncArtist
-----------

.. autoclass:: deezer.asyncio.AsyncArtist
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/chart.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncChart
----------

.. autoclass:: deezer.asyncio.AsyncChart
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/editorial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncEditorial
--------------

.. autoclass:: deezer.asyncio.AsyncEditorial
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/episode.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncEpisode
------------

.. autoclass:: deezer.asyncio.AsyncEpisode
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/genre.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncGenre
----------

.. autoclass:: deezer.asyncio.AsyncGenre
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/playlist.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncPlaylist
-------------

.. autoclass:: deezer.asyncio.AsyncPlaylist
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/podcast.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncPodcast
------------

.. autoclass:: deezer.asyncio.AsyncPodcast
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/radio.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncRadio
----------

.. autoclass:: deezer.asyncio.AsyncRadio
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/track.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncTrack
----------

.. autoclass:: deezer.asyncio.AsyncTrack
:members:
:undoc-members:
6 changes: 6 additions & 0 deletions docs/api_reference/async_resources/user.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AsyncUser
---------

.. autoclass:: deezer.asyncio.AsyncUser
:members:
:undoc-members:
3 changes: 3 additions & 0 deletions docs/api_reference/toc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ This is the auto-generated documentation for all the main modules in the library
client
pagination
resources
async_client
async_pagination
async_resources
126 changes: 126 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
(async-guide)=

# Async Client

If you are working with an async framework like [aiohttp](https://docs.aiohttp.org/) or [FastAPI](https://fastapi.tiangolo.com/), you can use the async client from the {mod}`deezer.asyncio` sub-package. It provides the same functionality as the synchronous {class}`Client <deezer.Client>`, but with `async`/`await` support.

## Getting started

The async client should be used as an async context manager:

```python
from deezer.asyncio import AsyncClient

async with AsyncClient() as client:
album = await client.get_album(302127)
print(album.title)
```

As with the sync client, you can pass an `access_token` for authenticated requests:

```python
async with AsyncClient(access_token="your-token") as client:
user = await client.get_user()
print(user.name)
```

## Fetching resources

All the methods that fetch resources from the Deezer API are coroutines and need to be awaited:

```python
async with AsyncClient() as client:
artist = await client.get_artist(27)
album = await client.get_album(302127)
track = await client.get_track(3135556)
```

The returned objects are async resource classes (e.g. {class}`~deezer.asyncio.AsyncArtist`, {class}`~deezer.asyncio.AsyncAlbum`), which mirror their sync counterparts.

## Navigating relationships

Relationship methods that return a single resource or a plain list need to be awaited:

```python
async with AsyncClient() as client:
track = await client.get_track(3135556)

# These return a single resource
artist = await track.get_artist()
album = await track.get_album()

# This returns a plain list
radio = await artist.get_radio()
```

Methods that return paginated responses also need to be awaited. The first page of results is fetched eagerly, making the returned {class}`~deezer.asyncio.AsyncPaginatedList` immediately usable:

```python
async with AsyncClient() as client:
artist = await client.get_artist(27)

# Await to get the paginated list (first page is fetched)
albums = await artist.get_albums()
```

## Async Pagination

For endpoints returning paginated responses, items are wrapped in an {class}`~deezer.asyncio.AsyncPaginatedList`. This works similarly to the synchronous {class}`~deezer.PaginatedList` described in the {ref}`pagination guide <pagination-guide>`, but uses async iteration and awaitable methods.

### Iterating over elements

Use `async for` to iterate over all elements, transparently fetching additional pages as needed:

```python
albums = await artist.get_albums()

async for album in albums:
print(album.title)
```

### Total number

The total number of items is available via the {attr}`~deezer.asyncio.AsyncPaginatedList.total` property. Since the first page is fetched eagerly, the total is already available without any extra API call:

```python
total = albums.total
# Or using len():
total = len(albums)
```

### Accessing elements by index

Use the {meth}`~deezer.asyncio.AsyncPaginatedList.get` method to access an element by index:

```python
first_album = await albums.get(0)
fifth_album = await albums.get(4)
```

As with the sync version, accessing a large index may trigger extra API calls to fetch preceding pages.

```{note}
Unlike the synchronous {class}`~deezer.PaginatedList`, the async version does not support the `[]` syntax for indexing or slicing. Use the {meth}`~deezer.asyncio.AsyncPaginatedList.get` method for index-based access, or {meth}`~deezer.asyncio.AsyncPaginatedList.collect` to get a plain list that supports regular slicing.
```

### Collecting all elements

To fetch all pages and get a plain list, use {meth}`~deezer.asyncio.AsyncPaginatedList.collect`:

```python
all_albums = await albums.collect()
```

### Search

Search methods also return {class}`~deezer.asyncio.AsyncPaginatedList` and need to be awaited:

```python
async with AsyncClient() as client:
results = await client.search("Daft Punk")

async for track in results:
print(track.title)
```

Variant search methods are also available: {meth}`~deezer.asyncio.AsyncClient.search_albums`, {meth}`~deezer.asyncio.AsyncClient.search_artists`, and {meth}`~deezer.asyncio.AsyncClient.search_playlists`.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Deezer-Python: a friendly wrapper around the [Deezer API](http://developers.deez
installation
usage
pagination
async
```

```{toctree}
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dev = [
"deezer-oauth-cli>=1,<2",
"environs>=15,<16",
"pytest>=9,<10",
"pytest-asyncio",
"pytest-cov>=7,<8",
"pytest-mock>=3.6,<4",
"pytest-vcr>=1,<2",
Expand Down
89 changes: 89 additions & 0 deletions src/deezer/_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from typing import Any, ClassVar

from deezer.exceptions import DeezerUnknownResource
from deezer.resources import (
Album,
Artist,
Chart,
Editorial,
Episode,
Genre,
Playlist,
Podcast,
Radio,
Resource,
Track,
User,
)


class DeezerMixin:
"""Mixin providing shared logic for both sync and async Deezer clients."""

_resource_base_class: ClassVar[type[Resource]] = Resource

objects_types: ClassVar[dict[str, type[Resource] | None]] = {
"album": Album,
"artist": Artist,
"chart": Chart,
"editorial": Editorial,
"episode": Episode,
"genre": Genre,
"playlist": Playlist,
"podcast": Podcast,
"radio": Radio,
"search": None,
"track": Track,
"user": User,
}

def _process_json(
self,
item: dict[str, Any],
parent: Any = None,
resource_type: type[Any] | None = None,
resource_id: int | None = None,
paginate_list=False,
):
"""
Recursively convert dictionary to :class:`~deezer.Resource` object.

:param item: the JSON response as dict.
:param parent: A reference to the parent resource, to avoid fetching again.
:param resource_type: The resource class to use as top level.
:param resource_id: The resource id to use as top level.
:param paginate_list: Whether to wrap list into a pagination object.
:returns: instance of :class:`~deezer.Resource`
"""
if "data" in item:
parsed_data = [self._process_json(i, parent, paginate_list=False) for i in item["data"]]
if not paginate_list:
return parsed_data
item["data"] = parsed_data
return item

result = {}
for key, value in item.items():
if isinstance(value, dict) and ("type" in value or "data" in value):
value = self._process_json(value, parent)
result[key] = value
if parent is not None:
result[parent.type] = parent

if "id" not in result and resource_id is not None:
result["id"] = resource_id

if "type" in result and result["type"] in self.objects_types:
object_class = self.objects_types[result["type"]]
elif "type" in result or (not resource_type and "id" in result):
object_class = self._resource_base_class
elif resource_type:
object_class = resource_type
elif item.get("results") is True:
return True
else:
raise DeezerUnknownResource(f"Unable to find resource type for {result!r}")
assert object_class is not None # noqa S101
return object_class(self, result)
Loading
Loading