Skip to content

Conversation

@OkoyaUsman
Copy link

@OkoyaUsman OkoyaUsman commented Feb 10, 2026

Currently using this on a project and Async is super important to me, took some time to write the async module.

Description of change

Implementation of #1339

This PR adds an async version of the Deezer Python client, available alongside the existing synchronous client.

What's New

  • New async_deezer package under src/async_deezer/:
  • AsyncClient — async HTTP client using httpx.AsyncClient
  • AsyncPaginatedList — async pagination with async for iteration
  • Async resource classes — async versions of Album, Artist, Track, User, etc., with async relation methods

Features

  • All network operations are async and non-blocking
  • Async pagination with async for iteration
  • Async resource methods (e.g., await album.get_artist(), album.get_tracks() returns AsyncPaginatedList)
  • Same API shape as the sync client for easy migration

Pull-Request Checklist

  • Code is up-to-date with the main branch
  • This pull request follows the contributing guidelines.
  • This pull request links relevant issues as Improves #1339
  • There are new or updated unit tests validating the change
  • Documentation has been updated to reflect this change
  • The new commits follow conventions outlined in the conventional commit spec, such as "fix(api): prevent racing of requests".
  • If pre-commit.ci is failing, try pre-commit run -a for further information.
  • If CI / test is failing, try poetry run pytest for further information.

Summary by Sourcery

Add an asynchronous Deezer client alongside the existing synchronous client, exposing async resource APIs and pagination while documenting how to use them.

New Features:

  • Introduce async_deezer.AsyncClient built on httpx.AsyncClient with async equivalents of the main Deezer API methods.
  • Add AsyncPaginatedList to support non-blocking async iteration and random access over paginated endpoints.
  • Provide async resource classes (Album, Artist, Track, User, Playlist, Podcast, Episode, Radio, Genre, Editorial, Chart) mirroring the synchronous resource API.

Documentation:

  • Document async client usage, including async context management and pagination with AsyncPaginatedList, in the README and usage guide.

Currently using this on a project and Async is super important to me, took some time to write the async module.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 10, 2026

Reviewer's Guide

Introduces a new async client package async_deezer that mirrors the existing synchronous Deezer client using httpx.AsyncClient, adds async-aware pagination and async resource models, and updates documentation to describe async usage and pagination patterns.

Sequence diagram for async pagination with AsyncPaginatedList

sequenceDiagram
    actor Developer
    participant AsyncClient
    participant AsyncPaginatedList
    participant httpx_AsyncClient as httpx_AsyncClient
    participant DeezerAPI

    Developer->>AsyncClient: get_user_tracks(user_id)
    AsyncClient->>AsyncClient: _get_paginated_list("user/{id}/tracks")
    AsyncClient-->>Developer: AsyncPaginatedList

    Developer->>AsyncPaginatedList: async for track in tracks
    loop iterate_loaded_elements
        AsyncPaginatedList-->>Developer: yield cached track
    end

    alt more_pages_available
        AsyncPaginatedList->>AsyncPaginatedList: _could_grow()
        AsyncPaginatedList->>AsyncPaginatedList: _grow()
        AsyncPaginatedList->>AsyncPaginatedList: _fetch_next_page()
        AsyncPaginatedList->>AsyncClient: request("GET", next_path, params)
        AsyncClient->>httpx_AsyncClient: request("GET", next_path, params)
        httpx_AsyncClient->>DeezerAPI: HTTP GET next_page_url
        DeezerAPI-->>httpx_AsyncClient: JSON page payload
        httpx_AsyncClient-->>AsyncClient: httpx.Response
        AsyncClient->>AsyncClient: response.raise_for_status()
        AsyncClient->>AsyncClient: _process_json(json_data, paginate_list=True)
        AsyncClient-->>AsyncPaginatedList: list of ResourceType
        AsyncPaginatedList->>AsyncPaginatedList: extend _elements
        AsyncPaginatedList-->>Developer: yield new track
    end
Loading

Class diagram for async_deezer core types

