Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ jobs:

- name: Install dependencies with Poetry
run: |
poetry install --no-interaction --with dev --without ML
poetry install --no-interaction --with dev --with db --without ML

- name: Migrate DB
run: |
alembic upgrade head
poetry run alembic upgrade head

- name: Run tests and generate coverage report
run: |
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,6 @@ marimo/_lsp/
__marimo__/

#IDE
.idea
.idea

postgres_container.test.yml
3 changes: 2 additions & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname

sqlalchemy.url = ...


[post_write_hooks]
Expand Down
19 changes: 10 additions & 9 deletions audiostats/db/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
import logging
from collections.abc import Iterator

from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
#from sqlalchemy.orm import sessionmaker, Session
from .uof import UnitOfWork
from .uow import UnitOfWork

from audiostats.handlers import AlbumDTO
from audiostats.db.session import SessionFactory


logger = logging.getLogger(__name__)

class DBApi:
def __init__(self, session_factory : async_sessionmaker[AsyncSession]):
self._session_factory = session_factory
def __init__(self, db_url : str):
self._session_factory = SessionFactory(db_url)

async def _upsert_album(self, album : AlbumDTO):
async with UnitOfWork(self._session_factory) as uow:
await uow.albums.upsert(album)
async with self._session_factory as sf:
async with UnitOfWork(sf()) as uow:
await uow.albums.upsert(album)

async def upsert_albums(self, albums : Iterator[AlbumDTO]):
batch = []
Expand All @@ -30,6 +30,7 @@ async def upsert_albums(self, albums : Iterator[AlbumDTO]):
await asyncio.gather(*batch)

async def get_all_albums(self):
async with UnitOfWork(self._session_factory) as uow:
return await uow.albums.all()
async with self._session_factory as sf:
async with UnitOfWork(sf()) as uow:
return await uow.albums.all()

1 change: 0 additions & 1 deletion audiostats/db/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ async def find_by_title_performer(self, title : str, performer : str | None) ->
selectinload(Album.tracks)
))
return result.scalar_one_or_none()
#return self._session.query(Album).filter(Album.title == title and Album.performer == performer if performer else Album.performer.is_(None)).first()

async def all(self) -> list[Album]:
result = await self._session.scalars(select(Album).options(
Expand Down
16 changes: 16 additions & 0 deletions audiostats/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession


class SessionFactory:
def __init__(self, db_url : str):
self._engine = create_async_engine(url=db_url)
self._session_maker = async_sessionmaker(bind=self._engine)

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._engine.dispose()

def __call__(self, *args, **kwargs) -> async_sessionmaker[AsyncSession]:
return self._session_maker
1 change: 0 additions & 1 deletion audiostats/db/uof.py → audiostats/db/uow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
#from sqlalchemy.orm import sessionmaker, Session
import logging

from .repositories import AlbumRepository
Expand Down
Empty file added audiostats/db/worker.py
Empty file.
7 changes: 6 additions & 1 deletion config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@
'-show_entries', 'format=duration',
'-of', 'default=nw=1:nokey=1',
'{path_to_file}'] #the last arg here is the audio.ape filepath, you should use it as in example


['DataBase']
AsyncUrl = 'postgresql+asyncpg://user:pass@localhost:5433/audiostats' #DB url with async driver, used to exchange the data with db
SyncUrl = 'postgresql+psycopg2://user:pass@localhost:5433/audiostats' #DB url with sync driver, used for migrations
UserName = 'user'
PassWord = 'pass'
12 changes: 11 additions & 1 deletion migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import os
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

from audiostats.db.models import Base

from dotenv import load_dotenv

load_dotenv()

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

config.set_main_option('sqlalchemy.url', os.getenv('SYNC_DB_URL'))


# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
Expand All @@ -18,7 +28,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand Down
55 changes: 55 additions & 0 deletions migrations/versions/bd8d699fa622_album_and_track_tables_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""album and track tables init

Revision ID: bd8d699fa622
Revises:
Create Date: 2025-09-12 23:16:13.273814

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'bd8d699fa622'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('albums',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=50), nullable=False),
sa.Column('performer', sa.String(length=50), nullable=True),
sa.Column('year', sa.Integer(), nullable=True),
sa.Column('path', sa.String(length=200), nullable=True),
sa.Column('cover', sa.String(length=200), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('performer', 'title', name='uq_album_performer_title')
)
op.create_table('tracks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=50), nullable=False),
sa.Column('album_id', sa.Integer(), nullable=False),
sa.Column('number', sa.Integer(), nullable=True),
sa.Column('path', sa.String(length=200), nullable=True),
sa.Column('offset', sa.Float(), nullable=True),
sa.Column('duration', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['album_id'], ['albums.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tracks_album_id'), 'tracks', ['album_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_tracks_album_id'), table_name='tracks')
op.drop_table('tracks')
op.drop_table('albums')
# ### end Alembic commands ###
22 changes: 21 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"tomlcfg (>=0.1.0,<0.2.0)",
"greenlet (>=3.2.4,<4.0.0)",
"asyncpg (>=0.30.0,<0.31.0)",
"psycopg2 (>=2.9.10,<3.0.0)",
]

[build-system]
Expand Down
44 changes: 37 additions & 7 deletions tests/test_db_api.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
import os

import pytest
import dotenv

from audiostats.db.api import DBApi
from audiostats.handlers import TrackDTO
from tests import test_session_factory, test_engine#, setup_database

@pytest.mark.asyncio
async def test_upsert_albums(test_session_factory, processed_album_dtos):
session_factory = await test_session_factory
api = DBApi(session_factory)
async def test_async_upsert_albums(processed_album_dtos):
dotenv.load_dotenv()
db_url = os.getenv('ASYNC_DB_URL')

api = DBApi(db_url)
await api.upsert_albums(processed_album_dtos)

albums = await api.get_all_albums()

assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(key=lambda x: x.title), 'Insert some albums to db'
assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(
key=lambda x: x.title), 'Insert some albums to db'

processed_album_dtos[0].tracks.pop(-1)
processed_album_dtos[0].tracks.pop(2)
processed_album_dtos[1].tracks.append(TrackDTO(title='New_Track_1', number=6, path='music/new_track_1.flac', offset=None, duration=None))
processed_album_dtos[1].tracks.append(
TrackDTO(title='New_Track_1', number=6, path='music/new_track_1.flac', offset=None, duration=None))
processed_album_dtos[1].year = None

await api.upsert_albums(processed_album_dtos)

albums = await api.get_all_albums()

assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(key=lambda x: x.title), 'Update some albums in db'
assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(
key=lambda x: x.title), 'Update some albums in db'



# @pytest.mark.asyncio
# async def test_upsert_albums(test_session_factory, processed_album_dtos):
# session_factory = await test_session_factory
# api = DBApi(session_factory)
#
# await api.upsert_albums(processed_album_dtos)
#
# albums = await api.get_all_albums()
#
# assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(key=lambda x: x.title), 'Insert some albums to db'
#
# processed_album_dtos[0].tracks.pop(-1)
# processed_album_dtos[0].tracks.pop(2)
# processed_album_dtos[1].tracks.append(TrackDTO(title='New_Track_1', number=6, path='music/new_track_1.flac', offset=None, duration=None))
# processed_album_dtos[1].year = None
#
# await api.upsert_albums(processed_album_dtos)
#
# albums = await api.get_all_albums()
#
# assert albums.sort(key=lambda x: x.title) == processed_album_dtos.sort(key=lambda x: x.title), 'Update some albums in db'



Expand Down