Import the settled coding into the working store

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:38:36 +08:00
co-authored by Claude Opus 5
parent 58189bae3a
commit 40c41f2734
6 changed files with 337 additions and 9 deletions
+150
View File
@@ -21,6 +21,7 @@ from pop_fem_audit_tools.database import DataSource
from pop_fem_audit_tools.models import (
Artist,
ChartEntry,
Coding,
Role,
Song,
)
@@ -266,6 +267,7 @@ class TestBuildDB(unittest.TestCase):
self.__lyrics: Path = self.__dir / "lyrics"
self.__wikidata: Path = \
self.__dir / "artists_wikidata.csv"
self.__codings: Path = self.__dir / "codings.csv"
self.__write_chart(self.CHART_CSV)
url: str = f"sqlite:///{self.__dir}/store.sqlite3"
config.set_settings(config.Settings(
@@ -290,6 +292,14 @@ class TestBuildDB(unittest.TestCase):
"""
self.__chart.write_text(content, encoding="utf-8")
def __write_codings(self, content: str) -> None:
"""Write the coding CSV fixture.
:param content: The CSV content.
:return: None.
"""
self.__codings.write_text(content, encoding="utf-8")
def __run_build(self, *options: str) -> tuple[int, str]:
"""Run the build with the standard error captured.
@@ -821,3 +831,143 @@ class TestBuildDB(unittest.TestCase):
(self.__derived / "artists.csv").read_text(
encoding="utf-8"),
artists_before)
CODINGS_CSV: str = (
"Song,Artist Credit,Keyword,Quote\n"
"Hello,Adele,longing,\"Hello from the other side\\nI must"
" have called a thousand times\"\n"
"Hello,Adele,regret,I'm sorry|It's no secret\n"
"One Dance,Drake featuring Wizkid,desire,\n")
"""The coding CSV fixture: a two-line quote, two quotes joined
by "|", and an empty quote."""
def __stored_codings(self) -> dict[tuple[str, str], str]:
"""Read the stored codings keyed by the song and keyword.
:return: The stored quotes, keyed by the song title and the
keyword.
"""
session: Session = self.__session()
return {(x.song.title, x.keyword): x.quotes
for x in session.scalars(sa.select(Coding))}
def test_codings_imported(self) -> None:
"""Test that the coding CSV imports one row per song and
keyword, restoring the newline and keeping the "|"-joined
quotes verbatim."""
self.__write_codings(self.CODINGS_CSV)
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertEqual(status, 0)
self.assertIn("3 codings", stderr)
self.assertEqual(
self.__stored_codings(),
{("Hello", "longing"):
"Hello from the other side\n"
"I must have called a thousand times",
("Hello", "regret"): "I'm sorry|It's no secret",
("One Dance", "desire"): ""})
def test_codings_reach_the_song(self) -> None:
"""Test that a stored coding reaches its song through the
relationship."""
self.__write_codings(self.CODINGS_CSV)
self.assertEqual(
self.__run_build("--codings", str(self.__codings))[0], 0)
session: Session = self.__session()
song: Song | None = session.get(Song, 1)
assert song is not None
self.assertEqual(sorted(x.keyword for x in song.codings),
["longing", "regret"])
def test_omitted_codings_leaves_table_empty(self) -> None:
"""Test that an omitted coding option leaves no codings."""
self.__write_codings(self.CODINGS_CSV)
status: int
stderr: str
status, stderr = self.__run_build()
self.assertEqual(status, 0)
self.assertIn("0 codings", stderr)
self.assertEqual(self.__stored_codings(), {})
def test_codings_unknown_song_fails(self) -> None:
"""Test that a coding row naming an unknown song fails the
build, leaving the previous store contents intact."""
self.assertEqual(self.__run_build()[0], 0)
titles: dict[int, str] = self.__song_titles()
self.__write_codings(
"Song,Artist Credit,Keyword,Quote\n"
"Nowhere,Nobody,longing,a line\n")
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertNotEqual(status, 0)
self.assertIn("no song \"Nowhere\" by \"Nobody\"", stderr)
self.assertEqual(self.__song_titles(), titles)
self.assertEqual(self.__stored_codings(), {})
def test_codings_duplicated_keyword_fails(self) -> None:
"""Test that two rows naming the same song and keyword fail
the build, without partial data."""
self.__write_codings(
"Song,Artist Credit,Keyword,Quote\n"
"Hello,Adele,longing,a line\n"
"Hello,Adele,longing,another line\n")
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertNotEqual(status, 0)
self.assertIn("duplicated coding", stderr)
session: Session = self.__session()
self.assertEqual(list(session.scalars(sa.select(Song))), [])
def test_codings_missing_column_fails(self) -> None:
"""Test that a coding CSV missing a required column fails
the build."""
self.__write_codings(
"Song,Artist Credit,Keyword\n"
"Hello,Adele,longing\n")
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertNotEqual(status, 0)
self.assertIn("missing column(s): Quote", stderr)
self.assertEqual(self.__stored_codings(), {})
def test_missing_codings_csv_fails(self) -> None:
"""Test that a given but missing coding CSV fails."""
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertNotEqual(status, 0)
self.assertIn("error:", stderr)
self.assertIn(str(self.__codings), stderr)
session: Session = self.__session()
self.assertEqual(list(session.scalars(sa.select(Song))), [])
def test_codings_replaced_on_rebuild(self) -> None:
"""Test that a rebuild replaces the previous codings rather
than adding to them."""
self.__write_codings(self.CODINGS_CSV)
self.assertEqual(
self.__run_build("--codings", str(self.__codings))[0], 0)
self.__write_codings(
"Song,Artist Credit,Keyword,Quote\n"
"Shape of You,Ed Sheeran,attraction,I'm in love with"
" your body\n")
status: int
stderr: str
status, stderr = self.__run_build(
"--codings", str(self.__codings))
self.assertEqual(status, 0)
self.assertIn("1 codings", stderr)
self.assertEqual(
self.__stored_codings(),
{("Shape of You", "attraction"):
"I'm in love with your body"})
+34
View File
@@ -14,6 +14,7 @@ from pop_fem_audit_tools.database import Base, DataSource
from pop_fem_audit_tools.models import (
Artist,
ChartEntry,
Coding,
Role,
Song,
SongArtist,
@@ -47,6 +48,10 @@ class TestModels(unittest.TestCase):
SongArtist(artist=Artist(name="Wizkid"),
role=Role.FEATURED, position=1)]
song.lyrics = "Baby, I like your style"
song.codings = [
Coding(keyword="desire",
quotes="Baby, I like your style|One dance"),
Coding(keyword="nightlife", quotes="")]
self.__session.add(song)
self.__session.commit()
@@ -83,6 +88,35 @@ class TestModels(unittest.TestCase):
with self.assertRaises(sa.exc.IntegrityError):
self.__session.commit()
def test_song_codings(self) -> None:
"""Test reading a song's codings back through the
relationship."""
self.__add_song()
self.__session.expunge_all()
song: Song | None = self.__session.scalar(
sa.select(Song).where(Song.title == "One Dance"))
assert song is not None
self.assertEqual(
{(x.keyword, x.quotes) for x in song.codings},
{("desire", "Baby, I like your style|One dance"),
("nightlife", "")})
coding: Coding | None = self.__session.scalar(
sa.select(Coding).where(Coding.keyword == "desire"))
assert coding is not None
self.assertEqual(coding.song.title, "One Dance")
def test_duplicated_coding_rejected(self) -> None:
"""Test that a repeated song and keyword fails."""
self.__add_song()
song: Song | None = self.__session.scalar(
sa.select(Song).where(Song.title == "One Dance"))
assert song is not None
self.__session.add(
Coding(song_id=song.id, keyword="desire",
quotes="another line"))
with self.assertRaises(sa.exc.IntegrityError):
self.__session.commit()
def test_invalid_role_rejected(self) -> None:
"""Test that an invalid song-artist role fails."""
self.__add_song()