classDiagram
    class AsyncClient {
        +dict~str, type Resource or None~ objects_types
        +AsyncClient(access_token: str, headers: HeaderTypes)
        +request(method: str, path: str, parent: Resource, resource_type: type Resource, resource_id: int, paginate_list: bool, kwargs: Any)
        +get_album(album_id: int) Album
        +get_artist(artist_id: int) Artist
        +get_chart(genre_id: int) Chart
        +get_tracks_chart(genre_id: int) list~Track~
        +get_albums_chart(genre_id: int) list~Album~
        +get_artists_chart(genre_id: int) list~Artist~
        +get_playlists_chart(genre_id: int) list~Playlist~
        +get_podcasts_chart(genre_id: int) list~Podcast~
        +get_editorial(editorial_id: int) Editorial
        +list_editorials() AsyncPaginatedList~Editorial~
        +get_episode(episode_id: int) Episode
        +get_genre(genre_id: int) Genre
        +list_genres() list~Genre~
        +get_playlist(playlist_id: int) Playlist
        +get_podcast(podcast_id: int) Podcast
        +get_radio(radio_id: int) Radio
        +list_radios() list~Radio~
        +get_radios_top() AsyncPaginatedList~Radio~
        +get_track(track_id: int) Track
        +get_user(user_id: int) User
        +get_user_recommended_tracks(kwargs: Any) AsyncPaginatedList~Track~
        +get_user_recommended_albums(kwargs: Any) AsyncPaginatedList~Album~
        +get_user_recommended_artists(kwargs: Any) AsyncPaginatedList~Artist~
        +get_user_recommended_playlists(kwargs: Any) AsyncPaginatedList~Playlist~
        +get_user_flow(kwargs: Any) AsyncPaginatedList~Track~
        +get_user_albums(user_id: int) AsyncPaginatedList~Album~
        +add_user_album(album_id: int) bool
        +remove_user_album(album_id: int) bool
        +get_user_artists(user_id: int) AsyncPaginatedList~Artist~
        +add_user_artist(artist_id: int) bool
        +remove_user_artist(artist_id: int) bool
        +get_user_followers(user_id: int) AsyncPaginatedList~User~
        +get_user_followings(user_id: int) AsyncPaginatedList~User~
        +add_user_following(user_id: int) bool
        +remove_user_following(user_id: int) bool
        +get_user_history() AsyncPaginatedList~Track~
        +get_user_tracks(user_id: int) AsyncPaginatedList~Track~
        +add_user_track(track_id: int) bool
        +remove_user_track(track_id: int) bool
        +remove_user_playlist(playlist_id: int) bool
        +add_user_playlist(playlist_id: int) bool
        +create_playlist(playlist_name: str) int
        +delete_playlist(playlist_id: int) bool
        +search(query: str, strict: bool, ordering: str, artist: str, album: str, track: str, label: str, dur_min: int, dur_max: int, bpm_min: int, bpm_max: int) AsyncPaginatedList~Track~
        +search_albums(query: str, strict: bool, ordering: str) AsyncPaginatedList~Album~
        +search_artists(query: str, strict: bool, ordering: str) AsyncPaginatedList~Artist~
        +search_playlists(query: str, strict: bool, ordering: str) AsyncPaginatedList~Playlist~
    }

    class httpx_AsyncClient {
    }

    AsyncClient --|> httpx_AsyncClient

    class AsyncPaginatedList~ResourceType~ {
        -list~ResourceType~ _elements
        -AsyncClient _client
        -str _base_path
        -dict _base_params
        -str _next_path
        -dict _next_params
        -Resource _parent
        -int _total
        +AsyncPaginatedList(client: AsyncClient, base_path: str, parent: Resource, params: dict)
        +__aiter__() AsyncGenerator~ResourceType, None~
        +aget(index: int) ResourceType
        +aslice(start: int, stop: int) list~ResourceType~
        +get_total() int
    }

    class Resource {
        +int id
        +str type
        -tuple~str~ _fields
        +Resource(client: AsyncClient, json: dict~str, Any~)
        +as_dict() dict~str, Any~
        +get_relation(relation: str, resource_type: type Resource, params: dict, fwd_parent: bool)
        +post_relation(relation: str, params: dict)
        +delete_relation(relation: str, params: dict)
        +get_paginated_list(relation: str, params: dict) AsyncPaginatedList
        +get()
    }

    AsyncClient o--> Resource : creates
    AsyncClient ..> AsyncPaginatedList : returns
    Resource ..> AsyncPaginatedList : returns
    AsyncPaginatedList --> AsyncClient : uses

    class Album {
        +int id
        +str title
        +str upc
        +str link
        +str share
        +str cover
        +str cover_small
        +str cover_medium
        +str cover_big
        +str cover_xl
        +str md5_image
        +int genre_id
        +list~Genre~ genres
        +str label
        +int nb_tracks
        +int duration
        +int fans
        +dt_date release_date
        +str record_type
        +bool available
        +Album alternative
        +str tracklist
        +bool explicit_lyrics
        +int explicit_content_lyrics
        +int explicit_content_cover
        +list~Artist~ contributors
        +Artist artist
        +list~Track~ tracks
        +get_artist() Artist
        +get_tracks(kwargs: Any) AsyncPaginatedList~Track~
    }

    class Artist {
        +int id
        +str name
        +str link
        +str share
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +int nb_album
        +int nb_fan
        +bool radio
        +str tracklist
        +get_top(kwargs: Any) AsyncPaginatedList~Track~
        +get_related(kwargs: Any) AsyncPaginatedList~Artist~
        +get_radio(kwargs: Any) list~Track~
        +get_albums(kwargs: Any) AsyncPaginatedList~Album~
        +get_playlists(kwargs: Any) AsyncPaginatedList~Playlist~
    }

    class Track {
        +int id
        +bool readable
        +str title
        +str title_short
        +str title_version
        +bool unseen
        +str isrc
        +str link
        +str share
        +int duration
        +int track_position
        +int disk_number
        +int rank
        +dt_date release_date
        +bool explicit_lyrics
        +int explicit_content_lyrics
        +int explicit_content_cover
        +str preview
        +float bpm
        +float gain
        +list~str~ available_countries
        +Track alternative
        +list~Artist~ contributors
        +str md5_image
        +Artist artist
        +Album album
        +get_artist() Artist
        +get_album() Album
    }

    class User {
        +int id
        +str name
        +str lastname
        +str firstname
        +str email
        +int status
        +dt_date birthday
        +dt_date inscription_date
        +str gender
        +str link
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +str country
        +str lang
        +bool is_kid
        +str explicit_content_level
        +list~str~ explicit_content_levels_available
        +str tracklist
        +get_albums(params: Any) AsyncPaginatedList~Album~
        +add_album(album: Album)
        +remove_album(album: Album)
        +get_tracks(kwargs: Any) AsyncPaginatedList~Track~
        +add_track(track: Track)
        +remove_track(track: Track)
        +get_artists(params: Any) AsyncPaginatedList~Artist~
        +add_artist(artist: Artist)
        +remove_artist(artist: Artist)
        +get_followers(params: Any) AsyncPaginatedList~User~
        +get_followings(params: Any) AsyncPaginatedList~User~
        +follow(user: User)
        +unfollow(user: User)
        +get_playlists(params: Any) AsyncPaginatedList~Playlist~
        +add_playlist(playlist: Playlist)
        +remove_playlist(playlist: Playlist)
        +create_playlist(title: str) int
    }

    class Playlist {
        +int id
        +str title
        +str description
        +int duration
        +bool public
        +bool is_loved_track
        +bool collaborative
        +int nb_tracks
        +int unseen_track_count
        +int fans
        +str link
        +str share
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +str checksum
        +User creator
        +list~Track~ tracks
        +get_tracks(kwargs: Any) AsyncPaginatedList~Track~
        +get_fans(kwargs: Any) AsyncPaginatedList~User~
        +mark_seen() bool
        +add_tracks(tracks: Iterable)
        +delete_tracks(tracks: Iterable)
        +reorder_tracks(order: Iterable)
    }

    class Episode {
        +int id
        +str title
        +str description
        +bool available
        +dt_datetime release_date
        +int duration
        +str link
        +str share
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +Podcast podcast
        +add_bookmark(offset: int) bool
        +remove_bookmark() bool
    }

    class Genre {
        +int id
        +str name
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +get_artists(kwargs: Any) list~Artist~
        +get_podcasts(kwargs: Any) AsyncPaginatedList~Podcast~
        +get_radios(kwargs: Any) list~Radio~
    }

    class Editorial {
        +int id
        +str name
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +get_selection() list~Album~
        +get_chart() Chart
        +get_releases(kwargs: Any) AsyncPaginatedList~Album~
    }

    class Podcast {
        +int id
        +str title
        +str description
        +bool available
        +int fans
        +str link
        +str share
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +get_episodes(kwargs: Any) AsyncPaginatedList~Episode~
    }

    class Radio {
        +int id
        +str title
        +str description
        +str share
        +str picture
        +str picture_small
        +str picture_medium
        +str picture_big
        +str picture_xl
        +str tracklist
        +str md5_image
        +get_tracks() list~Track~
    }

    Album --|> Resource
    Artist --|> Resource
    Track --|> Resource
    User --|> Resource
    Playlist --|> Resource
    Episode --|> Resource
    Genre --|> Resource
    Editorial --|> Resource
    Podcast --|> Resource
    Radio --|> Resource

    Album --> Artist
    Album --> Track
    Album --> Genre
    Track --> Artist
    Track --> Album
    Playlist --> Track
    Playlist --> User
    Episode --> Podcast
    Editorial --> Chart
    Genre --> Podcast
    Genre --> Radio
    Radio --> Track
    User --> Playlist
    User --> Album
    User --> Track
    User --> Artist
