199 lines
7.0 KiB
Python
199 lines
7.0 KiB
Python
# Tools for A Feminist Audit of Pop Music.
|
|
# Copyright 2026 imacat. All rights reserved.
|
|
# Authors:
|
|
# imacat@mail.imacat.idv.tw (imacat), 2026/7/31
|
|
"""The data models.
|
|
|
|
The schema covers the year-end chart data: songs with their
|
|
lyrics, their yearly chart entries, the individual artists, and
|
|
the song-artist credits with the role and order. It also covers
|
|
the settled coding of the songs: the keywords assigned to each
|
|
song with the lyric quotes they are grounded in.
|
|
|
|
"""
|
|
import enum
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from .database import Base
|
|
|
|
|
|
class Role(enum.StrEnum):
|
|
"""The role of an artist on a song credit."""
|
|
|
|
PRIMARY = "primary"
|
|
"""The primary artist role."""
|
|
FEATURED = "featured"
|
|
"""The featured artist role."""
|
|
|
|
|
|
class Song(Base):
|
|
"""A song, identified by its title and combined artist credit."""
|
|
__tablename__ = "songs"
|
|
"""The table name."""
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
"""The song ID."""
|
|
title: Mapped[str] = mapped_column()
|
|
"""The song title."""
|
|
artist_credit: Mapped[str] = mapped_column()
|
|
"""The combined artist credit string as printed on the chart."""
|
|
lyrics: Mapped[str | None]
|
|
"""The lyrics text, when available."""
|
|
performer_gender: Mapped[str | None]
|
|
"""The gender of the credited performers taken together:
|
|
"mixed" when they disagree, their common gender when every
|
|
credited artist's gender is known and they agree, and None
|
|
otherwise."""
|
|
chart_entries: Mapped[list[ChartEntry]] \
|
|
= relationship(back_populates="song")
|
|
"""The chart entries of the song."""
|
|
song_artists: Mapped[list[SongArtist]] \
|
|
= relationship(back_populates="song")
|
|
"""The song-artist credits of the song."""
|
|
codings: Mapped[list[Coding]] \
|
|
= relationship(back_populates="song")
|
|
"""The settled codings of the song."""
|
|
annotations: Mapped[list[Annotation]] \
|
|
= relationship(back_populates="song")
|
|
"""The settled pattern annotations of the song."""
|
|
__table_args__ = (sa.UniqueConstraint(title, artist_credit),)
|
|
"""The table-level constraints."""
|
|
|
|
|
|
class ChartEntry(Base):
|
|
"""An entry of a song on the year-end chart."""
|
|
__tablename__ = "chart_entries"
|
|
"""The table name."""
|
|
|
|
year: Mapped[int] = mapped_column(primary_key=True)
|
|
"""The chart year."""
|
|
rank: Mapped[int] = mapped_column(primary_key=True)
|
|
"""The rank of the song on the chart of the year."""
|
|
song_id: Mapped[int] = mapped_column(sa.ForeignKey(Song.id))
|
|
"""The ID of the charted song."""
|
|
song: Mapped[Song] = relationship(back_populates="chart_entries")
|
|
"""The charted song."""
|
|
|
|
|
|
class Artist(Base):
|
|
"""An individual artist."""
|
|
__tablename__ = "artists"
|
|
"""The table name."""
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
"""The artist ID."""
|
|
name: Mapped[str] = mapped_column(unique=True)
|
|
"""The artist name."""
|
|
wikidata_qid: Mapped[str | None]
|
|
"""The Wikidata QID of the artist."""
|
|
gender: Mapped[str | None]
|
|
"""The gender of the artist."""
|
|
type: Mapped[str | None]
|
|
"""The artist type: solo or group."""
|
|
genre: Mapped[str | None]
|
|
"""The music genre of the artist."""
|
|
country: Mapped[str | None]
|
|
"""The country of the artist."""
|
|
song_artists: Mapped[list[SongArtist]] \
|
|
= relationship(back_populates="artist")
|
|
"""The song-artist credits of the artist."""
|
|
|
|
|
|
class SongArtist(Base):
|
|
"""A credit of an artist on a song, with the role and order."""
|
|
__tablename__ = "song_artists"
|
|
"""The table name."""
|
|
|
|
song_id: Mapped[int] = mapped_column(sa.ForeignKey(Song.id),
|
|
primary_key=True)
|
|
"""The ID of the credited song."""
|
|
artist_id: Mapped[int] = mapped_column(
|
|
sa.ForeignKey(Artist.id), primary_key=True)
|
|
"""The ID of the credited artist."""
|
|
role: Mapped[str] = mapped_column()
|
|
"""The role of the artist, a ``Role`` value."""
|
|
position: Mapped[int]
|
|
"""The 0-based position of the artist in the credit order."""
|
|
song: Mapped[Song] = relationship(back_populates="song_artists")
|
|
"""The credited song."""
|
|
artist: Mapped[Artist] \
|
|
= relationship(back_populates="song_artists")
|
|
"""The credited artist."""
|
|
__table_args__ = (
|
|
sa.CheckConstraint(role.in_([x.value for x in Role]),
|
|
name="ck_song_artists_role"),)
|
|
"""The table-level constraints."""
|
|
|
|
|
|
class Coding(Base):
|
|
"""A settled coding keyword of a song, with its lyric quotes."""
|
|
__tablename__ = "codings"
|
|
"""The table name."""
|
|
|
|
song_id: Mapped[int] = mapped_column(sa.ForeignKey(Song.id),
|
|
primary_key=True)
|
|
"""The ID of the coded song."""
|
|
keyword: Mapped[str] = mapped_column(primary_key=True)
|
|
"""The coding keyword assigned to the song."""
|
|
quotes: Mapped[str] = mapped_column()
|
|
"""The lyric quotes the keyword is grounded in, joined by a
|
|
single "|", empty when the keyword carries no evidence."""
|
|
song: Mapped[Song] = relationship(back_populates="codings")
|
|
"""The coded song."""
|
|
|
|
|
|
class CodeGroup(Base):
|
|
"""A settled member keyword of a semantic code group."""
|
|
__tablename__ = "groups"
|
|
"""The table name."""
|
|
|
|
group: Mapped[str] = mapped_column(primary_key=True)
|
|
"""The group name, as the settled group table carries it."""
|
|
keyword: Mapped[str] = mapped_column(primary_key=True)
|
|
"""The member coding keyword."""
|
|
votes: Mapped[int] = mapped_column()
|
|
"""The number of the selection runs that selected the
|
|
keyword for the group."""
|
|
|
|
|
|
class Pattern(Base):
|
|
"""A stored gendered-language pattern definition."""
|
|
__tablename__ = "patterns"
|
|
"""The table name."""
|
|
|
|
pattern: Mapped[str] = mapped_column(primary_key=True)
|
|
"""The pattern ID, as the pattern table carries it."""
|
|
group: Mapped[str] = mapped_column()
|
|
"""The pattern group: male, female, or mixed."""
|
|
name: Mapped[str] = mapped_column()
|
|
"""The pattern name."""
|
|
description: Mapped[str] = mapped_column()
|
|
"""The pattern description."""
|
|
annotations: Mapped[list[Annotation]] \
|
|
= relationship(back_populates="pattern")
|
|
"""The settled annotations assigning the pattern to a song."""
|
|
|
|
|
|
class Annotation(Base):
|
|
"""A settled pattern annotation of a song, with its vote
|
|
count."""
|
|
__tablename__ = "annotations"
|
|
"""The table name."""
|
|
|
|
song_id: Mapped[int] = mapped_column(sa.ForeignKey(Song.id),
|
|
primary_key=True)
|
|
"""The ID of the annotated song."""
|
|
pattern_id: Mapped[str] = mapped_column(
|
|
sa.ForeignKey(Pattern.pattern), primary_key=True)
|
|
"""The ID of the assigned pattern."""
|
|
votes: Mapped[int] = mapped_column()
|
|
"""The number of the annotation runs that assigned the
|
|
pattern to the song."""
|
|
song: Mapped[Song] = relationship(back_populates="annotations")
|
|
"""The annotated song."""
|
|
pattern: Mapped[Pattern] \
|
|
= relationship(back_populates="annotations")
|
|
"""The assigned pattern."""
|