Skip to content

Schemas

Base schemas

The base schemas are Pydantic models used to represent track, artist, album or playlist data.

Pydantic schemas for albums.

AlbumData

Bases: BaseModel

Album data representation.

Attributes:

Name Type Description
name str

Album name

id str

Album id

uri str

Spotify URI for the album

release_date date | str

The year the album was released.

Source code in chopin/schemas/album.py
class AlbumData(BaseModel):
    """Album data representation.

    Attributes:
        name: Album name
        id: Album id
        uri: Spotify URI for the album
        release_date: The year the album was released.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    name: str
    id: str
    uri: str
    release_date: date | str

    @field_validator("release_date", mode="before")
    def release_date_validate(cls, v):
        """Format the release date based on the level of detail available."""
        if isinstance(v, str):
            return parse_release_date(v)
        return v

release_date_validate(v)

Format the release date based on the level of detail available.

Source code in chopin/schemas/album.py
@field_validator("release_date", mode="before")
def release_date_validate(cls, v):
    """Format the release date based on the level of detail available."""
    if isinstance(v, str):
        return parse_release_date(v)
    return v

Pydantic schemas for artists.

ArtistData

Bases: BaseModel

Artist data representation.

Attributes:

Name Type Description
name str

Name of the artist

id str

Id of the artist

uri str

Spotify URI for the artist

genres list[str] | None

A list of strings describing the artist genres

Warning

The 'genres' information is not always available. And it can be quite flaky

Source code in chopin/schemas/artist.py
class ArtistData(BaseModel):
    """Artist data representation.

    Attributes:
        name: Name of the artist
        id: Id of the artist
        uri: Spotify URI for the artist
        genres: A list of strings describing the artist genres

    !!! warning
        The 'genres' information is not always available. And it can be quite flaky
    """

    name: str
    id: str
    uri: str
    genres: list[str] | None = None

    model_config = ConfigDict(extra="ignore")

Pydantic schemas for playlists.

PlaylistData

Bases: BaseModel

Playlist representation.

Attributes:

Name Type Description
name str

Name of the playlist

uri str

Spotify URI for the playlist

Source code in chopin/schemas/playlist.py
class PlaylistData(BaseModel):
    """Playlist representation.

    Attributes:
        name: Name of the playlist
        uri: Spotify URI for the playlist
    """

    name: str
    uri: str
    id: str
    description: str = ""

PlaylistSummary

Bases: BaseModel

Representation of a full playlist. It is used to describe playlists and back them up.

Attributes:

Name Type Description
playlist PlaylistData

The playlist described

tracks list[TrackData]

A list of TrackData in the playlist

_nb_tracks int | None

Number of tracks in the playlist

_total_duration float | None

Length (in milliseconds) of the playlist

_nb_artists int | None

Number of artists in the playlist

_avg_features int | None

Average values across the track features

_avg_popularity float | None

Average popularity of the tracks in the playlist

Note

The private attributes are automatically computed in a validator.

Source code in chopin/schemas/playlist.py
class PlaylistSummary(BaseModel):
    """Representation of a full playlist. It is used to describe playlists and back them up.

    Attributes:
        playlist: The playlist described
        tracks: A list of TrackData in the playlist
        _nb_tracks: Number of tracks in the playlist
        _total_duration: Length (in milliseconds) of the playlist
        _nb_artists: Number of artists in the playlist
        _avg_features: Average values across the track features
        _avg_popularity: Average popularity of the tracks in the playlist

    !!! note
        The private attributes are automatically computed in a validator.
    """

    playlist: PlaylistData
    tracks: list[TrackData]
    _nb_tracks: int | None = None
    _total_duration: float | None = None
    _nb_artists: int | None = None
    _avg_popularity: float | None = None

    @model_validator(mode="after")
    def fill_fields(self):
        """Compute field values on initialzation."""
        tracks = self.tracks
        popularities = [track.popularity for track in tracks]
        self._nb_tracks = len(tracks)
        self._nb_artists = len(set(track.artists[0].name for track in tracks))
        self._total_duration = sum([track.duration_ms for track in tracks])
        self._avg_popularity = sum(popularities) / len(popularities) if popularities else 0
        return self

    def __str__(self):
        """Represent a playlist summary."""
        return (
            f"------ Playlist {self.playlist.name} ------\n\t{self._nb_tracks} tracks\n\t{self._nb_artists} artists\n"
        )

    @model_serializer
    def serialize_model(self):
        """Serialize plalist summaries and add version information."""
        return {
            "playlist": self.playlist.model_dump(),
            "tracks": [track.model_dump() for track in self.tracks],
            "version": VERSION,
        }

__str__()

Represent a playlist summary.

Source code in chopin/schemas/playlist.py
def __str__(self):
    """Represent a playlist summary."""
    return (
        f"------ Playlist {self.playlist.name} ------\n\t{self._nb_tracks} tracks\n\t{self._nb_artists} artists\n"
    )

fill_fields()

Compute field values on initialzation.

Source code in chopin/schemas/playlist.py
@model_validator(mode="after")
def fill_fields(self):
    """Compute field values on initialzation."""
    tracks = self.tracks
    popularities = [track.popularity for track in tracks]
    self._nb_tracks = len(tracks)
    self._nb_artists = len(set(track.artists[0].name for track in tracks))
    self._total_duration = sum([track.duration_ms for track in tracks])
    self._avg_popularity = sum(popularities) / len(popularities) if popularities else 0
    return self

serialize_model()

Serialize plalist summaries and add version information.

Source code in chopin/schemas/playlist.py
@model_serializer
def serialize_model(self):
    """Serialize plalist summaries and add version information."""
    return {
        "playlist": self.playlist.model_dump(),
        "tracks": [track.model_dump() for track in self.tracks],
        "version": VERSION,
    }

Pydantic schemas for tracks.

TrackData

Bases: BaseModel

Representation of a track.

Attributes:

Name Type Description
name str

Track name

id str

Track id

uri str

Spotify URI for the track

duration_ms int

Duration of the track, in milliseconds

popularity int | None

A [0, 100] measure for the track popularity. 100 is most popular

added_at FormattedDate | None

A date, when available, for which the track was added in the playlist.

album AlbumData | None

The album data

artists list[ArtistData] | None

The artists on the track

features list[ArtistData] | None

Audio features of the track.

Warning

By default, tracks sent by the Spotify API do not contain audio feature information. A call to the dedicated endpoint is necessary to fill the attribute.

Source code in chopin/schemas/track.py
class TrackData(BaseModel):
    """Representation of a track.

    Attributes:
        name: Track name
        id: Track id
        uri: Spotify URI for the track
        duration_ms: Duration of the track, in milliseconds
        popularity: A [0, 100] measure for the track popularity. 100 is most popular
        added_at: A date, when available, for which the track was added in the playlist.
        album: The album data
        artists: The artists on the track
        features: Audio features of the track.

    !!! warning
        By default, tracks sent by the Spotify API do not contain audio feature information.
        A call to the dedicated endpoint is necessary to fill the attribute.
    """

    name: str
    id: str
    uri: str
    duration_ms: int
    popularity: int
    added_at: FormattedDate | None = None
    album: AlbumData | None = None
    artists: list[ArtistData] | None = None
    popularity: int | None = 0

    def to_flatten_dict(self, **kwargs):
        """Export the track data as a non-nested dictionary."""
        if isinstance(self.artists, list):
            self.artists = self.artists[0]
        return flatten_dict(self.model_dump(**kwargs))

to_flatten_dict(**kwargs)

Export the track data as a non-nested dictionary.

Source code in chopin/schemas/track.py
def to_flatten_dict(self, **kwargs):
    """Export the track data as a non-nested dictionary."""
    if isinstance(self.artists, list):
        self.artists = self.artists[0]
    return flatten_dict(self.model_dump(**kwargs))

datetime_to_date(dt)

Truncate a datetime object into its date object.

Source code in chopin/schemas/track.py
def datetime_to_date(dt: datetime | date | None) -> date | None:
    """Truncate a datetime object into its date object."""
    if dt and isinstance(dt, datetime):
        return dt.date()
    return dt

Pydantc schemas for users.

UserData

Bases: BaseModel

User representation.

Attributes:

Name Type Description
name str

Name of the user

id str

The user id

uri str

Spotify URI for the user

Source code in chopin/schemas/user.py
class UserData(BaseModel):
    """User representation.

    Attributes:
        name: Name of the user
        id: The user id
        uri: Spotify URI for the user
    """

    name: str
    id: str
    uri: str

Composer schema

The composer schema is used to configure your playlist composition.

Schemas for playlist composition.

ComposerConfig

Bases: BaseModel

Schema for a composer configuration.

Attributes:

Name Type Description
name str

Name of the playlist you wish to create

description str

Description for your playlist

nb_songs Annotated[int, Field(gt=0)]

Target number of songs for the playlist.

playlists list[ComposerConfigItem] | None

A list of playlist names and their weight.

uris list[ComposerConfigItem] | None

A list of spotify playlist URIs to pick from directly.

history Annotated[list[ComposerConfigListeningHistory], Field(max_length=3)] | None

Include past listening habits and most listened songs.

Source code in chopin/schemas/composer.py
class ComposerConfig(BaseModel):
    """Schema for a composer configuration.

    Attributes:
        name: Name of the playlist you wish to create
        description: Description for your playlist
        nb_songs: Target number of songs for the playlist.
        playlists: A list of playlist names and their weight.
        uris: A list of spotify playlist URIs to pick from directly.
        history: Include past listening habits and most listened songs.
    """

    name: str = "🤖 Robot Mix"
    description: str = "Randomly generated mix"
    nb_songs: Annotated[int, Field(gt=0)]
    release_range: Annotated[tuple[str | None, str | None] | None, AfterValidator(read_date)] | None = None
    playlists: list[ComposerConfigItem] | None = []
    history: Annotated[list[ComposerConfigListeningHistory], Field(max_length=3)] | None = []
    uris: list[ComposerConfigItem] | None = []

    @field_validator("history")
    def history_field_ranges_must_be_unique(cls, v):
        """Check the history items are distinct."""
        ranges = [item.time_range for item in v]
        if len(set(ranges)) != len(ranges):
            raise ValueError("time_range items for history must be unique")
        return v

    @model_validator(mode="after")
    def fill_nb_songs(self) -> "ComposerConfig":
        """From the nb_songs and item weights, compute the nb_songs of each item.

        Args:
            values: Attributes of the composer configuration model.
        """
        item_weights: list[float] = [item.weight for category in SOURCES for item in getattr(self, category)]
        sum_of_weights: float = sum(item_weights)
        total_nb_songs: int = 0
        for category in SOURCES:
            for item in getattr(self, category):
                item.nb_songs = math.ceil((item.weight / sum_of_weights) * self.nb_songs)
                total_nb_songs += item.nb_songs
        logger.info(f"With the composer configuration parsed, {total_nb_songs} songs will be added.")
        return self

    @computed_field
    def items(self) -> list[list[ComposerConfigItem]]:  # noqa: D102
        return {source: getattr(self, source) for source in SOURCES}.items()

    @classmethod
    def parse_yaml(cls, file_path: Path) -> Self:
        """Read a composer configuration from a YAML file.

        Args:
            file_path: The YAML file path.

        Returns:
            A valid composer configuration model.
        """
        yaml = YAML(typ="safe", pure=True)
        with open(file_path) as f:
            data = yaml.load(f)
        return cls.model_validate(data)

fill_nb_songs()

From the nb_songs and item weights, compute the nb_songs of each item.

Parameters:

Name Type Description Default
values

Attributes of the composer configuration model.

required
Source code in chopin/schemas/composer.py
@model_validator(mode="after")
def fill_nb_songs(self) -> "ComposerConfig":
    """From the nb_songs and item weights, compute the nb_songs of each item.

    Args:
        values: Attributes of the composer configuration model.
    """
    item_weights: list[float] = [item.weight for category in SOURCES for item in getattr(self, category)]
    sum_of_weights: float = sum(item_weights)
    total_nb_songs: int = 0
    for category in SOURCES:
        for item in getattr(self, category):
            item.nb_songs = math.ceil((item.weight / sum_of_weights) * self.nb_songs)
            total_nb_songs += item.nb_songs
    logger.info(f"With the composer configuration parsed, {total_nb_songs} songs will be added.")
    return self

history_field_ranges_must_be_unique(v)

Check the history items are distinct.

Source code in chopin/schemas/composer.py
@field_validator("history")
def history_field_ranges_must_be_unique(cls, v):
    """Check the history items are distinct."""
    ranges = [item.time_range for item in v]
    if len(set(ranges)) != len(ranges):
        raise ValueError("time_range items for history must be unique")
    return v

parse_yaml(file_path) classmethod

Read a composer configuration from a YAML file.

Parameters:

Name Type Description Default
file_path Path

The YAML file path.

required

Returns:

Type Description
Self

A valid composer configuration model.

Source code in chopin/schemas/composer.py
@classmethod
def parse_yaml(cls, file_path: Path) -> Self:
    """Read a composer configuration from a YAML file.

    Args:
        file_path: The YAML file path.

    Returns:
        A valid composer configuration model.
    """
    yaml = YAML(typ="safe", pure=True)
    with open(file_path) as f:
        data = yaml.load(f)
    return cls.model_validate(data)

ComposerConfigItem

Bases: BaseModel

Base schema for input in the composer configuration.

Attributes:

Name Type Description
name Annotated[str, extract_uri_from_playlist_link]

Name of the item. It should respect the simplify_string nomenclature

weight Annotated[float, Field(ge=0)]

Weight of the input in the final composition

Source code in chopin/schemas/composer.py
class ComposerConfigItem(BaseModel):
    """Base schema for input in the composer configuration.

    Attributes:
        name: Name of the item. It should respect the simplify_string nomenclature
        weight: Weight of the input in the final composition
    """

    name: Annotated[str, extract_uri_from_playlist_link]
    weight: Annotated[float, Field(ge=0)] = 1.0
    nb_songs: int | None = 0
    selection_method: SelectionMethod | None = SelectionMethod.RANDOM

    @field_validator("name", mode="before")
    def extract_uri_from_link(cls, v: str):
        """If the value is an https link to spotify, extract the URI data.

        This allow users to share full links to their playlists.
        """
        if v.startswith("https://open.spotify.com"):
            return extract_uri_from_playlist_link(v)
        return v

    @field_validator("selection_method", mode="before")
    def lower_case_selection_method(cls, selection_method: str) -> SelectionMethod:
        """Force the selection method to be lower case."""  # TODO address this with proper enum handling
        if selection_method:
            return selection_method.lower()
        return

If the value is an https link to spotify, extract the URI data.

This allow users to share full links to their playlists.

Source code in chopin/schemas/composer.py
@field_validator("name", mode="before")
def extract_uri_from_link(cls, v: str):
    """If the value is an https link to spotify, extract the URI data.

    This allow users to share full links to their playlists.
    """
    if v.startswith("https://open.spotify.com"):
        return extract_uri_from_playlist_link(v)
    return v

lower_case_selection_method(selection_method)

Force the selection method to be lower case.

Source code in chopin/schemas/composer.py
@field_validator("selection_method", mode="before")
def lower_case_selection_method(cls, selection_method: str) -> SelectionMethod:
    """Force the selection method to be lower case."""  # TODO address this with proper enum handling
    if selection_method:
        return selection_method.lower()
    return

ComposerConfigListeningHistory

Bases: BaseModel

Base schema for the configuration section which will add the current user's best songs.

Attributes:

Name Type Description
time_range Literal['short_term', 'medium_term', 'long_term']

Time criteria for the best tracks. One of short_term (~ last 4 weeks), medium_term (~ last 6 months) or long_term (~ all time).

weight Annotated[float, Field(ge=0)]

Weight of the input in the final composition

Source code in chopin/schemas/composer.py
class ComposerConfigListeningHistory(BaseModel):
    """Base schema for the configuration section which will add the current user's best songs.

    Attributes:
        time_range: Time criteria for the best tracks. One of short_term (~ last 4 weeks), medium_term (~ last 6 months)
            or long_term (~ all time).
        weight: Weight of the input in the final composition
    """

    time_range: Literal["short_term", "medium_term", "long_term"] = "short_term"
    weight: Annotated[float, Field(ge=0)] = 1
    nb_songs: int | None = 0