Loading

Flow diagram for AsyncPaginatedList data loading strategy

flowchart TD
    A_start[Start async iteration or helper call
aget, aslice, get_total] --> B_check_cached{Enough elements
in _elements?}

    B_check_cached -- yes --> C_return_cached[Return data from _elements]

    B_check_cached -- no --> D_can_grow{_next_path is not None?}

    D_can_grow -- no --> E_end[Stop iteration or return
available elements]

    D_can_grow -- yes --> F_fetch_page[Call _fetch_next_page]

    F_fetch_page --> G_request[AsyncClient.request
GET next_path]
    G_request --> H_httpx[httpx.AsyncClient.request]
    H_httpx --> I_api[Deezer API
returns JSON page]
    I_api --> J_process[AsyncClient._process_json
with paginate_list = True]
    J_process --> K_payload[response_payload with
data, total, next]

    K_payload --> L_update_state[Update _elements,
_total, _next_path,
_next_params]
    L_update_state --> B_check_cached

    C_return_cached --> M_done[Result to caller]
    E_end --> M_done
Loading

File-Level Changes

Change Details Files
Add AsyncClient built on httpx.AsyncClient that mirrors deezer.Client and wires in async request handling and pagination.
  • Subclass httpx.AsyncClient to create AsyncClient with DeezerQueryAuth and Deezer-specific base URL configuration.
  • Implement async request method that wraps httpx.AsyncClient.request, normalizes error handling to DeezerHTTPError/DeezerErrorResponse, and converts JSON payloads into Resource objects.
  • Port high-level API methods from deezer.Client (get_album, search*, user operations, charts, radios, recommendations, etc.) to async equivalents returning either resources, lists, or AsyncPaginatedList instances.
  • Introduce internal _process_json helper that recursively instantiates appropriate async resource types based on payload type fields and supports paginated list payloads.
