Package the capture-layer application into a CaptureImporter class

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 15:34:36 +08:00
co-authored by Claude Fable 5
parent c561330bd0
commit 8fdd21f8a2
2 changed files with 88 additions and 55 deletions
+4 -2
View File
@@ -206,8 +206,10 @@
(身分判定、拆解、正名)隨行入 class 作公開 staticmethod
/class 常數,「哪個函式屬哪個工作」由 class 歸屬直接
表達。flush 定為匯入工作的完工契約——entry method 返回
時自身寫入已可查詢,不再由呼叫者補 flush。實測重構前後
工作儲存 dump 與衍生報表逐位元組相同。
時自身寫入已可查詢,不再由呼叫者補 flush。捕捉層套用
(歌詞、Wikidata 快照)隨後同型打包為 `CaptureImporter`
(單一入口收兩個可省略路徑)。實測重構前後工作儲存
dump 與衍生報表逐位元組相同。
- **歌手型態刪去 mixed 值**`ArtistType` 只留 sologroup。
mixed 是先導研究「男/女/混合團體」單一欄位的殘留,
正式設計拆成 gender+type 後從未定義其指涉;署名一律
+52 -21
View File
@@ -425,13 +425,52 @@ class ArtistImporter:
return folded, name
def load_lyrics(session: Session, directory: Path) -> None:
class CaptureImporter:
"""The capture-import job: applies the optional capture-layer
inputs onto the stored songs and artists."""
def __init__(self, session: Session) -> None:
"""Initialize the importer.
:param session: The database session.
"""
self.__session: Session = session
def import_captures(self, lyrics_dir: Path | None,
wikidata_csv: Path | None) -> None:
"""Apply the optional capture-layer inputs onto the store.
A None input leaves its capture layer unloaded. When
``lyrics_dir`` is given, its cached lyrics files load into
the matching songs (see `__load_lyrics`). When
``wikidata_csv`` is given, it applies onto the artist rows
(see `__apply_artist_csv`). When the method returns, the
applied changes are queryable in the session.
:param lyrics_dir: The lyrics cache directory to load, or
None to skip the lyrics capture layer.
:param wikidata_csv: The Wikidata artist snapshot CSV file
to apply, or None to skip the artist capture layer.
:return: None.
:raises BuildError: When ``lyrics_dir`` does not exist, or
a name in ``wikidata_csv`` matches no artist.
:raises OSError: When a capture file cannot be read.
"""
if lyrics_dir is not None:
if not lyrics_dir.is_dir():
raise BuildError(
f"{lyrics_dir}: no such directory")
self.__load_lyrics(lyrics_dir)
if wikidata_csv is not None:
self.__apply_artist_csv(wikidata_csv)
self.__session.flush()
def __load_lyrics(self, directory: Path) -> None:
"""Load the cached lyrics files into the matching songs.
A file whose stem is not an existing song ID is skipped with
a warning to the standard error.
A file whose stem is not an existing song ID is skipped
with a warning to the standard error.
:param session: The database session, with the songs flushed.
:param directory: The existing lyrics cache directory with
one ``<song_id>.txt`` file per song.
:return: None.
@@ -440,24 +479,21 @@ def load_lyrics(session: Session, directory: Path) -> None:
for path in sorted(directory.glob("*.txt")):
song: Song | None = None
if path.stem.isdigit():
song = session.get(Song, int(path.stem))
song = self.__session.get(Song, int(path.stem))
if song is None:
print(f"warning: {path}: no song with ID"
f" \"{path.stem}\"", file=sys.stderr)
continue
song.lyrics = path.read_text(encoding="utf-8")
def apply_artist_csv(session: Session, path: Path) -> None:
def __apply_artist_csv(self, path: Path) -> None:
"""Apply an artist attribute CSV onto the artist rows.
Artists match by exact name. Only the non-empty cells are
applied, field by field. The note column is ignored.
:param session: The database session, with the artists
flushed.
:param path: The CSV file with the columns name, qid, gender,
type, genre, country, and note.
:param path: The CSV file with the columns name, qid,
gender, type, genre, country, and note.
:return: None.
:raises BuildError: When a name matches no artist.
:raises OSError: When the file cannot be read.
@@ -465,12 +501,13 @@ def apply_artist_csv(session: Session, path: Path) -> None:
with open(path, encoding="utf-8", newline="") as file:
row: dict[str, str]
for row in csv.DictReader(file):
artist: Artist | None = session.scalar(
artist: Artist | None = self.__session.scalar(
sa.select(Artist)
.where(Artist.name == row["name"]))
if artist is None:
raise BuildError(
f"{path}: no artist named \"{row['name']}\"")
f"{path}: no artist named"
f" \"{row['name']}\"")
column: str
attribute: str
for column, attribute in ARTIST_FIELDS.items():
@@ -764,14 +801,8 @@ def main(argv: list[str] | None = None) -> int:
reset_store(session)
SongImporter(session).import_songs(args.chart_csv)
ArtistImporter(session).import_artists()
if args.lyrics_dir is not None:
if not args.lyrics_dir.is_dir():
raise BuildError(
f"{args.lyrics_dir}: no such directory")
load_lyrics(session, args.lyrics_dir)
if args.wikidata_csv is not None:
apply_artist_csv(session, args.wikidata_csv)
session.flush()
CaptureImporter(session).import_captures(
args.lyrics_dir, args.wikidata_csv)
violations: list[str] = find_violations(
session, YEARS, RANKS_PER_YEAR)
if len(violations) > 0: