Package the capture-layer application into a CaptureImporter class
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -206,8 +206,10 @@
|
||||
(身分判定、拆解、正名)隨行入 class 作公開 staticmethod
|
||||
/class 常數,「哪個函式屬哪個工作」由 class 歸屬直接
|
||||
表達。flush 定為匯入工作的完工契約——entry method 返回
|
||||
時自身寫入已可查詢,不再由呼叫者補 flush。實測重構前後
|
||||
工作儲存 dump 與衍生報表逐位元組相同。
|
||||
時自身寫入已可查詢,不再由呼叫者補 flush。捕捉層套用
|
||||
(歌詞、Wikidata 快照)隨後同型打包為 `CaptureImporter`
|
||||
(單一入口收兩個可省略路徑)。實測重構前後工作儲存
|
||||
dump 與衍生報表逐位元組相同。
|
||||
- **歌手型態刪去 mixed 值**:`ArtistType` 只留 solo/group。
|
||||
mixed 是先導研究「男/女/混合團體」單一欄位的殘留,
|
||||
正式設計拆成 gender+type 後從未定義其指涉;署名一律
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user