src/async_deezer/client.py
Introduce AsyncPaginatedList to support async iteration and random access over paginated endpoints without blocking the event loop.
  • Implement AsyncPaginatedList with internal element cache, next-page tracking, and async aiter for streaming results via async for.
  • Implement async helpers aget, aslice, and get_total to mirror synchronous pagination while avoiding blocking I/O.
  • Parse next-page URLs from Deezer API responses using urlparse/parse_qs, propagating base_path/params to compute subsequent requests.
  • Integrate AsyncPaginatedList with AsyncClient by having pagination-aware requests and a factory method on the client.
src/async_deezer/pagination.py
src/async_deezer/client.py
Create async resource model hierarchy mirroring deezer.resources with async relation methods and async pagination integration.
  • Define async Resource base class that initializes from JSON, supports as_dict conversion, and exposes async relation helpers (get_relation, post_relation, delete_relation) plus get_paginated_list returning AsyncPaginatedList.
  • Implement async counterparts for Album, Artist, Chart, Editorial, Episode, Genre, Playlist, Podcast, Radio, Track, and User with the same fields as sync models and async relation methods where network I/O occurs.
  • Wire resource methods to AsyncClient endpoints, e.g., async get_artist/get_album on Track and Album, async bookmark operations on Episode, playlist track management methods on Playlist, and user library/follow operations on User.
  • Export all async resource classes and AsyncClient from the async_deezer package’s init modules for public use.
