Add the data layer with the build-db, fetch-lyrics, and fetch-artists subcommands

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 15:12:22 +08:00
co-authored by Claude Fable 5
parent f9ab79f0c2
commit 8d223177cd
17 changed files with 2320 additions and 9 deletions
+106
View File
@@ -0,0 +1,106 @@
# 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 database connection.
"""
from functools import cached_property
from pathlib import Path
from typing import Any
import sqlalchemy as sa
from sqlalchemy.orm import DeclarativeBase, sessionmaker, Session
from .config import Settings, get_settings
class Base(DeclarativeBase):
"""The base data model."""
class DataSource:
"""A data source."""
@cached_property
def engine(self) -> sa.Engine:
"""Returns the database engine.
:return: The database engine.
"""
settings: Settings = get_settings()
return self.__create_engine(settings.SQLALCHEMY_DATABASE_URL)
@cached_property
def __session_local(self) -> sessionmaker:
"""Returns the callable to connect and return the database session.
:return: The callable to connect and return the database session.
"""
return sessionmaker(
autocommit=False, autoflush=False, bind=self.engine)
def get_db(self) -> Session:
"""Connects and returns the database session.
:return: The database session.
"""
return self.__session_local()
@classmethod
def __create_engine(cls, url: str) -> sa.Engine:
"""Constructs and returns the database engine.
The foreign key enforcement is enabled on every connection
of a SQLite engine.
:param url: The SQLAlchemy database URL.
:return: The database engine.
"""
url = cls.__resolve_sqlite_relative_url(url)
engine: sa.Engine
if url == "sqlite://":
engine = sa.create_engine(
url, connect_args={"check_same_thread": False},
poolclass=sa.StaticPool)
else:
engine = sa.create_engine(url)
if engine.url.get_backend_name() == "sqlite":
sa.event.listen(engine, "connect",
cls.__enable_sqlite_foreign_keys)
return engine
@staticmethod
def __enable_sqlite_foreign_keys(dbapi_connection: Any,
_: Any) -> None:
"""Enables the foreign key enforcement on a new connection.
:param dbapi_connection: The DBAPI connection.
:param _: The connection record (unused).
:return: None.
"""
cursor: Any = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
@staticmethod
def __resolve_sqlite_relative_url(url: str) -> str:
"""Resolves the SQLite relative URL to the instance folder.
:param url: The SQLAlchemy database URL.
:return: The resolved SQLAlchemy database URL.
"""
if not url.startswith("sqlite:///"):
return url
path: Path = Path(url[len("sqlite:///"):])
if path.is_absolute():
return url
base: Path = Path(__file__).parent.parent
if base.name == "src":
base = base.parent
path = base / "instance" / path
return f"sqlite:///{path}"
ds: DataSource = DataSource()
"""The data source."""