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:
@@ -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."""
|
||||
Reference in New Issue
Block a user