src/async_deezer/resources/resource.py
src/async_deezer/resources/album.py
src/async_deezer/resources/artist.py
src/async_deezer/resources/chart.py
src/async_deezer/resources/editorial.py
src/async_deezer/resources/episode.py
src/async_deezer/resources/genre.py
src/async_deezer/resources/playlist.py
src/async_deezer/resources/podcast.py
src/async_deezer/resources/radio.py
src/async_deezer/resources/track.py
src/async_deezer/resources/user.py
src/async_deezer/resources/__init__.py
src/async_deezer/__init__.py
Document async client usage and pagination patterns alongside the existing synchronous docs.
  • Fix existing httpx link formatting in docs/usage.md.
  • Add an “Async client” section to docs/usage.md explaining AsyncClient semantics, async context-manager usage, async pagination with AsyncPaginatedList, and async helpers aget/aslice/get_total.
  • Extend README with an “Async usage” section including minimal AsyncClient example, async pagination example, and a note that async_deezer is a separate internal package so the public deezer-python API remains unchanged.
  • Clarify that the existing “Basic Use” section refers to synchronous usage.
docs/usage.md
README.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.86%. Comparing base (65898f8) to head (ab31cb4).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1531   +/-   ##
=======================================
  Coverage   99.86%   99.86%           
=======================================
  Files          20       20           
  Lines         723      723           
  Branches       46       46           
=======================================
  Hits          722      722           
  Partials        1        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues, and left some high level feedback:

  • There is a circular import between async_deezer.client.AsyncClient and async_deezer.pagination.AsyncPaginatedList (each imports the other at module import time); consider moving the AsyncClient import in pagination.py behind TYPE_CHECKING or using string-based type hints to break the cycle.
  • In async_deezer.resources.Episode, _infer_missing_field calls super()._infer_missing_field, but the async Resource base class does not define this method, which will raise at runtime; either add _infer_missing_field to the async Resource or drop the super() call and handle all cases locally.
  • In async_deezer.pagination.AsyncPaginatedList, parent is typed as deezer.resources.Resource while the async resources inherit from async_deezer.resources.Resource, which can be confusing and makes static typing inaccurate; aligning the type to the async Resource (and adjusting imports accordingly) would make the async layer more consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There is a circular import between `async_deezer.client.AsyncClient` and `async_deezer.pagination.AsyncPaginatedList` (each imports the other at module import time); consider moving the `AsyncClient` import in `pagination.py` behind `TYPE_CHECKING` or using string-based type hints to break the cycle.
- In `async_deezer.resources.Episode`, `_infer_missing_field` calls `super()._infer_missing_field`, but the async `Resource` base class does not define this method, which will raise at runtime; either add `_infer_missing_field` to the async `Resource` or drop the `super()` call and handle all cases locally.
- In `async_deezer.pagination.AsyncPaginatedList`, `parent` is typed as `deezer.resources.Resource` while the async resources inherit from `async_deezer.resources.Resource`, which can be confusing and makes static typing inaccurate; aligning the type to the async `Resource` (and adjusting imports accordingly) would make the async layer more consistent.

## Individual Comments

### Comment 1
<location> `src/async_deezer/resources/episode.py:36-41` </location>
<code_context>
+
+    _parse_release_date = staticmethod(parse_datetime)
+
+    def _infer_missing_field(self, item) -> Any:  # type: ignore[override]
+        if item == "link":
+            return f"https://www.deezer.com/episode/{self.id}"
+        elif item == "share":
+            return f"{self.link}?utm_source=deezer&utm_content=episode-{self.id}&utm_medium=web"
+        return super()._infer_missing_field(item)  # type: ignore[no-any-return]
+
+    async def add_bookmark(self, offset: int) -> bool:
</code_context>

<issue_to_address>
**issue (bug_risk):** This override calls a base method that does not exist on Resource, which will raise at runtime if invoked.

`Episode` implements `_infer_missing_field`, but `Resource` doesn’t define it, so `super()._infer_missing_field(item)` will raise `AttributeError` if reached. Please either add `_infer_missing_field` to `Resource` (consistent with the sync variant) or remove this override and set `link`/`share` defaults elsewhere (e.g. in `__init__`).
</issue_to_address>

### Comment 2
<location> `src/async_deezer/resources/resource.py:9-18` </location>
<code_context>
+class Resource:
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Resource is missing the `_infer_missing_field` hook that some resource types expect to use for inferred attributes.

