Files
pop-fem-audit/tools/src/pop_fem_audit_tools/models.py
T

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."""