Async resources like `Episode` rely on `_infer_missing_field` from the sync base class to populate defaults (e.g. `link`, `share`). In this async `Resource`, that hook doesn’t exist, so subclasses calling `super()._infer_missing_field` will raise `AttributeError` and inferred fields won’t be set. To keep behavior consistent with the sync API, either port the `_infer_missing_field` / `__getattr__` logic here or update subclasses to no longer depend on it.

Suggested implementation:

```python
from typing import Any

from async_deezer.pagination import AsyncPaginatedList
from deezer.resources import Resource as SyncResource

```

```python
class Resource:
    """
    Async base class for any Deezer resource.

    This mirrors :class:`deezer.resources.Resource`, but relation helpers and
    ``get`` are implemented as async methods, and pagination returns
    :class:`async_deezer.pagination.AsyncPaginatedList`.
    """

    id: int
    type: str

    def _infer_missing_field(self, name: str) -> Any:
        """
        Hook used by __getattr__ and subclasses to lazily infer missing fields.

        This delegates to the synchronous Resource implementation to keep
        behavior (e.g. inferred ``link`` / ``share``) consistent with the
        sync API.
        """
        return SyncResource._infer_missing_field(self, name)

    def __getattr__(self, name: str) -> Any:
        """
        Fallback attribute access that mirrors the synchronous Resource.

        When an attribute is missing, attempt to infer it via
        :meth:`_infer_missing_field`. If inference fails, raise AttributeError.
        """
        return SyncResource.__getattr__(self, name)

```

If other parts of this file override `__getattr__` or `_infer_missing_field` later on, those overrides should either be removed in favor of this delegation, or updated to call `super().__getattr__(name)` / `super()._infer_missing_field(name)` so that the synchronous inference behavior is preserved.
</issue_to_address>

### Comment 3
<location> `src/async_deezer/client.py:51-53` </location>
<code_context>
+        "playlist": Playlist,
+        "podcast": Podcast,
+        "radio": Radio,
+        "search": None,
+        "track": Track,
+        "user": User,
+    }
+
</code_context>

<issue_to_address>
**issue (bug_risk):** The objects_types entry for 'search' is None but later code asserts that the resolved class is not None, which can cause an assertion failure.

Because `objects_types` defines `
</issue_to_address>

### Comment 4
<location> `src/async_deezer/client.py:385-389` </location>
<code_context>
+            query_parts.append(query)
+        query_parts.extend(
+            f'{param_name}:"{param_value}"'
+            for param_name, param_value in advanced_params.items()
+            if param_value
+        )
+
</code_context>

<issue_to_address>
**suggestion:** Filtering advanced search parameters by simple truthiness may unintentionally drop valid values like 0.

In `_search`, advanced params are only added when `param_value` is truthy, so values like `0` for `dur_min/dur_max/bpm_min/bpm_max` are dropped from the query. To allow falsy-but-valid values, change the condition to `if param_value is not None` so only `None` is excluded.

```suggestion
        query_parts.extend(
            f'{param_name}:"{param_value}"'
            for param_name, param_value in advanced_params.items()
            if param_value is not None
        )
```
</issue_to_address>

### Comment 5
<location> `README.md:58-59` </location>
<code_context>
-## Basic Use
+## Basic Use (synchronous)

 Easily query the Deezer API from you Python code. The data returned by the Deezer
 API is mapped to python resources:
</code_context>

<issue_to_address>
**issue (typo):** Typo in phrase "from you Python code"; should be "from your Python code".

Update the sentence to: "Easily query the Deezer API from your Python code."

```suggestion
Easily query the Deezer API from your Python code. The data returned by the Deezer
API is mapped to python resources:
```
</issue_to_address>

### Comment 6
<location> `README.md:107` </location>
<code_context>
+  project so it does not change the public API of `deezer-python`.
+
 Ready for more? Look at our whole [documentation](http://deezer-python.readthedocs.io/)
 on Read The Docs or have a play in pre-populated Jupyter notebook
 [on Binder](https://mybinder.org/v2/gh/browniebroke/deezer-python/main?filepath=demo.ipynb).
</code_context>

<issue_to_address>
**suggestion (typo):** Missing article before "pre-populated Jupyter notebook".

Suggest: "have a play in a pre-populated Jupyter notebook" (adding the article "a").
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@OkoyaUsman OkoyaUsman changed the title Introducing Async (re: #1339) feat(async): introduce async module Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant